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

14May/1016

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

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

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

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

STSADM Commands

PowerShell Cmdlets

Notes

gl-activatefeature

Enable-SPFeature2

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

gl-addaudiencerule

New-SPAudienceRule

 

gl-addavailablesitetemplate

  I’ll eventually create a cmdlet for this.

gl-adduser2

  Use the OOTB New-SPUser cmdlet.

gl-adduserpolicyforwebapp

Add-SPWebApplicationUserPolicy

 

gl-applytheme

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

gl-backup

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

gl-backupsites

Backup-SPSite2

Extends Backup-SPSite by including IIS settings.

gl-convertsubsitetositecollection

ConvertTo-SPSite

 

gl-copycontenttypes

Copy-SPContentType

 

gl-copylist

Copy-SPList

 

gl-copylistsecurity

Copy-SPListSecurity

 

gl-createaudience

New-SPAudience

 

gl-createcontentdb

  Use the OOTB New-SPContentDatabase cmdlet.

gl-createpublishingpage

New-SPPublishingPage

 

gl-createquotatemplate

New-SPQuotaTemplate

 

gl-createwebapp

  Use the OOTB New-SPWebApplication cmdlet

gl-deactivatefeature

Disable-SPFeature2

Extends the OOTB Disable-SPFeature cmdlet.

gl-deleteallusers

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

gl-deleteaudience

Remove-SPAudience

 

gl-deletelist

Remove-SPList

 

gl-deletewebapp

  Use the OOTB Remove-SPWebApplication cmdlet.

gl-disableuserpermissionforwebapp

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

gl-editquotatemplate

Set-SPQuotaTemplate

 

gl-enableuserpermissionforwebapp

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

gl-enumaudiencerules

Export-SPAudienceRules

 

gl-enumavailablepagelayouts

Get-SPPublishingPageLayout

 

gl-enumavailablesitetemplates

Get-SPAvailableWebTemplates

 

gl-enumeffectivebaseperms

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

gl-enumfeatures

  Use the OOTB Get-SPFeature cmdlet.

gl-enuminstalledsitetemplates

  Use the OOTB Get-SPWebTemplate cmdlet.

gl-enumpagewebparts

Get-SPWebPartList

 

gl-enumunghostedfiles

Get-SPCustomizedPages

 

gl-execadmsvcjobs

Start-SPAdminJob2

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

gl-exportaudiences

Export-SPAudiences

 

gl-exportcontenttypes

Export-SPContentType

 

gl-exportlist

Export-SPWeb2

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

gl-exportlistsecurity

Export-SPListSecurity

 

gl-extendwebapp

  Use the OOTB New-SPWebApplicationExtension cmdlet.

gl-fixpublishingpagespagelayouturl

Repair-SPPageLayoutUrl

 

gl-importaudiences

Import-SPAudiences

 

gl-importlist

Import-SPWeb2

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

gl-importlistsecurity

Import-SPListSecurity

 

gl-listaudiencetargeting

Set-SPListAudienceTargeting

 

gl-managecontentdbsettings

  Use the OOTB Set-SPContentDatabase cmdlet.

gl-propagatecontenttype

Propagate-SPContentType

 

gl-publishitems

Publish-SPListItems

 

gl-reghostfile

Reset-SPCustomizedPages

 

gl-removeavailablesitetemplate

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

gl-repairsitecollectionimportedfromsubsite

Repair-SPSite

 

gl-replacewebpartcontent

Replace-SPWebPartContent

 

gl-setbackconnectionhostnames

Set-SPBackConnectionHostNames

 

gl-setselfservicesitecreation

  Not sure if I’ll migrate this or not.

gl-syncquotas

Set-SPQuota

 

gl-tracelog

  Use the OOTB Set-SPDiagnosticConfig cmdlet.

gl-unextendwebapp

  Use the OOTB Remove-SPWebApplication cmdlet.
 

Get-SPAudience

 
 

Get-SPAudienceManager

 
 

Get-SPContentType

 
 

Get-SPFile

 
 

Get-SPLimitedWebPartManager

 
 

Get-SPList

 
 

Get-SPPublishingPage

 
 

Get-SPQuotaTemplate

 
 

Set-SPAudience

 

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

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

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

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

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

20Sep/099

Setting Back Connection Host Names for SharePoint 2007 Using STSADM

Not too long ago Microsoft introduced a security fix which addresses a possible attack vector in which malicious software tries to impersonate a local request, thereby bypassing certain constraints.  The problem with this fix is that it introduces some issues for SharePoint servers, effectively resulting in 401.1 Access Denied errors.  Spence Harbar does a great write-up of the fix and the options available to get your SharePoint environment working again so I won't re-hash all that here: http://www.harbar.net/archive/2009/07/02/disableloopbackcheck-amp-sharepoint-what-every-admin-and-developer-should-know.aspx.

As Spence points out, the preferred way to fix this is to add the host names to the BackConnectionHostNames registry key and to not set the DisableLoopbackCheck registry key.  You can of course do this using Group Policy but for those not managing their servers using GPO I decided to implement a custom STSADM command that would make setting the BackConnectionHostNames registry key really simple.  I called this new command, oddly enough, gl-setbackconnectionhostnames.

The command has two ways to run it, you can run it without any parameters which will cause it to update only the server in which the command is executed on, or you can pass in an -updatefarm parameter along with a username and password which will cause it to update every server in the farm.  There's no need to pass in the host header names as the code will dynamically determine them by inspecting each web application and their alternate access mappings (alternate URLs) and perform some logic to determine whether the host header is pointing to a local IP address or to a specific SharePoint server (I do this to exclude Central Admin which is usually accessed using a server name and non-standard port).

I accomplish the farm update by using a custom Timer Job which executes on each server.  Unfortunately the timer service account does not have access to write to the registry (unless you've given it rights, which you shouldn't) so it was necessary to pass in a username and password and then use impersonation to update the registry.  The custom timer job code is shown below, notice that all the core work is being done via the SetBackConnectionHostNames class which is shown below the timer job code:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Runtime.InteropServices;
   4: using System.Security.Principal;
   5: using System.Text;
   6: using Microsoft.SharePoint;
   7: using Microsoft.SharePoint.Administration;
   8:  
   9: namespace Lapointe.SharePoint.STSADM.Commands.WebApplications
  10: {
  11:     public class SetBackConnectionHostNamesTimerJob : SPJobDefinition
  12:     {
  13:         public const int LOGON32_LOGON_INTERACTIVE = 2;
  14:         public const int LOGON32_LOGON_SERVICE = 3;
  15:         public const int LOGON32_PROVIDER_DEFAULT = 0;
  16:  
  17:         [DllImport("advapi32.dll", CharSet = CharSet.Auto)]
  18:         public static extern bool LogonUser(
  19:           String lpszUserName,
  20:           String lpszDomain,
  21:           String lpszPassword,
  22:           int dwLogonType,
  23:           int dwLogonProvider,
  24:           ref IntPtr phToken
  25:         );
  26:  
  27:         [DllImport("kernel32.dll", CharSet = CharSet.Auto)]
  28:         public extern static bool CloseHandle(IntPtr handle);
  29:  
  30:         private const string JOB_NAME = "job-set-back-connection-host-names-";
  31:         private const string KEY_USER = "userName";
  32:         private const string KEY_PWD = "password";
  33:  
  34:         private static readonly string jobId = Guid.NewGuid().ToString();
  35:  
  36:         public SetBackConnectionHostNamesTimerJob() : base() { }
  37:  
  38:         /// <summary>
  39:         /// Initializes a new instance of the <see cref="SetBackConnectionHostNamesTimerJob"/> class.
  40:         /// </summary>
  41:         public SetBackConnectionHostNamesTimerJob(SPService service)
  42:             : base(JOB_NAME + jobId, service, null, SPJobLockType.None)
  43:         {
  44:             Title = "Set BackConnectionHostNames Registry Key";
  45:         }
  46:  
  47:         /// <summary>
  48:         /// Executes the job definition.
  49:         /// </summary>
  50:         /// <param name="targetInstanceId">For target types of <see cref="T:Microsoft.SharePoint.Administration.SPContentDatabase"></see> this is the database ID of the content database being processed by the running job. This value is Guid.Empty for all other target types.</param>
  51:         public override void Execute(Guid targetInstanceId)
  52:         {
  53:             string user = Properties[KEY_USER] as string;
  54:             string password = Properties[KEY_PWD] as string;
  55:  
  56:             if (string.IsNullOrEmpty(user) || password == null)
  57:                 throw new ArgumentNullException("Username and password is required.");
  58:  
  59:             if (user.IndexOf('\\') < 0)
  60:                 throw new ArgumentException("Username must be in the form \"DOMAIN\\USER\"");
  61:  
  62:             IntPtr userHandle = new IntPtr(0);
  63:             WindowsImpersonationContext impersonatedUser = null;
  64:  
  65:             bool returnValue = LogonUser(
  66:               user.Split('\\')[1],
  67:               user.Split('\\')[0],
  68:               password,
  69:               LOGON32_LOGON_INTERACTIVE,
  70:               LOGON32_PROVIDER_DEFAULT,
  71:               ref userHandle
  72:               );
  73:  
  74:             if (!returnValue)
  75:             {
  76:                 throw new Exception("Invalid Username");
  77:             }
  78:             WindowsIdentity newId = new WindowsIdentity(userHandle);
  79:             impersonatedUser = newId.Impersonate();
  80:  
  81:             SetBackConnectionHostNames.SetBackConnectionRegKey(SetBackConnectionHostNames.GetUrls());
  82:  
  83:             impersonatedUser.Undo();
  84:             CloseHandle(userHandle);
  85:         }
  86:  
  87:         /// <summary>
  88:         /// Submits the job.
  89:         /// </summary>
  90:         public void SubmitJob(string user, string password)
  91:         {
  92:             Properties[KEY_USER] = user;
  93:             Properties[KEY_PWD] = password;
  94:             Schedule = new SPOneTimeSchedule(DateTime.Now);
  95:             Update();
  96:         }
  97:     }
  98: }

The following is the code for the SetBackConnectionHostNames class:

   1: using System;
   2: using System.Collections;
   3: using System.Collections.Generic;
   4: using System.Collections.Specialized;
   5: using System.Net;
   6: using System.Text;
   7: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
   8: using Microsoft.SharePoint;
   9: using Microsoft.SharePoint.Administration;
  10: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
  11: using Microsoft.Win32;
  12:  
  13: namespace Lapointe.SharePoint.STSADM.Commands.WebApplications
  14: {
  15:     public class SetBackConnectionHostNames : SPOperation
  16:     {
  17:         /// <summary>
  18:         /// Initializes a new instance of the <see cref="SetBackConnectionHostNames"/> class.
  19:         /// </summary>
  20:         public SetBackConnectionHostNames()
  21:         {
  22:             
  23:             SPParamCollection parameters = new SPParamCollection();
  24:             parameters.Add(new SPParam("updatefarm", "uf"));
  25:             parameters.Add(new SPParam("username", "user", false, null, new SPNonEmptyValidator()));
  26:             parameters.Add(new SPParam("password", "pwd", false, null, new SPNullOrNonEmptyValidator()));
  27:  
  28:             StringBuilder sb = new StringBuilder();
  29:             sb.Append("\r\n\r\nSets the BackConnectionHostNames registry key with the URLs associated with each web application.\r\n\r\nParameters:");
  30:             sb.Append("\r\n\t[-updatefarm (update all servers in the farm)]");
  31:             sb.Append("\r\n\t[-username <DOMAIN\\user (must have rights to update the registry on each server)>]");
  32:             sb.Append("\r\n\t[-password <password>]");
  33:  
  34:             Init(parameters, sb.ToString());
  35:         }
  36:  
  37:         #region ISPStsadmCommand Members
  38:  
  39:         /// <summary>
  40:         /// Gets the help message.
  41:         /// </summary>
  42:         /// <param name="command">The command.</param>
  43:         /// <returns></returns>
  44:         public override string GetHelpMessage(string command)
  45:         {
  46:             return HelpMessage;
  47:         }
  48:  
  49:         /// <summary>
  50:         /// Runs the specified command.
  51:         /// </summary>
  52:         /// <param name="command">The command.</param>
  53:         /// <param name="keyValues">The key values.</param>
  54:         /// <param name="output">The output.</param>
  55:         /// <returns></returns>
  56:         public override int Execute(string command, StringDictionary keyValues, out string output)
  57:         {
  58:             output = string.Empty;
  59:  
  60:             if (!Params["updatefarm"].UserTypedIn)
  61:                 SetBackConnectionRegKey(GetUrls());
  62:             else
  63:             {
  64:                 SPTimerService timerService = SPFarm.Local.TimerService;
  65:                 if (null == timerService)
  66:                 {
  67:                     throw new SPException("The Farms timer service cannot be found.");
  68:                 }
  69:                 SetBackConnectionHostNamesTimerJob job = new SetBackConnectionHostNamesTimerJob(timerService);
  70:  
  71:                 string user = Params["username"].Value;
  72:                 if (user.IndexOf('\\') < 0)
  73:                     user = Environment.UserDomainName + "\\" + user;
  74:                 job.SubmitJob(user, Params["password"].Value + "");
  75:  
  76:                 output += "Timer job successfully created.";
  77:             }
  78:  
  79:             return OUTPUT_SUCCESS;
  80:         }
  81:  
  82:         public override void Validate(StringDictionary keyValues)
  83:         {
  84:             base.Validate(keyValues);
  85:  
  86:             if (Params["updatefarm"].UserTypedIn)
  87:             {
  88:                 if (!Params["username"].UserTypedIn)
  89:                     throw new SPSyntaxException("A valid username with rights to edit the registry is required.");
  90:             }
  91:         }
  92:  
  93:         #endregion
  94:  
  95:         public static List<string> GetUrls()
  96:         {
  97:             List<string> urls = new List<string>();
  98:             foreach (SPService svc in SPFarm.Local.Services)
  99:             {
 100:                 if (!(svc is SPWebService))
 101:                     continue;
 102:  
 103:                 foreach (SPWebApplication webApp in ((SPWebService)svc).WebApplications)
 104:                 {
 105:                     
 106:                     foreach (SPAlternateUrl url in webApp.AlternateUrls)
 107:                     {
 108:                         string host = url.Uri.Host.ToLower();
 109:                         if (!urls.Contains(host) && // Don't add if we already have it
 110:                             !url.Uri.IsLoopback && // Quick check to short circuit the more elaborate checks
 111:                             host != Environment.MachineName.ToLower() && // Quick check to short circuit the more elaborate checks
 112:                             IsLocalIpAddress(host) && // If the host name points locally then we need to add it
 113:                             !IsSharePointServer(host)) // Don't add if it matches an SP server name (handles central admin)
 114:                         {
 115:                             urls.Add(host);
 116:                         }
 117:                     }
 118:                 }
 119:             }
 120:             return urls;
 121:         }
 122:  
 123:         private static bool IsSharePointServer(string host)
 124:         {
 125:             foreach (SPServer server in SPFarm.Local.Servers)
 126:             {
 127:                 if (server.Address.ToLower() == host)
 128:                     return true;
 129:             }
 130:             return false;
 131:         }
 132:  
 133:         private static bool IsLocalIpAddress(string host)
 134:         {
 135:             try
 136:             { 
 137:                 IPAddress[] hostIPs = Dns.GetHostAddresses(host);
 138:                 IPAddress[] localIPs = Dns.GetHostAddresses(Dns.GetHostName());
 139:  
 140:                 // test if any host IP equals to any local IP or to localhost
 141:                 foreach (IPAddress hostIP in hostIPs)
 142:                 {
 143:                     // is localhost
 144:                     if (IPAddress.IsLoopback(hostIP)) return true;
 145:                     // is local address
 146:                     foreach (IPAddress localIP in localIPs)
 147:                     {
 148:                         if (hostIP.Equals(localIP)) return true;
 149:                     }
 150:                 }
 151:             }
 152:             catch { }
 153:             return false;
 154:         }
 155:  
 156:         public static void SetBackConnectionRegKey(List<string> urls)
 157:         {
 158:             const string KEY_NAME = "SYSTEM\\CurrentControlSet\\Control\\Lsa\\MSV1_0";
 159:             const string KEY_VAL_NAME = "BackConnectionHostNames";
 160:  
 161:             RegistryKey reg = Registry.LocalMachine.OpenSubKey(KEY_NAME, true);
 162:             if (reg != null)
 163:             {
 164:                 string[] existing = (string[])reg.GetValue(KEY_VAL_NAME);
 165:                 if (existing != null)
 166:                 {
 167:                     foreach (string val in existing)
 168:                     {
 169:                         if (!urls.Contains(val.ToLower()))
 170:                             urls.Add(val.ToLower());
 171:                     }
 172:                 }
 173:                 string[] multiVal = new string[urls.Count];
 174:                 urls.CopyTo(multiVal);
 175:                 
 176:                 reg.SetValue(KEY_VAL_NAME, multiVal, RegistryValueKind.MultiString);
 177:             }
 178:             else
 179:             {
 180:                 throw new SPException("Unable to open registry key.");
 181:             }
 182:         }
 183:  
 184:     }
 185: }

There's two core methods, GetUrls and SetBackConnectionRegKey.  The SetBackConnectionRegKey method started out from a bit of sample code that my friend Ben Robb sent me - there's no much of his original code but it saved me some time in trying to remember how to manipulate the registry using C#.  Essentially all this method does is get the current list of host names, add any missing items to the passed in list, and then reset the list (thus avoiding duplicate entries).  The GetUrls method is the more interesting piece - I'm looping through all the Farm's Web Applications and their corresponding Alternate URLs and then building a list of URLs that meet some basic inclusion criteria:

  • Don't add duplicates - you can get duplicates when both HTTP and HTTPS are used so we make sure that we exclude them
  • Don't add loopback URLs - this shouldn't come up but if the URL is localhost or 127.0.0.1 it will be flagged as a loopback URL so we exclude them
  • Don't add URLs that match the server name - if the host name matches the server name then exclude it (this is essentially just a short circuit for the next check which is a bit more thorough)
  • Exclude host names that map to the local IP address - this is the most crucial bit (the previous steps were just short circuits for this step to avoid the additional querying necessary); I use the System.Net.Dns class's static GetHostAddresses method to check the local addresses against those associated with the provided host name
  • Exclude host names that map to SharePoint servers - this step is necessary to address host names such as those belonging to Central Administration

The help for the command is shown below:

C:\>stsadm -help gl-setbackconnectionhostnames

stsadm -o gl-setbackconnectionhostnames


Sets the BackConnectionHostNames registry key with the URLs associated with each web application.
Parameters:
        [-updatefarm (update all servers in the farm)]
        [-username <DOMAIN\user (must have rights to update the registry on each server)>]
        [-password <password>]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-setbackconnectionhostnames WSS v3, MOSS 2007 Released: 9/20/2009

Parameter Name Short Form Required Description Example Usage
updatefarm uf No If provided then update the BackConnectionHostNames registry key on all servers in the farm. -updatefarm

-uf
username user Yes if updatefarm is provided The username with sufficient rights to update the registry.  If no domain part is specified then the current users domain is used. -username domain\spadmin

-user spadmin
password pwd No If the users password is blank then this parameter is not required (please change your password if this is the case!); otherwise, this parameter is required if the updatefarm parameter is provided. -password pa$$w0rd

-pwd pa$$w0rd

The following is an example of how to update the BackConnectionHostNames registry key on the current server only:

stsadm -o gl-setbackconnectionhostnames

The following is an example of how to update the BackConnectionHostNames registry key on all servers in the farm:

stsadm -o gl-setbackconnectionhostnames -updatefarm -username domain\spadmin -password pa$$w0rd

9Jul/0918

Custom SharePoint 2007 Site Collection Creation Page

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

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

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

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

  1. Grant your application pool accounts appropriate rights to the configuration database.  This approach is not recommended as you are violating the concept of least privileges and potentially exposing sensitive information and risking corruption if your application pool should become compromised.
  2. Create a custom windows service that runs as the farm account and uses .NET remoting to communicate tasks.  If you think you'll have lots of operations requiring privileged access then this is potentially a good way to go, but it introduces are high degree of complexity and requires an installer to be run on every server in the farm.  SharePoint uses this approach with its implementation of the "Windows SharePoint Services Administration" service (SPAdmin).  The OOTB scsignup.aspx page uses this service to handle the creation of the site collection and thus get around the security restrictions.  Unfortunately there's no way for us to leverage this service by having our own code run using it (like we can with custom timer jobs and the SPTimerV3 service).
  3. Create a virtual application under the _layouts folder of each web application and have it run using the SCA application pool.  Using this approach you can put the site collection creation application page under the virtual application and thus get the credentials required to edit the configuration database.  The problem with this approach is that you once again must touch not only every server but every web application, which defeats the purpose of using WSP packages for solution deployment.
  4. Direct all site collection requests to an application page under the SCA site and pass in target values.  This approach gets around a lot of the issues described above (simple to deploy, runs with an account having the appropriate permissions, etc.).  The problem is that you must now expose your SCA site to everyone and you must grant the "Browse User Information" right to everyone.
  5. Call a web service running under the SCA's _layouts folder.  The nice thing about this approach is that it is simple to deploy (standard WSP deployment from a single server updates all existing servers and any new servers), easy to create and debug, and simple to maintain.  The only downside is that it requires that your WFE servers be able to access the SCA web site.  The upside is that you don't have to expose this to everyone - just the WFE servers, and you don't need to grant the "Browse User Information" right as your application pool accounts should have the appropriate rights already.  You can also get around high availability issues by having the SCA site run on each server (see Spence's article on high availability of SCA: http://www.harbar.net/articles/spca.aspx).

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

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

<%@ Assembly Name="Microsoft.SharePoint.ApplicationPages, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c"%>
<%@ Page Language="C#" Inherits="Lapointe.SharePoint.SiteCollectionProvisioner.ApplicationPages.CreateSitePage, Lapointe.SharePoint.SiteCollectionProvisioner, Version=1.0.0.0, Culture=neutral, PublicKeyToken=29b13c54ceef5193" MasterPageFile="~/_layouts/simple.master"%>
<%@ Import Namespace="Microsoft.SharePoint.ApplicationPages" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="Utilities" Namespace="Microsoft.SharePoint.Utilities" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Import Namespace="Microsoft.SharePoint" %>
<%@ Register TagPrefix="wssuc" TagName="InputFormSection" src="~/_controltemplates/InputFormSection.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="InputFormControl" src="~/_controltemplates/InputFormControl.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="ButtonSection" src="~/_controltemplates/ButtonSection.ascx" %>
<%@ Register TagPrefix="wssuc" TagName="TemplatePickerControl" src="~/_controltemplates/TemplatePickerControl.ascx" %>
<%@ Register Tagprefix="wssawc" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register Tagprefix="SharePoint" Namespace="Microsoft.SharePoint.WebControls" Assembly="Microsoft.SharePoint, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c" %>
<%@ Register TagPrefix="wssuc" TagName="Welcome" src="~/_controltemplates/Welcome.ascx" %>
<asp:Content ID="Content1" contentplaceholderid="PlaceHolderPageTitle" runat="server">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,scsignup_pagetitle%>" EncodeMethod='HtmlEncode'/>
</asp:Content>
<asp:Content ID="Content2" contentplaceholderid="PlaceHolderPageTitleInTitleArea" runat="server">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,scsignup_pagetitle%>" EncodeMethod='HtmlEncode'/>
</asp:Content>
<asp:Content ID="Content3" contentplaceholderid="PlaceHolderPageDescription" runat="server">
<SharePoint:EncodedLiteral runat="server" text="<%$Resources:wss,scsignup_pagedescription%>" EncodeMethod='HtmlEncode'/>
</asp:Content>
<asp:Content ID="Content4" contentplaceholderid="PlaceHolderAdditionalPageHead" runat="server">
<script src="/_layouts/<%=System.Threading.Thread.CurrentThread.CurrentUICulture.LCID%>/commonvalidation.js"></script>

<script Language="javascript">

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

</script>

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


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

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

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

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


#region Properties

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

#region Event Handlers

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

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

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

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

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

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

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

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

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

string portalUrl;
string portalName;

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

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

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

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

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

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

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

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

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

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

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

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

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

});

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

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

base.OnLoad(e);

SPWebApplication webApplication = SPContext.Current.Site.WebApplication;

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


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

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

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

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

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

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

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

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

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

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


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


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

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

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

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

#endregion

#region Helper Methods

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

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

source.OnFormSave(site);
}
}

#endregion

}

}

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

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

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

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

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

Logger.WriteException(ex);
}

}
}
}

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

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

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

namespace Lapointe.SharePoint.SiteCollectionProvisioner
{
public class Utilities
{


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

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


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

SPWebApplication webApp = SPWebApplication.Lookup(webAppUri);

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

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

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

}

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

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

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

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

return targetDb;
}

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

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

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

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

return quota;
}



}
}

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

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

Parameter Name Description Example Usage
Data Allows the passing of arbitrary data through the site creation process.  The provided string value is appended to the return URL as a querystring parameter named Data. /_Layouts/SCP/CreateSite.aspx?Data=480E13C2-DCA5-4a76-ACE1-10A82F7181B2
ReturnUrl The URL to return to after creating the site collection. /_Layouts/SCP/CreateSite.aspx?ReturnUrl=http%3A%2F%2Fportal%2Fdepartments%2FHR
PortalName The name of the portal site to link back to.  This sets the SPWeb.PortalName property. /_Layouts/SCP/CreateSite.aspx?PortalName=Main%20Portal
PortalUrl The URL of the portal site to link back to.  This sets the SPWeb.PortalUrl property. /_Layouts/SCP/CreateSite.aspx?PortalUrl=http%3A%2F%2Fportal%2F
UrlName The name to use for the site collection URL. /_Layouts/SCP/CreateSite.aspx?UrlName=hrteamsite
Title The title of the site collection to create. /_Layouts/SCP/CreateSite.aspx?Title=HR%20Team%20Site
SscPrefix The managed path to create the site under.  Specifying this value will prevent the user from choosing a different value. /_Layouts/SCP/CreateSite.aspx?SscPrefix=departments
Template The site template to use when creating the site collection.  Specifying this parameter will hide the site template picker from the form. /_Layouts/SCP/CreateSite.aspx?Template=STS%230
SiteDirectory The site directory used to store an entry for the new site collection. /_Layouts/SCP/CreateSite.aspx?SiteDirectory=http%3A%2F%2Fportal%2FSiteDirectory&EnforceListing=False&EntryReq=1
EnforceListing Indicates whether an entry in the site directory is required.  Valid values are "True" or "False". (see above)
EntryReq The site directory entry requirement specifies whether all, none, or at least one of the fields is required.  Valid values are 0=none, 1=at least one category, 2=all categories. (see above)

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

8Jul/090

Download My Custom Extensions Source From CodePlex

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

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

6Jul/092

Setting List Properties using STSADM

Recently I had a reader of my blog send me a modified version of my gl-addlist command in which he added some additional properties to set a couple of the SPList properties (specifically the versioning settings).  In thinking about this I decided that it might be helpful to have a command specifically for setting most of the SPList properties rather than try to incorporate them into a more general command like the gl-addlist command so I decided to create a new command that I called gl-setlistproperties.

Now ideally you would just use PowerShell to set list properties and you can do so pretty easily using my custom CmdLet: Get-SPList.  Here’s an example of how you could do this using my CmdLet:

$list = Get-SPList http://portal/documents
$list.EnableVersioning = $true
$list.EnableMinorVersions = $true
$list.EnableModeration = $true
$list.Update()

For a full list of the properties that you can set see the SPList documentation in the SDK. But if you’re not a PowerShell guy and prefer batch files then you can use my gl-setlistproperties command instead. The code, as you can imagine, is really simple:

   1: using System;
   2: using System.Collections.Specialized;
   3: using System.Text;
   4: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
   5: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
   6: using Microsoft.SharePoint;
   7:  
   8: namespace Lapointe.SharePoint.STSADM.Commands.Lists
   9: {
  10:     public class SetListProperties : SPOperation
  11:     {
  12:         internal enum VersionSettings {None, Major, MajorMinor}
  13:         /// <summary>
  14:         /// Initializes a new instance of the <see cref="SetListProperties"/> class.
  15:         /// </summary>
  16:         public SetListProperties()
  17:         {
  18:             SPParamCollection parameters = new SPParamCollection();
  19:             parameters.Add(new SPParam("url", "url", true, null, new SPUrlValidator(), "Please specify URL to the list."));
  20:             SPEnumValidator versionValidator = new SPEnumValidator(typeof(VersionSettings));
  21:             parameters.Add(new SPParam("versioning", "version", false, null, versionValidator, "Please specify the version settings."));
  22:             SPEnumValidator draftVisibilityValidator = new SPEnumValidator(typeof(DraftVisibilityType));
  23:             parameters.Add(new SPParam("draftvisibility", "dvv", false, null, draftVisibilityValidator));
  24:             parameters.Add(new SPParam("majorversionlimit", "mvl", false, "0", new SPIntRangeValidator(0, 50000)));
  25:             parameters.Add(new SPParam("majorwithminorversionlimit", "mmvl", false, "0", new SPIntRangeValidator(0, 50000)));
  26:             parameters.Add(new SPParam("forcecheckout", "fc", false, null, new SPTrueFalseValidator()));
  27:             parameters.Add(new SPParam("enablemoderation", "mod", false, null, new SPTrueFalseValidator()));
  28:             parameters.Add(new SPParam("enablecontenttypes", "ect", false, null, new SPTrueFalseValidator()));
  29:             parameters.Add(new SPParam("enablefoldercreation", "efc", false, null, new SPTrueFalseValidator()));
  30:             parameters.Add(new SPParam("allowdeletion", "del", false, null, new SPTrueFalseValidator()));
  31:             parameters.Add(new SPParam("alloweveryoneviewitems", "aevi", false, null, new SPTrueFalseValidator()));
  32:             parameters.Add(new SPParam("enablesyndication", "syn", false, null, new SPTrueFalseValidator()));
  33:             parameters.Add(new SPParam("hidden", "hidden", false, null, new SPTrueFalseValidator()));
  34:             parameters.Add(new SPParam("onquicklaunch", "oql", false, null, new SPTrueFalseValidator()));
  35:             SPEnumValidator itemOpenValidator = new SPEnumValidator(typeof(DefaultItemOpen));
  36:             parameters.Add(new SPParam("defaultitemopen", "open", false, null, itemOpenValidator));
  37:             parameters.Add(new SPParam("ordered", "ordered", false, null, new SPTrueFalseValidator()));
  38:             parameters.Add(new SPParam("title", "title", false, null, new SPNullOrNonEmptyValidator()));
  39:             parameters.Add(new SPParam("description", "desc", false, null, new SPNullOrNonEmptyValidator()));
  40:             parameters.Add(new SPParam("nocrawl", "nocrawl", false, null, new SPTrueFalseValidator()));
  41:             parameters.Add(new SPParam("enableattachments", "att", false, null, new SPTrueFalseValidator()));
  42:  
  43:             StringBuilder sb = new StringBuilder();
  44:             sb.Append("\r\n\r\nSets various properties of a list.\r\n\r\nParameters:");
  45:             sb.Append("\r\n\t-url <URL to the list to update>");
  46:             sb.Append("\r\n\t[-title <title of the list>]");
  47:             sb.Append("\r\n\t[-description <title of the list>]");
  48:             sb.AppendFormat("\r\n\t[-versioning <{0}>", versionValidator.DisplayValue);
  49:             sb.AppendFormat("\r\n\t[-draftvisibility <{0}>", draftVisibilityValidator.DisplayValue);
  50:             sb.Append("\r\n\t[-majorversionlimit <0 - 50000>]");
  51:             sb.Append("\r\n\t[-majorwithminorversionlimit <0 - 50000>]");
  52:             sb.Append("\r\n\t[-forcecheckout <true | false>]");
  53:             sb.Append("\r\n\t[-enablemoderation <true | false>]");
  54:             sb.Append("\r\n\t[-enablecontenttypes <true | false>]");
  55:             sb.Append("\r\n\t[-enablefoldercreation <true | false>]");
  56:             sb.Append("\r\n\t[-allowdeletion <true | false>]");
  57:             sb.Append("\r\n\t[-alloweveryoneviewitems <true | false>]");
  58:             sb.Append("\r\n\t[-enablesyndication <true | false>]");
  59:             sb.Append("\r\n\t[-hidden <true | false>]");
  60:             sb.Append("\r\n\t[-onquicklaunch <true | false>]");
  61:             sb.AppendFormat("\r\n\t[-defaultitemopen <{0}>", itemOpenValidator.DisplayValue);
  62:             sb.Append("\r\n\t[-ordered <true | false>]");
  63:             sb.Append("\r\n\t[-nocrawl <true | false>]");
  64:             sb.Append("\r\n\t[-enableattachments <true | false>]");
  65:             Init(parameters, sb.ToString());
  66:         }
  67:  
  68:        
  69:         #region ISPStsadmCommand Members
  70:  
  71:         /// <summary>
  72:         /// Gets the help message.
  73:         /// </summary>
  74:         /// <param name="command">The command.</param>
  75:         /// <returns></returns>
  76:         public override string GetHelpMessage(string command)
  77:         {
  78:             return HelpMessage;
  79:         }
  80:  
  81:         /// <summary>
  82:         /// Runs the specified command.
  83:         /// </summary>
  84:         /// <param name="command">The command.</param>
  85:         /// <param name="keyValues">The key values.</param>
  86:         /// <param name="output">The output.</param>
  87:         /// <returns></returns>
  88:         public override int Execute(string command, StringDictionary keyValues, out string output)
  89:         {
  90:             output = string.Empty;
  91:             Verbose = true;
  92:  
  93:             string url = Params["url"].Value.TrimEnd('/');
  94:  
  95:             using (SPSite site = new SPSite(url))
  96:             using (SPWeb web = site.OpenWeb())
  97:             {
  98:                 SPList list = Utilities.GetListFromViewUrl(web, url);
  99:  
 100:                 if (list == null)
 101:                     throw new SPException("List was not found.");
 102:  
 103:  
 104:                 if (Params["versioning"].UserTypedIn)
 105:                 {
 106:                     VersionSettings versioning =
 107:                         (VersionSettings) Enum.Parse(typeof (VersionSettings), Params["versioning"].Value, true);
 108:                     SetVersioning(list, versioning);
 109:                 }
 110:  
 111:  
 112:                 SetProperties(list,
 113:                     GetValue(list.Title, "title"),
 114:                     GetValue(list.Title, "description"),
 115:                     GetValue(list.ContentTypesEnabled, "enablecontenttypes"),
 116:                     GetValue(list.DraftVersionVisibility, "draftvisibility"),
 117:                     GetValue(list.MajorVersionLimit, "majorversionlimit"),
 118:                     GetValue(list.MajorWithMinorVersionsLimit, "majorwithminorversionlimit"),
 119:                     GetValue(list.ForceCheckout, "forcecheckout"),
 120:                     GetValue(list.EnableModeration, "enablemoderation"),
 121:                     GetValue(list.EnableFolderCreation, "enablefoldercreation"),
 122:                     GetValue(list.AllowDeletion, "allowdeletion"),
 123:                     GetValue(list.AllowEveryoneViewItems, "alloweveryoneviewitems"),
 124:                     GetValue(list.EnableSyndication, "enablesyndication"),
 125:                     GetValue(list.DefaultItemOpen, "defaultitemopen"),
 126:                     GetValue(list.Hidden, "hidden"),
 127:                     GetValue(list.OnQuickLaunch, "onquicklaunch"),
 128:                     GetValue(list.Ordered, "ordered"),
 129:                     GetValue(list.NoCrawl, "nocrawl"),
 130:                     GetValue(list.EnableAttachments, "enableattachments"));
 131:  
 132:                 list.Update();
 133:             }
 134:  
 135:             return OUTPUT_SUCCESS;
 136:         }
 137:  
 138:         #endregion
 139:  
 140:         /// <summary>
 141:         /// Gets the value.
 142:         /// </summary>
 143:         /// <typeparam name="T"></typeparam>
 144:         /// <param name="defaultValue">The default value.</param>
 145:         /// <param name="paramName">Name of the param.</param>
 146:         /// <returns></returns>
 147:         private T GetValue<T>(T defaultValue, string paramName)
 148:         {
 149:             if (!Params[paramName].UserTypedIn)
 150:                 return defaultValue;
 151:  
 152:             string val = Params[paramName].Value;
 153:             if (typeof(T).IsEnum)
 154:                 return (T) Enum.Parse(typeof (T), val, true);
 155:  
 156:             return (T)Convert.ChangeType(val, typeof (T));
 157:  
 158:         }
 159:  
 160:         /// <summary>
 161:         /// Sets the properties.
 162:         /// </summary>
 163:         /// <param name="list">The list.</param>
 164:         /// <param name="title">The title.</param>
 165:         /// <param name="description">The description.</param>
 166:         /// <param name="contentTypesEnabled">if set to <c>true</c> [content types enabled].</param>
 167:         /// <param name="draftVisibility">The draft visibility.</param>
 168:         /// <param name="majorVersionLimit">The major version limit.</param>
 169:         /// <param name="majorWithMinorVersionLimit">The major with minor version limit.</param>
 170:         /// <param name="forceCheckout">if set to <c>true</c> [force checkout].</param>
 171:         /// <param name="enableModeration">if set to <c>true</c> [enable moderation].</param>
 172:         /// <param name="enableFolderCreation">if set to <c>true</c> [enable folder creation].</param>
 173:         /// <param name="allowDeletion">if set to <c>true</c> [allow deletion].</param>
 174:         /// <param name="allowEveryoneViewItems">if set to <c>true</c> [allow everyone view items].</param>
 175:         /// <param name="enableSyndication">if set to <c>true</c> [enable syndication].</param>
 176:         /// <param name="itemOpen">The item open.</param>
 177:         /// <param name="hidden">if set to <c>true</c> [hidden].</param>
 178:         /// <param name="onQuickLaunch">if set to <c>true</c> [on quick launch].</param>
 179:         /// <param name="ordered">if set to <c>true</c> [ordered].</param>
 180:         /// <param name="noCrawl">if set to <c>true</c> [no crawl].</param>
 181:         /// <param name="enableAttachments">if set to <c>true</c> [enable attachments].</param>
 182:         private void SetProperties(SPList list,
 183:             string title,
 184:             string description,
 185:             bool contentTypesEnabled,
 186:             DraftVisibilityType draftVisibility,
 187:             int majorVersionLimit,
 188:             int majorWithMinorVersionLimit,
 189:             bool forceCheckout,
 190:             bool enableModeration,
 191:             bool enableFolderCreation,
 192:             bool allowDeletion,
 193:             bool allowEveryoneViewItems,
 194:             bool enableSyndication,
 195:             DefaultItemOpen itemOpen,
 196:             bool hidden,
 197:             bool onQuickLaunch,
 198:             bool ordered,
 199:             bool noCrawl,
 200:             bool enableAttachments)
 201:         {
 202:             list.Title = title;
 203:             list.Description = description;
 204:  
 205:             if (list.AllowContentTypes || !contentTypesEnabled)
 206:                 list.ContentTypesEnabled = contentTypesEnabled;
 207:  
 208:             list.DraftVersionVisibility = draftVisibility;
 209:  
 210:             if (enableModeration && list.BaseTemplate == SPListTemplateType.PictureLibrary)
 211:                 Log("WARNING: Cannot set moderation on a picture library.");
 212:             else
 213:                 list.EnableModeration = enableModeration;
 214:  
 215:             if (list.EnableVersioning)
 216:                 list.MajorVersionLimit = majorVersionLimit;
 217:  
 218:             if (list.EnableMinorVersions && list.EnableModeration)
 219:                 list.MajorWithMinorVersionsLimit = majorWithMinorVersionLimit;
 220:  
 221:  
 222:             if (list.BaseTemplate == SPListTemplateType.DocumentLibrary)
 223:                 list.ForceCheckout = forceCheckout;
 224:             else if (forceCheckout)
 225:                 Log("WARNING: Force checkout can only be set on document libraries.");
 226:  
 227:             list.EnableFolderCreation = enableFolderCreation;
 228:             list.AllowDeletion = allowDeletion;
 229:             list.AllowEveryoneViewItems = allowEveryoneViewItems;
 230:             list.EnableSyndication = enableSyndication;
 231:             list.DefaultItemOpen = itemOpen;
 232:             list.Hidden = hidden;
 233:             list.OnQuickLaunch = onQuickLaunch;
 234:             list.NoCrawl = noCrawl;
 235:             
 236:             if (list.BaseTemplate == SPListTemplateType.GenericList)
 237:                 list.Ordered = ordered;
 238:             else if (ordered)
 239:                 Log("WARNING: The Ordered property can only be set for generic lists.");
 240:  
 241:             if (!enableAttachments)
 242:                 list.EnableAttachments = false;
 243:             else
 244:             {
 245:                 if (!(list.BaseType == SPBaseType.DocumentLibrary || list.BaseType == SPBaseType.Survey))
 246:                     list.EnableVersioning = true;
 247:                 else
 248:                     Log("WARNING: Attachments are only allowed on document libraries and surveys.");
 249:             }
 250:  
 251:  
 252:         }
 253:  
 254:         /// <summary>
 255:         /// Sets the versioning.
 256:         /// </summary>
 257:         /// <param name="list">The list.</param>
 258:         /// <param name="versioning">The versioning.</param>
 259:         private void SetVersioning(SPList list, VersionSettings versioning)
 260:         {
 261:             switch (versioning)
 262:             {
 263:                 case VersionSettings.None:
 264:                     list.EnableVersioning = false;
 265:                     list.EnableMinorVersions = false;
 266:                     break;
 267:                 case VersionSettings.Major:
 268:                     list.EnableVersioning = true;
 269:                     list.EnableMinorVersions = false;
 270:                     break;
 271:                 case VersionSettings.MajorMinor:
 272:                     list.EnableVersioning = true;
 273:                     list.EnableMinorVersions = true;
 274:                     break;
 275:             }
 276:         }
 277:     }
 278: }

The help for the command is shown below:

C:\>stsadm -help gl-setlistproperties

stsadm -o gl-setlistproperties


Sets various properties of a list.

Parameters:
        -url <URL to the list to update>
        [-title <title of the list>]
        [-description <title of the list>]
        [-versioning <none | major | majorminor>
        [-draftvisibility <reader | author | approver>
        [-majorversionlimit <0 - 50000>]
        [-majorwithminorversionlimit <0 - 50000>]
        [-forcecheckout <true | false>]
        [-enablemoderation <true | false>]
        [-enablecontenttypes <true | false>]
        [-enablefoldercreation <true | false>]
        [-allowdeletion <true | false>]
        [-alloweveryoneviewitems <true | false>]
        [-enablesyndication <true | false>]
        [-hidden <true | false>]
        [-onquicklaunch <true | false>]
        [-defaultitemopen <preferclient | browser>
        [-ordered <true | false>]
        [-nocrawl <true | false>]
        [-enableattachments <true | false>]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-setlistproperties WSS 3.0
MOSS 2007
Released: 7/6/2009

Parameter Name Short Form Required Description Example Usage
url   Yes The URL of the list or library to update. -url http://portal/documents
title   No String representing the title of the list.  Corresponds to the SPList.Title property. -title Documents
description desc No String representing the description of the list.  Corresponds to the SPList.Description property. -description "Shared Documents"

-desc "Shared Documents"
versioning version No Sets whether to enable versioning and if minor versions should be created or not (sets the SPList.EnableVersioning and the SPList.EnableMinorVersions properties).  Valid values are “none”, “major”, and “majorminor”. -versioning majorminor

-version majorminor
draftvisibility dvv No Sets a value that determines the type of user who can view minor versions of document drafts within the list.  Corresponds to the SPList.DraftVersionVisibility property.  Valid values are “reader”, “author”, and “approver”. -draftvisibility approver

-dvv approver
majorversionlimit mvl No

Sets the maximum number of major versions allowed for an item in a document library that uses version control with major versions only.  Corresponds to the SPList.MajorVersionLimit property.

-majorversionlimit 3

-mvl 3
majorwithminorversionlimit mmvl No Sets the maximum number of major versions that are allowed for an item in a document library that uses version control with both major and minor versions.  Corresponds to the SPList.MajorWithMinorVersionsLimit property. -majorwithminorversionlimit 3

-mmvl 3
forcecheckout fc No Sets whether forced checkout is enabled for the document library.  Valid values are “true” and “false”.  Corresponds to the SPList.ForceCheckout property. -forcecheckout true

-fc true
enablemoderation mod No Sets whether Content Approval is enabled for the list.  Valid values are “true” and “false”.  Corresponds to the SPList.EnableModeration property. -enablemoderation true

-mod true
enablecontenttypes ect No Sets whether content types are enabled for the list.  Valid values are “true” and “false”.  Corresponds to the SPList.ContentTypesEnabled property. -enablecontenttypes true

-ect true
enablefoldercreation efc No Sets whether folders can be created for the list.  Valid values are “true” and “false”.  Corresponds to the SPList.EnableFolderCreation property. -enablefoldercreation true

-efc true
allowdeletion del No Sets whether the list can be deleted.  Valid values are “true” and “false”.  Corresponds to the SPList.AllowDeletion property. -allowdeletion true

-del true
alloweveryoneviewitems aevi No Sets whether everyone can view documents in the document library or attachments to items in the list.  Valid values are “true” and “false”.  Corresponds to the SPList.AllowEveryoneViewItems property. -alloweveryoneviewitems true

-aevi true
enablesyndication syn No Sets whether RSS syndication is enabled for the list.  Valid values are “true” and “false”.  Corresponds to the SPList.EnableSyndication property. -enablesyndication true

-syn true
hidden   No Sets whether the list is hidden.  Valid values are “true” and “false”.  Corresponds to the SPList.Hidden property. -hidden false
onquicklaunch oql No Sets whether the list appears on the Quick Launch of the home page.  Valid values are “true” and “false”.  Corresponds to the SPList.OnQuickLaunch property. -onquicklaunch true

-oql true
defaultitemopen open No Sets whether to open list items in a client application or in the browser.  Valid values are “preferclient” and “browser”.  Corresponds to the SPList.DefaultItemOpen property. -defaultitemopen preferclient

-open preferclient
ordered   No Sets whether the option to allow users to reorder items in the list is available on the Edit View page for the list.  Valid values are “true” and “false”.  Corresponds to the SPList.Ordered property. -ordered true
nocrawl   No Sets whether crawling is enabled for the list.  Valid values are “true” and “false”.  Corresponds to the SPList.NoCrawl property. -nocrawl false
enableattachments att No Sets whether attachments can be added to items in the list.  Valid values are “true” and “false”.  Corresponds to the SPList.EnableAttachments property. -enableattachments false

-att false

The following is an example of how to set a few properties on a list:

stsadm -o gl-setlistproperties -url http://portal/documents -versioning majorminor -enablemoderation true -draftvisibility approver

Note that I'm not exposing every property on the SPList class but I believe I've hit the more widely used ones.

6Jul/095

Recent Custom STSADM Extensions Bug Fixes

For those that follow me on twitter (http://twitter.com/glapointe) you probably already know about these fixes but I’ll try to provide more details here.  While I was waiting to start my new job (see my previous post) I decided to take a look at a few of the STSADM commands that I built and fix some things that’s been nagging me for a while.  The one I spent the most time on was the gl-convertsubsitetositecollection command and more specifically its partner the gl-repairsitecollectionimportedfromsubsite command.

This command is undoubtedly my most popular command – it’s corresponding blog post is consistently the top requested post on my blog every week.  The problem is that I had some direct access database calls in the code which never really set well with me – it violated everything I always tell people – never hit the database directly.  The reason for the code was due to two issues: the ContentType field of the master page gallery was getting incorrectly set to a Text type instead of a Choice type and there was no way via the API to change this; discussion lists were getting flattened and there was no way via the API to change this without recreating each item (copy/move operation) which messes up historical/audit information.

There are two things that enabled me to get rid of this DB access code:  I’m smarter than I was 2 years ago when I originally wrote the code and SP2 came out.  Using code that I wrote for copying lists I was able to simply export the original source master page gallery and import it on top of the target gallery thus forcing the content type field to be fixed.  I also found that, in every case I tested, activating the PublishingResources feature now fixes the list (I believe this is the result of SP2 but I’ve not had time to confirm specifically).  Also, I removed some code to deal with an earlier bug that was resulting in items being put in arbitrary locations (under the wrong folder) so now discussion list items are correctly imported and not flattened.

So, what does all this mean – if you are not running SP2 then most everything with these commands should work just fine for you but I can’t guarantee everything I’m accounting for will be fixed (particularly discussion lists) – so if you plan on using these commands I strongly recommend you deploy SP2.  I also added some additional logging and removed the unnecessary site template related parameters.

Another command that I “fixed” was the gl-createsiteindb command.  This one never really sat well with me because I was using reflection to call an internal method in order to pass the target database in – it just seemed like there had to be a way to do this without using reflection.  Turns out there was but for whatever reason I just missed it (maybe it’s new with an update or something or maybe I just brain farted).  Once you get the SPContentDatabase object you can simply use the Sites property which returns an SPSiteCollection object and then call it’s Add method to add the site to the database – seems so obvious now.  Anyways, the code  has been updated to use this approach thus avoiding the use of reflection.

The following remaining commands also had some minor fixes done:

  1. gl-copylistsecurity: Fixed issues for list items (document libraries were not affected) where the target items was being located via the file name property (SPListItem.Name) which was not necessarily set for a generic list.  I now first try the FileLeafRef field and if that fails then I use the index of the item in the list.
  2. gl-importaudiences: The XML file that I was outputting was reversing the value of the SearchString and ReplaceString attributes.
  3. gl-copycontenttypes: If the document template pointed to an existing document outside the current web the code now correctly copies the value of the document template property without attempting to copy the document itself (previously it would ignore the property if it couldn’t access the file).
  4. gl-replacefieldvalues: I was previously checking the document out, making the change, and checking the document back in.  For various reasons I’ve decided to not attempt to check the document out and instead simply call SystemUpdate to save the changes.  I went back and forth on this and would be curious to get peoples thoughts – is it better to have a separate version for each change or leave it as I’ve got it now where no version history is generated?

I’ve not yet updated the documentation for all of these commands but I will attempt do so this week.

Tagged as: 5 Comments
6Jul/091

Deploying SharePoint Files Not Handled by the WSP Solution Schema

I was working on a project recently where I had to deploy a settings file to the root of my web applications folder (where the web.config file resides).  If you've ever had to do something like this before then you know that you cannot do this declaratively using the WSP's Solution schema.  The Solution schema is really quite limiting as to where you can actually deploy files - as a result your only option is to create a custom Feature that runs some code when executed (because we certainly don't want to go the xcopy route).

To do this we're going to create a custom Feature which contains all the files that we need to copy and then we'll provision a one-time timer job to copy the file to the target location on each server.

Here's our Feature.xml file:

<?xml version="1.0" encoding="utf-8"?>
<Feature
Id="1960C4A0-7A47-42A8-A382-F7A91214BA39"
Title="Settings Provisioner"
Description="This Feature deploys a settings file to a the web application root."
Version="1.0.0.0"
Scope="WebApplication"
Hidden="false"
ReceiverAssembly="MyCustomFeature, Version=1.0.0.0, Culture=neutral, PublicKeyToken=39b13c54ceef5193"
ReceiverClass="MyCustomFeature.FeatureReceivers.SettingsFeatureReceiver" xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementFile Location="Files\settings.config" />
</ElementManifests>
</Feature>

As you can see we are including a "settings.config" file which is located in a folder called "Files" directly under the Feature folder.  You could easily have any number of files here by simply adding additional ElementFile elements.  Also note that we are linking a feature receiver to the Feature which will execute upon activation and deactivation.

Here's our feature receiver class:

   1: public class SettingsFeatureReceiver : SPFeatureReceiver
   2: {
   3:     /// <summary>
   4:     /// Occurs after a Feature is activated.
   5:     /// </summary>
   6:     /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
   7:     public override void FeatureActivated(SPFeatureReceiverProperties properties)
   8:     {
   9:         SPWebApplication webApp = properties.Feature.Parent as SPWebApplication;
  10:         try
  11:         {
  12:             TimerJobs.CopySettingsJob job = new TimerJobs.CopySettingsJob(webApp);
  13:             job.SubmitJob(true, properties.Feature.Definition.RootDirectory);
  14:         }
  15:         catch (Exception ex)
  16:         {
  17:             Logger.WriteException(ex);
  18:         }
  19:  
  20:         
  21:     }
  22:  
  23:     /// <summary>
  24:     /// Occurs when a Feature is deactivated.
  25:     /// </summary>
  26:     /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
  27:     public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
  28:     {
  29:         try
  30:         {
  31:             TimerJobs.CopySettingsJob job = new TimerJobs.CopySettingsJob(webApp);
  32:             job.SubmitJob(false, properties.Feature.Definition.RootDirectory);
  33:         }
  34:         catch (Exception ex)
  35:         {
  36:             Logger.WriteException(ex);
  37:         }
  38:     }
  39:  
  40:     public override void FeatureInstalled(SPFeatureReceiverProperties properties)
  41:     {
  42:         /* no op */
  43:     }
  44:     public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
  45:     {
  46:         /* no op */
  47:     }
  48: }

Notice that within the FeatureActivated method I'm getting a reference to the SPWebApplication object and passing that to a CopySettingsJob class which is our timer job that will do all the work.  On the FeatureDeactivating event you can see similar code but I'm passing in false instead of true to the Submit method.  The Boolean value indicates whether we are activating or deactivating our Feature.  I'm also passing in the path to the Feature folder in the 12 hive as that is where our source files are located.

Lets look at the timer job class now:

   1: public class CopySettingsJob : SPJobDefinition
   2: {
   3:     private const string KEY_ACTIVATING = "Activating";
   4:     private const string KEY_FEATUREFOLDER = "FeatureFolder";
   5:     private const string JOB_NAME = "job-settings-copy-";
   6:     private static readonly string jobId = Guid.NewGuid().ToString();
   7:  
   8:     public CopySettingsJob() : base() { }
   9:  
  10:     /// <summary>
  11:     /// Initializes a new instance of the <see cref="CopySettingsJob"/> class.
  12:     /// </summary>
  13:     /// <param name="webApp">The web app.</param>
  14:     public CopySettingsJob(SPWebApplication webApp)
  15:         : base(JOB_NAME + jobId, webApp, null, SPJobLockType.None)
  16:     {
  17:         Title = "Copy Settings Job";
  18:     }
  19:  
  20:     /// <summary>
  21:     /// Executes the job definition.
  22:     /// </summary>
  23:     /// <param name="targetInstanceId">For target types of <see cref="T:Microsoft.SharePoint.Administration.SPContentDatabase"></see> this is the database ID of the content database being processed by the running job. This value is Guid.Empty for all other target types.</param>
  24:     public override void Execute(Guid targetInstanceId)
  25:     {
  26:         Logger.WriteInformation(string.Format("Starting {0} timer job.", Name));
  27:  
  28:         try
  29:         {
  30:             string settingsFilePath = Path.Combine(Properties[KEY_FEATUREFOLDER].ToString(), "Files\\Settings.config");
  31:             string targetPath = Path.Combine(WebApplication.IisSettings[SPUrlZone.Default].Path.ToString(), "Settings.config");
  32:             if ((bool)Properties[KEY_ACTIVATING])
  33:             {
  34:                 Logger.WriteInformation(string.Format("Copying file from \"{0}\" to \"{1}\".", settingsFilePath, targetPath));
  35:                 File.Copy(settingsFilePath, targetPath, true);
  36:             }
  37:             else
  38:             {
  39:                 Logger.WriteInformation(string.Format("Deleting file from \"{0}\"", targetPath));
  40:                 File.Delete(targetPath);
  41:             }
  42:         }
  43:         catch (Exception ex)
  44:         {
  45:             Logger.WriteException(ex);
  46:             return;
  47:         }
  48:         Logger.WriteSuccessAudit(string.Format("Timer job {0} completed successfully", Name));
  49:     }
  50:  
  51:     /// <summary>
  52:     /// Submits the job.
  53:     /// </summary>
  54:     /// <param name="activating">if set to <c>true</c> [activating].</param>
  55:     public void SubmitJob(bool activating, string featureFolder)
  56:     {
  57:         Properties[KEY_ACTIVATING] = activating;
  58:         Properties[KEY_FEATUREFOLDER] = featureFolder;
  59:         Schedule = new SPOneTimeSchedule(DateTime.Now);
  60:         Title += " (" + jobId + ")";
  61:         Update();
  62:     }
  63: }

As you can see the code simply stores the Feature folder as a property and then sets a one-time schedule.  When the code runs it copies the source file to the target.  Because we're using an SPJobLockType value of "None" in the constructor the code will execute on every server (set it to "Job" if you want it to run on just the server in which the Feature was actually activated).

Of course the code above isn't very generic as it hard codes the settings.config file which isn't very reusable but I wanted to keep this sample nice and simple.  A better approach would be to require an either an XML file to be stored in the Feature folder and then read by the timer job or have the SubmitJob method take in parameters that describe what files to move and where to move them.

One key thing to remember is that this code will run once on each server for every web application on which the Feature has been activated.  If you need a Farm scoped Feature because perhaps you are copying the noise words file for instance then you'll want to change the constructor of the timer job to take in an SPService object and change the FeatureActivated method as shown below:

   1: try
   2: {
   3:     string featurePath = properties.Feature.Definition.RootDirectory;
   4:  
   5:     SPTimerService timerService = SPFarm.Local.TimerService;
   6:     if (null == timerService)
   7:     {
   8:         throw new SPException("The Farms timer service cannot be found.");
   9:     }
  10:     TimerJobs.CopySettingsJob job = timerService.JobDefinitions.GetValue<TimerJobs.CopySettingsJob>(TimerJobs.CopySettingsJob.JOB_NAME);
  11:     if (null == job)
  12:     {
  13:         job = new TimerJobs.CopySettingsJob(timerService);
  14:     }
  15:     job.SubmitJob(true, properties.Feature.Definition.RootDirectory);
  16: }
  17: catch (Exception ex)
  18: {
  19:     Logger.WriteException(ex);
  20: }

Hopefully this simple example helps you to solve your file deployment challenges.

1May/094

New STSADM Commands and Parameters with SharePoint 2007 SP2

I was working on getting SP2 installed so that I could write this post and then I saw that my buddy Todd Klindt beat me to the punch: http://www.toddklindt.com/blog/Lists/Posts/Post.aspx?List=56f96349%2D3bb6%2D4087%2D94f4%2D7f95ff4ca81f&ID=127.  But, as Todd only listed the new commands I figured I’d continue with what I was planning which was to show the commands and the new parameters added to existing commands.

To do this I threw together a quick PowerShell script which parses the output of running “stsadm -help” and then calls the help for each of the commands listed, saving the results to a file.  I did this for my MOSS 2007 with the IU (Infrastructure Update) installed (my baseline), installed SP2, and then ran the script again.  I then used Beyond Compare (great tool) to compare the two files.  I’ll start as Todd did, with the new commands (in no particular order):

New Commands

enumallwebs

This command is interesting because it doesn't take in an URL like the enumsubwebs command does (note that Todd references an enumwebs command - he's referring to enumsubwebs). Instead it takes a database name and optional database server. So what's going to happen is that the command will look at the database provided and dump out all the site collections in that database along with every web within that site collection.  Here’s the help for the command:

stsadm.exe -o enumallwebs
           -databasename <database name>
           [-databaseserver <database server name>]

Running the command for one of my databases produces the following results:

<Sites Count="1">
  <Site Id="f26a9beb-6a70-470e-82b1-4c3f6b34b19a" OwnerLogin="SPDEV\spadmin" InSiteMap="True">
    <Webs Count="6">
      <Web Id="93f63c3a-25b9-49d1-a315-2cbcf0b30614" Url="/" LanguageId="1033" TemplateName="SPS#0" TemplateId="20" />
      <Web Id="36f63321-d475-4786-adcf-4d43cfd3eb32" Url="/Docs" LanguageId="1033" TemplateName="BDR#0" TemplateId="7" />
      <Web Id="f0106227-339a-48a8-80f4-f9feeef9b840" Url="/News" LanguageId="1033" TemplateName="SPSNHOME#0" TemplateId="33" />
      <Web Id="1db29e75-2e73-4453-a517-a2b657a80680" Url="/Reports" LanguageId="1033" TemplateName="SPSREPORTCENTER#0" TemplateId="38" />
      <Web Id="51bf7973-c692-45f6-a5d8-81fa682cce42" Url="/SearchCenter" LanguageId="1033" TemplateName="SRCHCEN#0" TemplateId="50" />
      <Web Id="a4e50ce6-a527-4269-a590-45f78ea7bf30" Url="/SiteDirectory" LanguageId="1033" TemplateName="SPSSITES#0" TemplateId="34" />
    </Webs>
  </Site>
</Sites>

Pay attention to the results above because the information provided can be used to help delete orphaned site collections using the updated deletesite command (see below for details).

exportipfsadminobjects

If you deploy custom InfoPath Forms you might be interested in this command - Export InfoPath Forms Services Admin Objects.  Running this command will create a CAB file containing all your deployed objects.  If you deployed the objects via a Feature then they will not be exported.  As Todd points out, there is no corresponding import but as the output is just a CAB file you can easily crack it open and use it manually re-add any of your objects.  Here’s the help for the command:

stsadm -o exportipfsadminobjects
	-filename <path to file>

 

forcedeletelist

This isn’t actually a new command - it’s name change for the previously incorrectly named forcedeleteweb - you’ll notice that forcedeleteweb no longer exists.

 

listqueryprocessoroptions

This command is used to identify the current search query processor options.  The command itself isn’t very useful as a standalone but rather to help when using the setqueryprocessoroptions (shown below) command.  Here’s the help for the command (see http://technet.microsoft.com/en-us/library/dd789568.aspx for more details):

stsadm -o listqueryprocessoroptions

    -ssp <ssp name>

Running the command in my VM environment shows the following:

C:\>stsadm -o listqueryprocessoroptions -ssp SSP1

        securitytrimmingcachesize     10000
        securitytrimmingmultiplier    <default>
        nearduplicatemultiplier       <default>
        joinmultiplier                10
        sdidjoinmultiplier            <default>

 

preupgradecheck

This is perhaps one of the coolest new commands as it helps to give us some insight as to what is going to be coming with the next version, albeit not much in the grand scheme but hey, anything is helpful.  You can find more information here: http://technet.microsoft.com/en-us/library/dd793605.aspx.  Here’s the help for the command:

stsadm.exe -o preupgradecheck
                   [ -rulefiles <rule files delimited by comma or semicolon.> ]
                   [ -listrulefiles ]
                   [ -localonly ]

             The preupgrade checker does not perform any repairs, but instead only checks for issues and outputs the list of issues and possible remedies to the issues.

Running this command in my single server VM environment produces the fo

C:\>stsadm -o preupgradecheck

Processing configuration file: OssPreUpgradeCheck.xml
        SearchContentSourcesInfo... Information Only
        SearchInfo... Information Only
Processing configuration file: WssPreUpgradeCheck.xml
        ServerInfo... Information Only
        FarmInfo... Information Only
        UpgradeTypes... Information Only
        SiteDefinitionInfo... Information Only
        LanguagePackInfo... Information Only
        FeatureInfo... Information Only
        AamUrls... Information Only
        LargeList... Information Only
        CustomListViewInfo... Passed
        CustomFieldTypeInfo... Information Only
        CustomWorkflowActionsFileInfo... Passed
        ModifiedWebConfigWorkflowAuthorizedTypesInfo... Information Only
        ModifiedWorkflowActionsFileInfo... Passed
        DisabledWorkFlowsInfo... Passed
        OSPrerequisite... Passed
        WindowsInternalDatabaseMigration... Passed
        WindowsInternalDatabaseSite... Passed
        MissingWebConfig... Passed
        ReadOnlyDatabase... Passed
        InvalidDatabaseSchema... Passed
        ContentOrphan... Passed
        SiteOrphan... Passed
        PendingUpgrade... Passed
        InvalidServiceAccount... Passed
        InvalidHostName... Passed
        SPSearchInfo... Information Only

Operation completed successfully.


Please review the results at C:\Program Files\Common Files\Microsoft Shared\Web Server Extensions\12\Logs\PreUpgradeCheck-20090501-105935-359.htm.

The report that it generates is also very useful - I’d recommend running this guy and saving off the report somewhere even if you are not currently thinking of upgrading - it lists all your details about your topology, Features installed, web applications, databases, site definitions installed, custom field types that won’t be upgraded (note that there are some ootb ones), etc., etc., etc.

setqueryprocessoroptions

This is the sister command for the listqueryprocessoroptions command.  You can use this command to change the values identified by the list command.  The command is detailed here: http://technet.microsoft.com/en-us/library/dd789632.aspx.  Here’s the help for the command:

stsadm -o setqueryprocessoroptions

    -ssp <ssp name>
    [-securitytrimmingcachesize <security trimming cache size>]
    [-securitytrimmingmultiplier <security trimming overfetch multiplier>]
    [-nearduplicatemultiplier <duplicate removal overfetch multiplier>]
    [-joinmultiplier <join discard overfetch multiplier>]
    [-sdidjoinmultiplier <missing security descriptor overfetch multiplier>]

variationsfixuptool

If you’re doing any kind of variations work then you should be very interested in this tool.  It basically does what my gl-fixvariationrelationships command does which is to repair the relationships between sibling pages in your various labels.  The command is detailed here: http://technet.microsoft.com/en-us/library/dd789658.aspx.  Here’s the help for the command:

stsadm -o variationsfixuptool
	  -url <absolute web URL>
  	 [-recurse]
  	 [-label <label to fix or spawn>]
  	 [-fix]
  	 [-scan]
  	 [-spawn]
  	 [-showrunningjobs] 

 

New Parameters

So we’ve gone through all the new commands, no lets look at the commands that have been updated with new parameters.

backup

The backup command has been updated to include two new parameters: nositelock and force.  The nositelock parameter is specific to site collection backups and was added to mimic earlier behavior of not locking the site collection during a backup.  I don’t recommend you use this behavior but it might be useful if you have scripts that are already using the setsitelock command to make the database read-only (the backup stores the lock state so doing a restore could end up with the wrong state if you are setting the lock then doing your backup).  Details about the issue can be found here: http://support.microsoft.com/default.aspx/kb/967568.

The force parameter is specific to doing catastrophic backups and is used to ignore the disk space check.

deletesite

The deletesite command introduces 4 new parameters: force, siteid, databasename, and databaseserver.  These new parameters are specific to deleting orphaned site collections.  These parameters are detailed here: http://technet.microsoft.com/en-us/library/cc288016.aspx.

The key thing is that when adding a content database to an existing web application it is possible that the content database contains a site collection that is mapped to an existing site collection.  This results in the site collection being orphaned (not site mapped).  You can use the new enumallwebs command (detailed above) to get the list of site collections in a database along with the whether the site is site mapped (InSiteMap attribute) along with the site ID.

deleteweb

Like the deletesite command the deleteweb command introduces 4 new parameters: force, webid, databasename, and databaseserver.  These commands serve the same purpose as those for the deletesite command but with regards to subsites instead of site collections.  The details for the command are here: http://technet.microsoft.com/en-us/library/cc287710.aspx.  Note that the wording in this document is a bit confusing as they seem to interchange site, subsite, web, and site collection for the same thing - I think the document fell victim to some copy and paste.

getproperty/setproperty

The getproperty and setproperty commands introduce three new parameters (or properties): change-log-expiration-enabled, change-log-retention-period, event-log-retention-period.

The first property, change-log-expiration-enabled (detailed here: http://technet.microsoft.com/en-us/library/cc263361.aspx), specifies whether change logs are deleted after the time span defined by the change-log-retention-period.

The second property, change-log-retention-period (detailed here: http://technet.microsoft.com/en-us/library/cc261921.aspx), specifies the amount of time to preserve the change logs.  This property is equivalent to the “Change Log” setting on the Web Application General Settings page.

The third property, event-log-retention-period (I couldn’t actually find any documentation on this one), obviously has to do with how long the event logs are retained but what I can’t figure out is what event logs?  I did find that the property maps to the SPWebApplication’s EventLogRetentionPeriod property but as that property is also not documented it didn’t really tell me much.

osearch

The osearch command introduces one new parameter: reprovisionindex.  I couldn’t find anything about this parameter other than a short paragraph from the SP2 changes list.  Essentially this is just useful if you have more than one SSP as the following describes:

“In a farm with multiple SSPs, the administrators can use the new stsadm command to re-initialize the query servers for one of the SSPs while the other SSP continues to serve the query request. The command is the following, where SharedServices1 is the name of the SSP whose query servers are being re-initialized: "stsadm -o osearch -reprovisionindex -ssp SharedServices1"”

updatefarmcredentials

The updatefarmcredentials command introduces one new parameter: resume.  I couldn’t find any documentation about this parameter but think of it as a force parameter (that’s how it’s used internally).  I suspect this was added to get around various encryption errors that people get now and again when using this command.  If anyone has more information on this please share :)

Tagged as: 4 Comments
1May/090

Backup/Restore Now Supported Between Farms (via April CU)

I just saw a very exciting blog post from Stefan Goßner regarding the April 2009 Cumulative Update (CU) - with this update you can now use backup and restore to move site collections between farms: http://blogs.technet.com/stefan_gossner/archive/2009/05/01/red-is-green-up-is-down-and-the-unsupported-suddenly-becomes-supported.aspx

So first off, yes there is an April CU even though SP2 just came out a couple of days ago (think about it - SP2 has been cooked for a while but they’ve been testing, writing SDK docs, etc., etc., - in the meantime the team has continued working on new updates).  I strongly recommend that you install SP2 before you install this update! 

So what’s the change that makes it supported now?  Anyone ever have to use my gl-fixpublishingpagespagelayouturl command to fix page layouts that were absolute and pointing to the wrong server?  Well, this is the primary fix that Microsoft has made so that site collections (specifically those that use the publishing features) can be moved across farms.  Note that I suspect that my command will still have lots of uses (I need to do some disassembling to figure out the specific change - good chance my command won’t be needed anymore but I doubt it).

Some more information about the April CU can be found here: http://blogs.msdn.com/joerg_sinemus/archive/2009/05/01/should-i-install-sp2-and-or-april-cu.aspx

Tagged as: No Comments
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()