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

4Sep/0815

Create a Publishing Page via STSADM

I'm working on a project which is going to require the creation of about a hundred publishing pages which were going to have to be created by hand.  I really didn't want to have to sit there going through the UI to create all those pages so I threw together a new STSADM command that I could use via a script to create the pages: gl-createpublishingpage.

Creating publishing pages via code is pretty simple - you just use the PublishingWeb object's GetPublishingPages() method to return back the collection of pages and then call its Add method passing in the page name and layout.  From there it's just a matter of setting the field properties.

   1: public static void CreatePage(string url, string pageName, string title, string layoutName, Dictionary<string, string> fieldDataCollection)
   2: {
   3:     using (SPSite site = new SPSite(url))
   4:     using (SPWeb web = site.AllWebs[Utilities.GetServerRelUrlFromFullUrl(url)])
   5:     {
   6:         if (!PublishingWeb.IsPublishingWeb(web))
   7:             throw new ArgumentException("The specified web is not a publishing web.");
   8:  
   9:         PublishingWeb pubweb = PublishingWeb.GetPublishingWeb(web);
  10:         PageLayout layout = null;
  11:         string availableLayouts = string.Empty;
  12:         foreach (PageLayout lo in pubweb.GetAvailablePageLayouts())
  13:         {
  14:             availableLayouts += "\t" + lo.Name + "\r\n";
  15:             if (lo.Name.ToLowerInvariant() == layoutName.ToLowerInvariant())
  16:             {
  17:                 layout = lo;
  18:                 break;
  19:             }
  20:         }
  21:         if (layout == null)
  22:             throw new ArgumentException("The layout specified could not be found.  Available layouts are:\r\n" + availableLayouts);
  23:  
  24:         if (!pageName.ToLowerInvariant().EndsWith(".aspx"))
  25:             pageName += ".aspx";
  26:         
  27:         PublishingPage page = pubweb.GetPublishingPages().Add(pageName, layout);
  28:         page.Title = title;
  29:         SPListItem item = page.ListItem;
  30:  
  31:         foreach (string fieldName in fieldDataCollection.Keys)
  32:         {
  33:             string fieldData = fieldDataCollection[fieldName];
  34:  
  35:             try
  36:             {
  37:                 SPField field = item.Fields.GetFieldByInternalName(fieldName);
  38:  
  39:                 if (field.ReadOnlyField)
  40:                 {
  41:                     Console.WriteLine("Field '{0}' is read only and will not be updated.", field.InternalName);
  42:                     continue;
  43:                 }
  44:  
  45:                 if (field.Type == SPFieldType.Computed)
  46:                 {
  47:                     Console.WriteLine("Field '{0}' is a computed column and will not be updated.", field.InternalName);
  48:                     continue;
  49:                 }
  50:  
  51:                 if (field.Type == SPFieldType.URL)
  52:                     item[field.Id] = new SPFieldUrlValue(fieldData);
  53:                 else if (field.Type == SPFieldType.User)
  54:                     AddListItem.SetUserField(web, item, field, fieldData);
  55:                 else
  56:                     item[field.Id] = fieldData;
  57:             }
  58:             catch (ArgumentException)
  59:             {
  60:                 Console.WriteLine("WARNING: Could not set field {0} for item {1}.", fieldName, item.ID);
  61:             }
  62:         }
  63:  
  64:         page.Update();
  65:     }
  66: }

The help for the command is shown below:

C:\>stsadm -help gl-createpublishingpage

stsadm -o gl-createpublishingpage


Creates a new publishing page.

Parameters:
        -url <url to the publishing web within which to create the page>
        -name <the filename of the page to create (do not include the extension)>
        -title <the page title>
        -layout <the filename of the page layout to use>
        [-fielddata <semi-colon separated list of key value pairs: "Field1=Val1;Field2=Val2"> (use ';;' to escape semi-colons in data values)]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-createpublishingpage MOSS 2007 Released: 9/4/2008

Parameter Name Short Form Required Description Example Usage
url   Yes The URL to the publishing web within which to create the page. -url http://portal
name n Yes The filename of the page to create.  It is not necessary to include the extension.  Do not use special characters. -name NewPage

-n NewPage
title t Yes The title of the page.  Wrap within quotes if includes spaces. -title "New Page"

-t "New Page"
layout l Yes The filename of the page layout to use. -layout DefaultLayout.aspx

-l DefaultLayout.aspx
fielddata fd No Key/Value pairs of metadata to apply to the new page.  Use the internal field name and separate multiple pairs with a semi-colon.  Use the following format when setting data: "Field1=Val1;Field2=Val2". -fielddata "PublishingContactEmail=user@domain.com;PublishingContactName=First Last"

-fd "PublishingContactEmail=user@domain.com;PublishingContactName=First Last"

The following is an example of how to add a new publishing page:

stsadm -o gl-createpublishingpage -url http://portal -name NewPage -title "New Page" -layout DefaultLayout.aspx

28Aug/084

Enumerating Email Enabled Lists via STSADM

I can't really take much credit for this command - fellow MVP Todd Klindt had a custom console application that he'd created to do the same thing but it was lacking some functionality and was not created as an stsadm extension.  He asked if I'd mind reworking it to allow different scopes and to make it an extension.  Fortunately I had some time so I took a look at his code to get this one started.  In the end I only ended up using one line of code from Todd's version - the code to get the email suffix (the part after the "@").  To get the suffix you use the following code:

m_emailSuffix = SPFarm.Local.GetChild<SPIncomingEmailService>().ServerDisplayAddress.ToLower();

Having this saved me a lot of time though as I had no idea how to retrieve this information (though some reflection would have eventually revealed it to me).  The rest of it I had to recreate because we wanted to add the ability to use different scopes and because I wanted it to have the results output as XML (because XML is cool) and I figured I might as well dump out the other details related to the email configuration, not just the email alias.  To handle the scope requirement I used the SPEnumerator class that I built a while back - made it real easy as all I had to do was listen to the list enumerated event and initialize the object with the right starting place.

I thought about making the XML results hierarchical but in the end decided that a flat list was sufficient as it accomplished the main goal.  The complete code can be seen below - you can see an example of the output at the bottom of this post:

   1: using System;
   2: using System.Collections.Generic;
   3: using System.Collections.Specialized;
   4: using System.IO;
   5: using System.Text;
   6: using System.Xml;
   7: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
   8: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
   9: using Microsoft.SharePoint;
  10: using Microsoft.SharePoint.Administration;
  11:  
  12: namespace Lapointe.SharePoint.STSADM.Commands.Lists
  13: {
  14:     public class EnumEmailEnabledLists : SPOperation
  15:     {
  16:         private static XmlTextWriter xmlWriter;
  17:         private static string m_emailSuffix;
  18:  
  19:         /// <summary>
  20:         /// Initializes a new instance of the <see cref="EnumEmailEnabledLists"/> class.
  21:         /// </summary>
  22:         public EnumEmailEnabledLists()
  23:         {
  24:             SPParamCollection parameters = new SPParamCollection();
  25:             parameters.Add(new SPParam("url", "url", false, null, new SPUrlValidator()));
  26:             parameters.Add(new SPParam("scope", "s", false, "site", new SPRegexValidator("(?i:^Farm$|^WebApplication$|^Site$|^Web$)")));
  27:             parameters.Add(new SPParam("output", "output", false, null, new SPDirectoryExistsAndValidFileNameValidator()));
  28:  
  29:             StringBuilder sb = new StringBuilder();
  30:             sb.Append("\r\n\r\nOutputs the lists that have been email enabled.\r\n\r\nParameters:");
  31:             sb.Append("\r\n\t[-scope <Farm | WebApplication | Site | Web>]");
  32:             sb.Append("\r\n\t[-url <url of web application, site, or web to iterate)>]");
  33:             sb.Append("\r\n\t[-output <file to save the results to>]");
  34:  
  35:             Init(parameters, sb.ToString());
  36:         }
  37:  
  38:         #region ISPStsadmCommand Members
  39:  
  40:         /// <summary>
  41:         /// Gets the help message.
  42:         /// </summary>
  43:         /// <param name="command">The command.</param>
  44:         /// <returns></returns>
  45:         public override string GetHelpMessage(string command)
  46:         {
  47:             return HelpMessage;
  48:         }
  49:  
  50:         /// <summary>
  51:         /// Runs the specified command.
  52:         /// </summary>
  53:         /// <param name="command">The command.</param>
  54:         /// <param name="keyValues">The key values.</param>
  55:         /// <param name="output">The output.</param>
  56:         /// <returns></returns>
  57:         public override int Execute(string command, StringDictionary keyValues, out string output)
  58:         {
  59:             output = string.Empty;
  60:             string url = null;
  61:             if (Params["url"].UserTypedIn)
  62:             {
  63:                 url = Params["url"].Value.TrimEnd('/');
  64:             }
  65:             string scope = Params["scope"].Value.ToLowerInvariant();
  66:             string xml = GetEmailEnabledLists(scope, url);
  67:  
  68:             if (Params["output"].UserTypedIn)
  69:             {
  70:                 File.WriteAllText(Params["output"].Value, xml);
  71:             }
  72:             else
  73:                 Console.WriteLine(xml);
  74:  
  75:             return OUTPUT_SUCCESS;
  76:         }
  77:  
  78:  
  79:         /// <summary>
  80:         /// Validates the specified key values.
  81:         /// </summary>
  82:         /// <param name="keyValues">The key values.</param>
  83:         public override void Validate(StringDictionary keyValues)
  84:         {
  85:             Params["url"].IsRequired = (Params["scope"].Value.ToLowerInvariant() != "farm");
  86:  
  87:             base.Validate(keyValues);
  88:         }
  89:  
  90:         #endregion
  91:  
  92:         /// <summary>
  93:         /// Gets the email enabled lists.
  94:         /// </summary>
  95:         /// <param name="scope">The scope.</param>
  96:         /// <param name="url">The URL.</param>
  97:         /// <returns></returns>
  98:         public static string GetEmailEnabledLists(string scope, string url)
  99:         {
 100:             m_emailSuffix = SPFarm.Local.GetChild<SPIncomingEmailService>().ServerDisplayAddress.ToLower();
 101:  
 102:             SPEnumerator enumerator;
 103:             if (scope == "farm")
 104:             {
 105:                 enumerator = new SPEnumerator(SPFarm.Local);
 106:             }
 107:             else if (scope == "webapplication")
 108:             {
 109:                 enumerator = new SPEnumerator(SPWebApplication.Lookup(new Uri(url)));
 110:             }
 111:             else if (scope == "site")
 112:             {
 113:                 using (SPSite site = new SPSite(url))
 114:                 {
 115:                     enumerator = new SPEnumerator(site);
 116:                 }
 117:             }
 118:             else
 119:             {
 120:                 using (SPSite site = new SPSite(url))
 121:                 using (SPWeb web = site.AllWebs[Utilities.GetServerRelUrlFromFullUrl(url)])
 122:                 {
 123:                     enumerator = new SPEnumerator(web);
 124:                 }
 125:             }
 126:  
 127:             enumerator.SPListEnumerated += new SPEnumerator.SPListEnumeratedEventHandler(enumerator_SPListEnumerated);
 128:  
 129:             StringBuilder sb = new StringBuilder();
 130:  
 131:             xmlWriter = new XmlTextWriter(new StringWriter(sb));
 132:             xmlWriter.Formatting = Formatting.Indented;
 133:  
 134:             xmlWriter.WriteStartElement("Lists");
 135:  
 136:             enumerator.Enumerate();
 137:  
 138:             xmlWriter.WriteEndElement();
 139:             xmlWriter.Flush();
 140:             return sb.ToString();
 141:         }
 142:  
 143:         /// <summary>
 144:         /// Handles the SPListEnumerated event of the enumerator control.
 145:         /// </summary>
 146:         /// <param name="sender">The source of the event.</param>
 147:         /// <param name="e">The <see cref="Lapointe.SharePoint.STSADM.Commands.OperationHelpers.SPEnumerator.SPListEventArgs"/> instance containing the event data.</param>
 148:         private static void enumerator_SPListEnumerated(object sender, SPEnumerator.SPListEventArgs e)
 149:         {
 150:             if (e.List.EmailAlias != null)
 151:             {
 152:                 xmlWriter.WriteStartElement("List");
 153:                 xmlWriter.WriteAttributeString("siteUrl", e.Site.Url);
 154:                 xmlWriter.WriteAttributeString("webUrl", e.Web.ServerRelativeUrl);
 155:                 xmlWriter.WriteAttributeString("listUrl", e.List.RootFolder.Url);
 156:                 xmlWriter.WriteAttributeString("alias", e.List.EmailAlias + "@" + m_emailSuffix);
 157:                 
 158:                 //Group attachments in folders, options: "subject"/"sender"/"root"
 159:                 if (e.List.RootFolder.Properties["vti_emailattachmentfolders"] != null)
 160:                     xmlWriter.WriteAttributeString("groupAttachmentsBy", e.List.RootFolder.Properties["vti_emailattachmentfolders"].ToString());
 161:                 
 162:                 //Overwrite files with the same name, options 1/0
 163:                 if (e.List.RootFolder.Properties["vti_emailoverwrite"] != null)
 164:                     xmlWriter.WriteAttributeString("overwriteExisting", (e.List.RootFolder.Properties["vti_emailoverwrite"].ToString() == "1"?"Yes":"No"));
 165:                 
 166:                 //Save original e-mail, options 1/0
 167:                 if (e.List.RootFolder.Properties["vti_emailsaveoriginal"] != null)
 168:                     xmlWriter.WriteAttributeString("saveOriginal", (e.List.RootFolder.Properties["vti_emailsaveoriginal"].ToString()=="1"?"Yes":"No"));
 169:                 
 170:                 //Save meeting invitations, options 1/0
 171:                 if (e.List.RootFolder.Properties["vti_emailsavemeetings"] != null)
 172:                     xmlWriter.WriteAttributeString("saveMeetingInvitations", (e.List.RootFolder.Properties["vti_emailsavemeetings"].ToString()=="1"?"Yes":"No"));
 173:                 
 174:                 //Email Security Policy, options 1/0
 175:                 if (e.List.RootFolder.Properties["vti_emailusesecurity"] != null)
 176:                     xmlWriter.WriteAttributeString("securityPolicy", (e.List.RootFolder.Properties["vti_emailusesecurity"].ToString() == "1"?"UseDocLibPermissions":"AcceptFromAnySender"));
 177:  
 178:  
 179:                 xmlWriter.WriteEndElement();
 180:             }
 181:         }
 182:  
 183:     }
 184: }

The help for the command is shown below:

C:\>stsadm -help gl-enumemailenabledlists

stsadm -o gl-enumemailenabledlists


Outputs the lists that have been email enabled.

Parameters:
        [-scope <Farm | WebApplication | Site | Web>]
        [-url <url of web application, site, or web to iterate)>]
        [-output <file to save the results to>]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-enumemailenabledlists WSS v3, MOSS 2007 Released: 8/26/2008

Parameter Name Short Form Required Description Example Usage
scope s No One of the following values: Farm, WebApplication, Site, Web.  The command will recursively loop through each item contained within the specified scope level.  If omitted then defaults to "site". -scope site

-s site
url   Yes, unless the scope is Farm The URL to the web application, site collection, or web per the scope setting. -url http://portal
output   No The output path to save the results to.  Must be a valid filename.  If not specified then the results will be displayed in the console. -output "c:\lists.xml"

The following is an example of how to get the list of email enabled lists:

stsadm -o gl-enumemailenabledlists -scope site -url http://portal -output c:\lists.xml

The results of running the above command will look something like the following:

<Lists>
  <List siteUrl="http://portal" webUrl="/" listUrl="Documents" alias="test1@domain.com" groupAttachmentsBy="root" overwriteExisting="No" saveOriginal="No" saveMeetingInvitations="No" securityPolicy="UseDocLibPermissions" />
  <List siteUrl="http://portal" webUrl="/SubSite1" listUrl="Lists/Announcements" alias="test3@domain.com" saveOriginal="No" saveMeetingInvitations="No" securityPolicy="UseDocLibPermissions" />
  <List siteUrl="http://portal" webUrl="/SubSite1" listUrl="Documents" alias="test2@domain.com" groupAttachmentsBy="subject" overwriteExisting="Yes" saveOriginal="Yes" saveMeetingInvitations="Yes" securityPolicy="AcceptFromAnySender" />
</Lists>
16Aug/080

Updating the Default Content Access Account via STSADM

This is one that I've been wanting to address for a while and I finally decided to sit down and just do it.  If you've had your environment in place long enough to have to change the passwords you know that you can change most of the passwords using the out of the box STSADM commands - many refer to this support article from Microsoft on how to do this: http://support.microsoft.com/kb/934838.

Because there's so many accounts to change and so many places to visit this is definitely one of those things you want to have scripted (just be careful where you store your script).  If you look at the article though you'll notice that it doesn't address updating the user profile import account and it mentions that you have to manually change the default content access account.  I already have a command to change the user profile import account but I didn't have anything for changing the default content access account and having scripts with manual steps just kind of defeats the purpose in my opinion.  So, I created a new command which I called gl-updatedefaultcontentaccessaccount.

Setting the default content access account through code is real easy - you just call the SetDefaultGatheringAccount method of an instance of the Content class which can be obtained by calling the static GetContext method from the SearchContext object:

   1: /// <summary>
   2: /// Updates the account.
   3: /// </summary>
   4: /// <param name="sspName">Name of the SSP.</param>
   5: /// <param name="user">The user.</param>
   6: /// <param name="pwd">The PWD.</param>
   7: public static void UpdateAccount(string sspName, string user, string pwd)
   8: {
   9:     ServerContext context;
  10:     if (string.IsNullOrEmpty(sspName))
  11:         context = ServerContext.Default;
  12:     else
  13:         context = ServerContext.GetContext(sspName);
  14:  
  15:     SearchContext parent = SearchContext.GetContext(context);
  16:    
  17:     Content content = new Content(parent);
  18:     try
  19:     {
  20:         content.SetDefaultGatheringAccount(user, Utilities.CreateSecureString(pwd));
  21:     }
  22:     catch (RemotingException)
  23:     {
  24:         throw new Exception("Invalid login.");
  25:     }
  26:     catch (COMException)
  27:     {
  28:         throw new Exception("Invalid login.");
  29:     }
  30: }

The help for the command is shown below:

C:\>stsadm -help gl-updatedefaultcontentaccessaccount

stsadm -o gl-updatedefaultcontentaccessaccount


Sets the account to use as the default account when crawling content.  This account must have read access to the content being crawled. To avoid crawling unpublished versions of documents, ensure that this account is not an administrator on the target server.

Parameters:
        [-ssp <SSP name>]
        -username <DOMAIN\name>
        -password <password>

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-updatedefaultcontentaccessaccount MOSS 2007 Released: 8/15/2008

Parameter Name Short Form Required Description Example Usage
ssp   No The SSP that the account is associated with.  If omitted then the default SSP is used. -ssp SSP1
username u Yes The username of the account to use.  The account must have read access to the content being crawled.  To avoid crawling unpublished versions of documents, ensure that the account is not an administrator on the target server. -username "domain\sspcontent"

-u "domain\sspcontent"
password pwd Yes The password associated with the specified username. -password "pa$$w0rd"

-pwd "pa$$w0rd"

The following is an example of how to set the default content access account:

stsadm -o gl-updatedefaultcontentaccessaccount -username "domain\sspcontent" -password "pa$$w0rd"

I'll follow up this post with a sample password change script that I use which includes this command.

16Aug/083

Enabling Audience Targeting on a List

I thought I was done documenting my audience related STSADM commands and then I realized that I had completely forgotten one.  The first audience related command I had created was to enable audience targeting on a list - I called it gl-listaudiencetargeting.  I needed this because my current project had tons of lists that that needed audience targeting turned on.  Eventually these lists were added via a Feature but initially I did it via the command line with STSADM so that we could get a demo put together.  The nice thing is that adding the code to the Feature became a no-brainer because the code was already tested so it became simple copy and paste.

So, how do you programmatically enable audience targeting?  When I first started on this I thought it would be easy as setting a property on the list because enabling it via the browser is as simple as setting a checkbox but turns out it's not quite that simple.  The way audience targeting works is that a "special" field is added to the list.  The field schema looks like this:

<Field ID="61cbb965-1e04-4273-b658-eedaa662f48d" Type="TargetTo" Name="TargetTo" DisplayName="Target Audience" Required="FALSE" />

Once we have this schema we can add the field to the list using the AddFieldAsXml method (member of the SPFieldCollection class which can be obtained via the lists Fields property):

   1: /// <summary>
   2: /// Sets the whether audience targeting is enabled or not.
   3: /// </summary>
   4: /// <param name="url">The URL.</param>
   5: /// <param name="enabled">if set to <c>true</c> [enabled].</param>
   6: public static void SetTargeting(string url, bool enabled)
   7: {
   8:     using (SPSite site = new SPSite(url))
   9:     using (SPWeb web = site.OpenWeb())
  10:     {
  11:         SPList list = Utilities.GetListFromViewUrl(web, url);
  12:  
  13:         if (list == null)
  14:             throw new SPException("List was not found.");
  15:  
  16:         SPField targetingField = GetTargetingField(list);
  17:         if (enabled && (targetingField == null))
  18:         {
  19:             string createFieldAsXml = CreateFieldAsXml();
  20:             list.Fields.AddFieldAsXml(createFieldAsXml);
  21:             list.Update();
  22:         }
  23:         else if (!enabled && (targetingField != null))
  24:         {
  25:             list.Fields.Delete(targetingField.InternalName);
  26:             list.Update();
  27:         }
  28:  
  29:     }
  30: }
  31:  
  32: /// <summary>
  33: /// Gets the targeting field.
  34: /// </summary>
  35: /// <param name="list">The list.</param>
  36: /// <returns></returns>
  37: private static SPField GetTargetingField(SPList list)
  38: {
  39:     SPField field = null;
  40:     try
  41:     {
  42:         field = list.Fields[new Guid("61cbb965-1e04-4273-b658-eedaa662f48d")];
  43:     }
  44:     catch (ArgumentException)
  45:     {
  46:     }
  47:     return field;
  48: }
  49:  
  50: /// <summary>
  51: /// Gets the field as XML.
  52: /// </summary>
  53: /// <returns></returns>
  54: private static string CreateFieldAsXml()
  55: {
  56:     XmlElement element = new XmlDocument().CreateElement("Field");
  57:     element.SetAttribute("ID", "61cbb965-1e04-4273-b658-eedaa662f48d");
  58:     element.SetAttribute("Type", "TargetTo");
  59:     element.SetAttribute("Name", "TargetTo");
  60:     element.SetAttribute("DisplayName", "Target Audiences");
  61:     element.SetAttribute("Required", "FALSE");
  62:     return element.OuterXml;
  63: }

The help for the command is shown below:

C:\>stsadm -help gl-listaudiencetargeting

stsadm -o gl-listaudiencetargeting


Enabling audience targeting will create a targeting column for the list. Web parts, such as the Content Query Web Part, can use this data to filter list contents based on the user's context.

Parameters:
        -url <list view url>
        -enabled <true|false>

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-listaudiencetargeting MOSS 2007 Released: 8/6/2008

Parameter Name Short Form Required Description Example Usage
url url Yes The URL to the list that you wish to enable audience targeting on.  This can be the URL to the root folder or to a specific list view. -url http://portal/lists/announcements

or

-url http://portal/lists/announcements/forms/allitems.aspx
enabled e Yes "true" to enable audience targeting, "false" to disable audience targeting.  Note that disabling audience targeting deletes the "Target Audience" field and therefore any data that may have been assigned to the field. -enabled true

The following is an example of how to enable audience targeting for an announcements list:

stsadm -o gl-listaudiencetargeting -url http://portal/lists/announcements -enabled true

14Aug/081

Setting the Audience Compilation Schedule via STSADM

In an effort to wrap up my audience related STSADM commands I created a command that allows me to set the audience compilation schedule via STSADM.  I had to do some disassembling to figure out how to do this and it turned out that the code was virtually identical to what I had done for the gl-setuserprofileimportschedule command.  So it turned out that I was able to create this command by simply coping the code from my other command and then just tweaking a couple lines to load up different class types.  I named the command gl-setaudiencecompilationschedule.  The downside of this code (and the code it's based off of) is that I had to use reflection to get it done as all the classes are marked internally (no idea why).  If anyone knows of a way to do this without all the reflect I'm all ears.

Here's the code - it's ugly, but it works:

   1: #if MOSS
   2: using System;
   3: using System.Collections.Specialized;
   4: using System.Reflection;
   5: using System.Text;
   6: using System.Threading;
   7: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
   8: using Microsoft.Office.Server;
   9: using Microsoft.Office.Server.UserProfiles;
  10: using Microsoft.SharePoint;
  11: using Microsoft.SharePoint.Administration;
  12: using Microsoft.SharePoint.StsAdmin;
  13: using PropertyInfo=System.Reflection.PropertyInfo;
  14: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
  15:  
  16: namespace Lapointe.SharePoint.STSADM.Commands.TimerJob
  17: {
  18:     public class SetAudienceCompilationSchedule : SPOperation
  19:     {
  20:         private enum OccurrenceType
  21:         {
  22:             daily,
  23:             weekly,
  24:             monthly
  25:         }
  26:  
  27:         /// <summary>
  28:         /// Initializes a new instance of the <see cref="SetAudienceCompilationSchedule"/> class.
  29:         /// </summary>
  30:         public SetAudienceCompilationSchedule()
  31:         {
  32:             SPParamCollection parameters = new SPParamCollection();
  33:             parameters.Add(new SPParam("sspname", "ssp", false, null, new SPNonEmptyValidator(), "Please specify the SSP name."));
  34:             parameters.Add(new SPParam("occurrence", "oc", true, null, new SPRegexValidator("^daily$|^weekly$|^monthly$")));
  35:             parameters.Add(new SPParam("hour", "hour", true, null, new SPIntRangeValidator(0, 23)));
  36:             parameters.Add(new SPParam("day", "day", false, null, new SPIntRangeValidator(1, 31)));
  37:             string regex = "^" + string.Join("$|^", Enum.GetNames(typeof (DayOfWeek))) + "$";
  38:             parameters.Add(new SPParam("dayofweek", "dayofweek", false, null, new SPRegexValidator(regex.ToLowerInvariant() + "|" + regex)));
  39:             parameters.Add(new SPParam("enabled", "enabled", false, "true", new SPTrueFalseValidator()));
  40:             parameters.Add(new SPParam("runjob", "run"));
  41:            
  42:             StringBuilder sb = new StringBuilder();
  43:             sb.Append("\r\n\r\nSets the audience compilation schedule.\r\n\r\nParameters:");
  44:             sb.Append("\r\n\t[-sspname <SSP name>]");
  45:             sb.Append("\r\n\t-occurrence <daily|weekly|monthly>");
  46:             sb.Append("\r\n\t-hour <hour to run (0-23)>");
  47:             sb.Append("\r\n\t[-day <the day to run if monthly is specified>]");
  48:             sb.AppendFormat("\r\n\t[-dayofweek <the day of week to run if weekly is specified ({0})>]", string.Join("|", Enum.GetNames(typeof(DayOfWeek))).ToLowerInvariant());
  49:             sb.Append("\r\n\t[-enabled <true|false> (default is true)]");
  50:             sb.Append("\r\n\t[-runjob]");
  51:             Init(parameters, sb.ToString());
  52:         }
  53:  
  54:         #region ISPStsadmCommand Members
  55:  
  56:         /// <summary>
  57:         /// Gets the help message.
  58:         /// </summary>
  59:         /// <param name="command">The command.</param>
  60:         /// <returns></returns>
  61:         public override string GetHelpMessage(string command)
  62:         {
  63:             return HelpMessage;
  64:         }
  65:  
  66:         /// <summary>
  67:         /// Runs the specified command.
  68:         /// </summary>
  69:         /// <param name="command">The command.</param>
  70:         /// <param name="keyValues">The key values.</param>
  71:         /// <param name="output">The output.</param>
  72:         /// <returns></returns>
  73:         public override int Execute(string command, StringDictionary keyValues, out string output)
  74:         {
  75:             output = string.Empty;
  76:  
  77:             
  78:  
  79:             #region Check Arguments
  80:  
  81:             OccurrenceType occurrence = (OccurrenceType)Enum.Parse(typeof(OccurrenceType), Params["occurrence"].Value, true);
  82:             if (occurrence == OccurrenceType.monthly && !Params["day"].UserTypedIn)
  83:             {
  84:                 output = "Please specify the day to run the import.";
  85:                 output += GetHelpMessage(command);
  86:                 return (int)ErrorCodes.SyntaxError;
  87:             }
  88:             if (occurrence == OccurrenceType.weekly && !Params["dayofweek"].UserTypedIn)
  89:             {
  90:                 output = "Please specify the day of week to run the import.";
  91:                 output += GetHelpMessage(command);
  92:                 return (int)ErrorCodes.SyntaxError;
  93:             }
  94:  
  95:             #endregion
  96:  
  97:             string day = Params["day"].Value;
  98:             string dayofweek = Params["dayofweek"].Value;
  99:             string sspname = Params["sspname"].Value;
 100:             int hour = int.Parse(Params["hour"].Value);
 101:             bool enabled = bool.Parse(Params["enabled"].Value);
 102:             bool runJob = Params["runjob"].UserTypedIn;
 103:             if (!enabled && runJob)
 104:                 throw new SPSyntaxException("The runjob parameter cannot be specified when enabled is set to false.");
 105:  
 106:             ServerContext current;
 107:             if (Params["sspname"].UserTypedIn)
 108:                 current = ServerContext.GetContext(sspname);
 109:             else
 110:                 current = ServerContext.Default;
 111:  
 112:             // What follows is a whole lot of reflection which is required in order to get the SPScheduledJob object.
 113:             // Problem is that the only way to get the correct instance of this object is to use several internal
 114:             // classes, methods, and properties - why on earth these were not made public is absolutely beyond me!
 115:  
 116:             // The bulk of the reflection is recreating the following which was taken from 
 117:             // Microsoft.SharePoint.Portal.UserProfiles.AdminUI.Sched.InitializeComponent().
 118:             // Once we have the job objects we can start setting properties.
 119:             /*
 120:             private void InitializeComponent()
 121:             {
 122:                 ServerContext current = ServerContext.Current;
 123:                 UserProfileApplication userProfileApplication = current.UserProfileApplication;
 124:                 try
 125:                 {
 126:                     using (PortalApplication.BeginSecurityContext())
 127:                     {
 128:                         JobSchedulerSharedApplicationCollection applications = new JobSchedulerSharedApplicationCollection(SPFarm.Local.Services.GetValue<JobSchedulerService>(string.Empty));
 129:                         JobSchedulerSharedApplication sharedApplication = (JobSchedulerSharedApplication) applications[current.SharedResourceProvider];
 130:                         ScheduledJobCollection jobs = new ScheduledJobCollection(sharedApplication);
 131:                         this.AudienceCompileScheduler.Job = jobs[userProfileApplication.AudienceCompilationJobId];
 132:                     }
 133:                 }
 134:                 catch (Exception)
 135:                 {
 136:                     throw;
 137:                 }
 138:             }
 139:             */
 140:  
 141:             // UserProfileApplication userProfileApplication = current.UserProfileApplication;
 142:             object userProfileApplication = Utilities.GetPropertyValue(current, "UserProfileApplication");
 143:  
 144:             // The SSP is locked down so we need to use reflection to get at it.
 145:             object sharedResourceProvider = Utilities.GetSharedResourceProvider(current);
 146:  
 147:             // JobSchedulerService jobSchedulerService = SPFarm.Local.Services.GetValue(typeof(JobSchedulerService));
 148:             Type jobSchedulerServiceType = Type.GetType("Microsoft.Office.Server.Administration.JobSchedulerService, Microsoft.Office.Server, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
 149:  
 150:  
 151:             MethodInfo getValue =
 152:                 SPFarm.Local.Services.GetType().GetMethod("GetValue",
 153:                                                           BindingFlags.NonPublic | BindingFlags.Public |
 154:                                                           BindingFlags.Instance | BindingFlags.InvokeMethod, null, new Type[] {typeof(Type), typeof(string)}, null);
 155:  
 156:             object jobSchedulerService = getValue.Invoke(SPFarm.Local.Services,
 157:                                                           new object[]
 158:                                                               {
 159:                                                                   jobSchedulerServiceType, string.Empty
 160:                                                               });
 161:  
 162:  
 163:             // JobSchedulerSharedApplicationCollection application = new JobSchedulerSharedApplicationCollection(jobSchedulerServiceType);
 164:             Type jobSchedulerSharedApplicationCollectionType = Type.GetType("Microsoft.Office.Server.Administration.JobSchedulerSharedApplicationCollection, Microsoft.Office.Server, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
 165:  
 166:             ConstructorInfo jobSchedulerSharedApplicationCollectionConstructor =
 167:                 jobSchedulerSharedApplicationCollectionType.GetConstructor(
 168:                     BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public,
 169:                     null,
 170:                     new Type[] {jobSchedulerService.GetType()}, null);
 171:             object applications = jobSchedulerSharedApplicationCollectionConstructor.Invoke(new object[] { jobSchedulerService });
 172:  
 173:             // JobSchedulerSharedApplication jobSchedulerSharedApplication = applications[sharedResourceProvider];
 174:             PropertyInfo itemProp = applications.GetType().GetProperty("Item",
 175:                                                                      BindingFlags.NonPublic |
 176:                                                                      BindingFlags.Instance |
 177:                                                                      BindingFlags.InvokeMethod |
 178:                                                                      BindingFlags.GetProperty |
 179:                                                                      BindingFlags.Public);
 180:             object jobSchedulerSharedApplication = itemProp.GetValue(applications, new object[] { sharedResourceProvider });
 181:  
 182:  
 183:             //ScheduledJobCollection scheduledJobCollection = new ScheduledJobCollection(sharedApplication);
 184:             Type scheduledJobCollectionType = Type.GetType("Microsoft.Office.Server.Administration.ScheduledJobCollection, Microsoft.Office.Server, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
 185:             ConstructorInfo scheduledJobCollectionConstructor =
 186:                 scheduledJobCollectionType.GetConstructor(
 187:                     BindingFlags.NonPublic | BindingFlags.Instance | BindingFlags.InvokeMethod | BindingFlags.Public,
 188:                     null,
 189:                     new Type[] {jobSchedulerSharedApplication.GetType()}, null);
 190:             object scheduledJobCollection = scheduledJobCollectionConstructor.Invoke(new object[] { jobSchedulerSharedApplication });
 191:  
 192:  
 193:             // userProfileApplication.AudienceCompilationJobId
 194:             Guid audienceCompilationJobId = (Guid)Utilities.GetPropertyValue(userProfileApplication, "AudienceCompilationJobId");
 195:  
 196:  
 197:  
 198:             // ScheduledJob compilationJob = scheduledJobCollection[audienceCompilationJobId];
 199:             itemProp = scheduledJobCollection.GetType().GetProperty("Item",
 200:                                                                     BindingFlags.NonPublic |
 201:                                                                     BindingFlags.Instance |
 202:                                                                     BindingFlags.InvokeMethod |
 203:                                                                     BindingFlags.GetProperty |
 204:                                                                     BindingFlags.Public);
 205:             object compilationJob = itemProp.GetValue(scheduledJobCollection, new object[] { audienceCompilationJobId });
 206:  
 207:  
 208:             PropertyInfo scheduleProp = compilationJob.GetType().GetProperty("Schedule",
 209:                                                                             BindingFlags.FlattenHierarchy |
 210:                                                                             BindingFlags.NonPublic |
 211:                                                                             BindingFlags.Instance |
 212:                                                                             BindingFlags.InvokeMethod |
 213:                                                                             BindingFlags.GetProperty |
 214:                                                                             BindingFlags.Public);
 215:  
 216:             MethodInfo update =
 217:                 compilationJob.GetType().GetMethod("Update",
 218:                                                   BindingFlags.NonPublic | 
 219:                                                   BindingFlags.Public |
 220:                                                   BindingFlags.Instance | 
 221:                                                   BindingFlags.InvokeMethod |
 222:                                                   BindingFlags.FlattenHierarchy, 
 223:                                                   null,
 224:                                                   new Type[] {typeof (bool)}, null);
 225:             
 226:             // Woohoo!!! We are finally at a point where we can actually set the schedule - what a pain the @$$ that was!!!
 227:             SPSchedule schedule;
 228:             
 229:             if (occurrence == OccurrenceType.daily)
 230:             {
 231:                 schedule = SetUserProfileImportSchedule.ScheduledJobHelper.GetScheduleDaily(hour);
 232:             }
 233:             else if (occurrence == OccurrenceType.weekly)
 234:             {
 235:                 schedule = SetUserProfileImportSchedule.ScheduledJobHelper.GetScheduleWeekly((DayOfWeek)Enum.Parse(typeof(DayOfWeek), dayofweek, true), hour);
 236:             }
 237:             else if (occurrence == OccurrenceType.monthly)
 238:             {
 239:                 schedule = SetUserProfileImportSchedule.ScheduledJobHelper.GetScheduleMonthly(int.Parse(day), hour);
 240:             }
 241:             else
 242:                 throw new Exception("Unknown occurance type.");
 243:  
 244:             Type scheduledJobType = Type.GetType("Microsoft.Office.Server.Administration.ScheduledJob, Microsoft.Office.Server, Version=12.0.0.0, Culture=neutral, PublicKeyToken=71e9bce111e9429c");
 245:  
 246:  
 247:             // fullImportJob.Schedule = schedule;
 248:             scheduleProp.SetValue(compilationJob, schedule, null);
 249:  
 250:             // fullImportJob.Enabled = enabled;
 251:             Utilities.SetPropertyValue(compilationJob, scheduledJobType, "Disabled", !enabled);
 252:  
 253:             // fullImportJob.Update(true);
 254:             update.Invoke(compilationJob, new object[] { true });
 255:  
 256:             if (runJob)
 257:             {
 258:                 // fullImportJob.Execute();
 259:                 Utilities.ExecuteMethod(compilationJob, "Execute", new Type[] { }, new object[] { });
 260:             }
 261:  
 262:             if (runJob)
 263:             {
 264:                 // We want to wait until the import is finished before moving on in case we are being run in a batch that requires this to complete before continueing.
 265:                 UserProfileConfigManager manager = new UserProfileConfigManager(current);
 266:                 while (manager.IsImportInProgress())
 267:                     Thread.Sleep(500);
 268:             }
 269:  
 270:  
 271:             return OUTPUT_SUCCESS;
 272:         }
 273:  
 274:         #endregion
 275:  
 276:     }
 277: }
 278: #endif

The help for the command is shown below:

C:\>stsadm -help gl-setaudiencecompilationschedule

stsadm -o gl-setaudiencecompilationschedule


Sets the audience compilation schedule.

Parameters:
        [-sspname <SSP name>]
        -occurrence <daily|weekly|monthly>
        -hour <hour to run (0-23)>
        [-day <the day to run if monthly is specified>]
        [-dayofweek <the day of week to run if weekly is specified (sunday|monday|tuesday|wednesday|thursday|friday|saturday)>]
        [-enabled <true|false> (default is true)]
        [-runjob]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-setaudiencecompilationschedule MOSS 2007 Release: 8/14/2008

Parameter Name Short Form Required Description Example Usage
sspname ssp No The name of the SSP that the audiences to compile are associated with.  If omitted the default SSP will be used. -sspname SSP1

-ssp SSP1
occurrence oc Yes Specifies how frequently the compilation should occur.  Valid values are "daily", "weekly", and "monthly". -occurrence daily

-oc monthly
hour   Yes The hour in which to run the compilation.  This should be an integer between 0 and 23 (where 0 is 12:00am and 23 is 11:00pm). -hour 22
day   No, unless occurrence is monthly The day of the month to run the compilation job.  Valid values are between 1 and 31. -day 1
dayofweek   No, unless occurrence is weekly The day of the week to run the compilation job.  Valid values are "sunday", "monday", "tuesday", "wednesday", "thursday", and "saturday". -dayofweek saturday
enabled   No "true" to enable the compilation schedule, "false" to disable it.  If not specified then the compilation schedule will be enabled. -enabled true
runjob run No If specified then the compilation job will be immediately executed after setting the schedule. -runjob

-run

The following is an example of how to set the compilation schedule to run every Satruday at 10:00pm:

stsadm -o gl-setaudiencecompilationschedule -occurrence weekly -hour 22 -dayofweek saturday -enabled true -runjob

14Aug/0812

Importing Profile Properties

The project that I'm currently on has a test environment in which many configurations were made to the user profile properties settings.  I just began the process of building out their production environment and was faced with a minor issue - how do I get all the settings that have been applied to the test environments profile properties migrated to the production environment in a reliable and repeatable way?  I took a look around and remembered that I already had a command to dump out the profile property settings into an XML file - gl-enumprofileproperties - so now I just needed another command that could take the output from my previous command and use it to import those settings into another farm.

Fortunately this turned out to be very easy (with one minor caveat).  I called this new command gl-importprofileproperties.  First lets take a look at a sample of the output generated by the gl-enumprofileproperties command:

<Properties>
  <Property>
    <Name>UserProfile_GUID</Name>
    <AllowPolicyOverride>False</AllowPolicyOverride>
    <ChoiceList />
    <ChoiceType>Off</ChoiceType>
    <DefaultPrivacy>Public</DefaultPrivacy>
    <Description />
    <DisplayName>Id</DisplayName>
    <DisplayOrder>1</DisplayOrder>
    <IsAdminEditable>False</IsAdminEditable>
    <IsAlias>False</IsAlias>
    <IsColleagueEventLog>False</IsColleagueEventLog>
    <IsImported>False</IsImported>
    <IsMultivalued>False</IsMultivalued>
    <IsReplicable>False</IsReplicable>
    <IsRequired>True</IsRequired>
    <IsSearchable>True</IsSearchable>
    <IsSection>False</IsSection>
    <IsSystem>True</IsSystem>
    <IsUpgrade>False</IsUpgrade>
    <IsUpgradePrivate>False</IsUpgradePrivate>
    <IsUserEditable>False</IsUserEditable>
    <IsVisibleOnEditor>False</IsVisibleOnEditor>
    <IsVisibleOnViewer>False</IsVisibleOnViewer>
    <Length>0</Length>
    <ManagedPropertyName>UserProfile_GUID</ManagedPropertyName>
    <MaximumShown>10</MaximumShown>
    <PrivacyPolicy>Mandatory</PrivacyPolicy>
    <Separator>Unknown</Separator>
    <Type>unique identifier</Type>
    <URI>urn:schemas-microsoft-com:sharepoint:portal:profile:UserProfile_GUID</URI>
    <UserOverridePrivacy>False</UserOverridePrivacy>
    <ImportMapping />
  </Property>
  <Property>
    <Name>SID</Name>
    <AllowPolicyOverride>False</AllowPolicyOverride>
    <ChoiceList />
    <ChoiceType>Off</ChoiceType>
    <DefaultPrivacy>Public</DefaultPrivacy>
    <Description />
    <DisplayName>SID</DisplayName>
    <DisplayOrder>2</DisplayOrder>
    <IsAdminEditable>False</IsAdminEditable>
    <IsAlias>False</IsAlias>
    <IsColleagueEventLog>False</IsColleagueEventLog>
    <IsImported>True</IsImported>
    <IsMultivalued>False</IsMultivalued>
    <IsReplicable>False</IsReplicable>
    <IsRequired>False</IsRequired>
    <IsSearchable>False</IsSearchable>
    <IsSection>False</IsSection>
    <IsSystem>True</IsSystem>
    <IsUpgrade>False</IsUpgrade>
    <IsUpgradePrivate>False</IsUpgradePrivate>
    <IsUserEditable>False</IsUserEditable>
    <IsVisibleOnEditor>False</IsVisibleOnEditor>
    <IsVisibleOnViewer>False</IsVisibleOnViewer>
    <Length>512</Length>
    <ManagedPropertyName>SID</ManagedPropertyName>
    <MaximumShown>10</MaximumShown>
    <PrivacyPolicy>OptIn</PrivacyPolicy>
    <Separator>Unknown</Separator>
    <Type>binary</Type>
    <URI>urn:schemas-microsoft-com:sharepoint:portal:profile:SID</URI>
    <UserOverridePrivacy>False</UserOverridePrivacy>
    <ImportMapping>
      <DSPropName>objectSID</DSPropName>
      <ConnectionName />
      <AssociationName />
    </ImportMapping>
  </Property>
  ...
</Properties>

As you can see from the above XML there's really not much to - it's just a dump of all the properties (including read-only properties) that are found on the Microsoft.Office.Server.UserProfiles.Property object.  So, importing these settings back in is a simple matter of looping through the Property XML elements shown above, finding the right Property object, and then set each property value.  Here's the core code that accomplishes this:

   1: /// <summary>
   2: /// Imports the user profile properties using the provided input file.
   3: /// </summary>
   4: /// <param name="sspName">Name of the SSP.</param>
   5: /// <param name="input">The input.</param>
   6: /// <param name="removeMissing">if set to <c>true</c> [remove missing].</param>
   7: public static void Import(string sspName, string input, bool removeMissing)
   8: {
   9:     ServerContext serverContext;
  10:     if (string.IsNullOrEmpty(sspName))
  11:         serverContext = ServerContext.Default;
  12:     else
  13:         serverContext = ServerContext.GetContext(sspName);
  14:  
  15:     UserProfileConfigManager manager = new UserProfileConfigManager(serverContext);
  16:     PropertyCollection properties = manager.GetProperties();
  17:  
  18:     XmlDocument xmlDoc = new XmlDocument();
  19:     xmlDoc.Load(input);
  20:  
  21:     Log("Import started at {0}", DateTime.Now.ToString());
  22:  
  23:     try
  24:     {
  25:         bool displayOrderChanged = false;
  26:         foreach (XmlElement propElement in xmlDoc.SelectNodes("//Property"))
  27:         {
  28:             string propName = propElement.SelectSingleNode("Name").InnerText;
  29:             Property prop = properties.GetPropertyByName(propName);
  30:             bool isNew = false;
  31:             if (prop == null)
  32:             {
  33:                 Log("Progress: Creating property '{0}'.", propName);
  34:                 // We couldn't find a matching property so go ahead and create it
  35:                 prop = properties.Create(bool.Parse(propElement.SelectSingleNode("IsSection").InnerText));
  36:                 prop.Name = propName;
  37:                 isNew = true;
  38:                 SetProperties(prop, propElement, isNew, manager);
  39:                 properties.Add(prop);
  40:             }
  41:             Log("Progress: Setting properties for '{0}'.", propName);
  42:             try
  43:             {
  44:                 if (!isNew)
  45:                     SetProperties(prop, propElement, isNew, manager);
  46:                 prop.Commit();
  47:  
  48:                 SetDataMapping(propElement, prop, manager);
  49:  
  50:                 Log("Progress: Property '{0}' imported.", propName);
  51:             }
  52:             catch (Exception ex)
  53:             {
  54:                 Log("ERROR: {0}\r\n{1}", EventLogEntryType.Error, ex.Message, ex.StackTrace);
  55:             }
  56:         }
  57:  
  58:         if (removeMissing)
  59:         {
  60:             Log("Progress: Removing properties not included in the import...");
  61:             PropertyCollection workColl = manager.GetProperties();
  62:             foreach (Property prop in properties)
  63:             {
  64:                 if (xmlDoc.SelectSingleNode(string.Format("//Property[Name='{0}']", prop.Name)) == null)
  65:                 {
  66:                     Log("Progress: Removing property '{0}'.", prop.Name);
  67:                     workColl.RemovePropertyByName(prop.Name);
  68:                 }
  69:             }
  70:         }
  71:  
  72:         Log("Progress: Setting display order...");
  73:         properties = manager.GetProperties();
  74:         foreach (XmlElement propElement in xmlDoc.SelectNodes("//Property"))
  75:         {
  76:             string propName = propElement.SelectSingleNode("Name").InnerText;
  77:             Property prop = properties.GetPropertyByName(propName);
  78:  
  79:             if (!string.IsNullOrEmpty(propElement.SelectSingleNode("DisplayOrder").InnerText))
  80:             {
  81:                 int displayOrder = int.Parse(propElement.SelectSingleNode("DisplayOrder").InnerText);
  82:                 if (displayOrder != prop.DisplayOrder)
  83:                 {
  84:                     Log("Progress: Setting display order for '{0}' to '{1}'.", prop.Name, displayOrder.ToString());
  85:                     properties.SetDisplayOrderByPropertyName(prop.Name, displayOrder);
  86:                     displayOrderChanged = true;
  87:                 }
  88:             }
  89:             
  90:         }
  91:         if (displayOrderChanged)
  92:         {
  93:             Log("Progress: Committing display order.");
  94:             properties.CommitDisplayOrder();
  95:         }
  96:     }
  97:     catch (Exception ex)
  98:     {
  99:         Log("ERROR: {0}\r\n{1}", EventLogEntryType.Error, ex.Message, ex.StackTrace);
 100:     }
 101:     finally
 102:     {
 103:         Log("Import finished at {0}", DateTime.Now.ToString());
 104:     }
 105: }
 106:  
 107: /// <summary>
 108: /// Sets the properties.
 109: /// </summary>
 110: /// <param name="prop">The prop.</param>
 111: /// <param name="propXml">The prop XML.</param>
 112: /// <param name="isNew">if set to <c>true</c> [is new].</param>
 113: /// <param name="manager">The manager.</param>
 114: private static void SetProperties(Property prop, XmlElement propXml, bool isNew, UserProfileConfigManager manager)
 115: {
 116:     string displayName = propXml.SelectSingleNode("DisplayName").InnerText;
 117:     if (isNew)
 118:     {
 119:         prop.DisplayName = displayName;
 120:  
 121:         bool isMultivalued = bool.Parse(propXml.SelectSingleNode("IsMultivalued").InnerText);
 122:         if (isMultivalued != prop.IsMultivalued)
 123:             prop.IsMultivalued = isMultivalued;
 124:  
 125:         int length = int.Parse(propXml.SelectSingleNode("Length").InnerText);
 126:         if (prop.Length != length)
 127:             prop.Length = length;
 128:  
 129:         string type = propXml.SelectSingleNode("Type").InnerText;
 130:         if (type != prop.Type)
 131:             prop.Type = type;
 132:     }
 133:  
 134:     //prop.AllowPolicyOverride = bool.Parse(propXml.SelectSingleNode("AllowPolicyOverride").InnerText);
 135:     //prop.DisplayOrder = propXml.SelectSingleNode("DisplayOrder").InnerText;
 136:     //prop.IsAdminEditable = bool.Parse(propXml.SelectSingleNode("IsAdminEditable").InnerText);
 137:     //prop.IsImported = bool.Parse(propXml.SelectSingleNode("IsImported").InnerText);
 138:     //prop.IsRequired = bool.Parse(propXml.SelectSingleNode("IsRequired").InnerText);
 139:     //prop.IsSection = bool.Parse(propXml.SelectSingleNode("IsSection").InnerText);
 140:     //prop.ManagedPropertyName = propXml.SelectSingleNode("ManagedPropertyName").InnerText;
 141:     //prop.URI = propXml.SelectSingleNode("URI").InnerText;
 142:     
 143:     if (displayName != prop.DisplayName)
 144:         prop.DisplayName = displayName;
 145:     
 146:     Privacy defaultPrivacy = (Privacy) Enum.Parse(typeof (Privacy), propXml.SelectSingleNode("DefaultPrivacy").InnerText, true);
 147:     if (defaultPrivacy != prop.DefaultPrivacy)
 148:         prop.DefaultPrivacy = defaultPrivacy;
 149:  
 150:     string desc = propXml.SelectSingleNode("Description").InnerText;
 151:     if (desc != prop.Description)
 152:         prop.Description = desc;
 153:  
 154:     bool isAlias = bool.Parse(propXml.SelectSingleNode("IsAlias").InnerText);
 155:     if (isAlias != prop.IsAlias)
 156:         prop.IsAlias = isAlias;
 157:  
 158:     bool isColleagueEventLog = bool.Parse(propXml.SelectSingleNode("IsColleagueEventLog").InnerText);
 159:     if (isColleagueEventLog != prop.IsColleagueEventLog)
 160:         prop.IsColleagueEventLog = isColleagueEventLog;
 161:  
 162:     bool isReplicable = bool.Parse(propXml.SelectSingleNode("IsReplicable").InnerText);
 163:     if (isReplicable != prop.IsReplicable)
 164:         prop.IsReplicable = isReplicable;
 165:  
 166:     bool isSearchable = bool.Parse(propXml.SelectSingleNode("IsSearchable").InnerText);
 167:     if (isSearchable != prop.IsSearchable)
 168:         prop.IsSearchable = isSearchable;
 169:  
 170:     bool isUpgrade = bool.Parse(propXml.SelectSingleNode("IsUpgrade").InnerText);
 171:     if (isUpgrade != prop.IsUpgrade)
 172:         prop.IsUpgrade = isUpgrade;
 173:  
 174:     bool isUpgradePrivate = bool.Parse(propXml.SelectSingleNode("IsUpgradePrivate").InnerText);
 175:     if (isUpgradePrivate != prop.IsUpgradePrivate)
 176:         prop.IsUpgradePrivate = isUpgradePrivate;
 177:  
 178:     bool isUserEditable = bool.Parse(propXml.SelectSingleNode("IsUserEditable").InnerText);
 179:     if (isUserEditable != prop.IsUserEditable)
 180:         prop.IsUserEditable = isUserEditable;
 181:  
 182:     bool isVisibleOnEditor = bool.Parse(propXml.SelectSingleNode("IsVisibleOnEditor").InnerText);
 183:     if (isVisibleOnEditor != prop.IsVisibleOnEditor)
 184:         prop.IsVisibleOnEditor = isVisibleOnEditor;
 185:  
 186:     bool isVisibleOnViewer = bool.Parse(propXml.SelectSingleNode("IsVisibleOnViewer").InnerText);
 187:     if (isVisibleOnViewer != prop.IsVisibleOnViewer)
 188:         prop.IsVisibleOnViewer = isVisibleOnViewer;
 189:  
 190:     int maxShown = int.Parse(propXml.SelectSingleNode("MaximumShown").InnerText);
 191:     if (maxShown != prop.MaximumShown)
 192:         prop.MaximumShown = maxShown;
 193:  
 194:     PrivacyPolicy privacyPolicy = (PrivacyPolicy)Enum.Parse(typeof(PrivacyPolicy), propXml.SelectSingleNode("PrivacyPolicy").InnerText, true);
 195:     if (privacyPolicy != prop.PrivacyPolicy)
 196:         prop.PrivacyPolicy = privacyPolicy;
 197:  
 198:     MultiValueSeparator separator = (MultiValueSeparator)Enum.Parse(typeof(MultiValueSeparator), propXml.SelectSingleNode("Separator").InnerText, true);
 199:     if (separator != prop.Separator)
 200:         prop.Separator = separator;
 201:  
 202:     bool userOverridePrivacy = bool.Parse(propXml.SelectSingleNode("UserOverridePrivacy").InnerText);
 203:     if (userOverridePrivacy != prop.UserOverridePrivacy)
 204:         prop.UserOverridePrivacy = userOverridePrivacy;
 205:  
 206:     ChoiceTypes choiceType = (ChoiceTypes)Enum.Parse(typeof(ChoiceTypes), propXml.SelectSingleNode("ChoiceType").InnerText, true);
 207:     if (choiceType != prop.ChoiceType)
 208:         prop.ChoiceType = choiceType;
 209:  
 210:  
 211:     foreach (XmlElement choiceXml in propXml.SelectNodes("ChoiceList/Choice"))
 212:     {
 213:         if (prop.ChoiceList.FindTerms(choiceXml.InnerText, ChoiceListSearchOption.ExactMatch).Length == 0)
 214:         {
 215:             prop.ChoiceList.Add(choiceXml.InnerText);
 216:             // Settng choice list values doesn't mark the property as dirty so we have to manually mark it using reflection.
 217:             Utilities.SetFieldValue(prop, typeof(Property), "m_fIsChanged", true);
 218:         }
 219:     }
 220: }
 221:  
 222: /// <summary>
 223: /// Sets the data mapping.
 224: /// </summary>
 225: /// <param name="propXml">The prop XML.</param>
 226: /// <param name="prop">The prop.</param>
 227: /// <param name="manager">The manager.</param>
 228: private static void SetDataMapping(XmlElement propXml, Property prop, UserProfileConfigManager manager)
 229: {
 230:     PropertyMapCollection propertyMapping = manager.GetDataSource().PropertyMapping;
 231:     if (propXml.SelectSingleNode("ImportMapping") != null && propXml.SelectSingleNode("ImportMapping").ChildNodes.Count > 0)
 232:     {
 233:         PropertyMap map = propertyMapping[prop.Name];
 234:         
 235:         string dsPropName = propXml.SelectSingleNode("ImportMapping/DSPropName").InnerText;
 236:         string connectionName = propXml.SelectSingleNode("ImportMapping/ConnectionName").InnerText;
 237:         string associationName = propXml.SelectSingleNode("ImportMapping/AssociationName").InnerText;
 238:  
 239:         // Remove any mappings that are assigned to the current mapping.
 240:         if (!string.IsNullOrEmpty(dsPropName))
 241:         {
 242:             foreach (PropertyMap tempMap in propertyMapping)
 243:             {
 244:                 if (tempMap.DSPropName == dsPropName && 
 245:                     tempMap.ConnectionName == connectionName && 
 246:                     tempMap.AssociationName == associationName && 
 247:                     tempMap.PropName != prop.Name)
 248:                 {
 249:                     Log("Progress: Removing mapping associated with '{0}' so that it may be assigned to '{1}'.",
 250:                         tempMap.PropName, prop.Name);
 251:                     propertyMapping.Remove(tempMap.PropName);
 252:                     propertyMapping = manager.GetDataSource().PropertyMapping;
 253:                     break;
 254:                 }
 255:             }
 256:         }
 257:  
 258:         if (map == null)
 259:         {
 260:             Log("Progress: Adding import mapping to '{0}'.", prop.Name);
 261:             propertyMapping.Add(prop.Name, dsPropName, connectionName, associationName);
 262:         }
 263:         else
 264:         {
 265:             Log("Progress: Updating import mapping for '{0}'.", prop.Name);
 266:             if (map.DSPropName != dsPropName)
 267:                 map.DSPropName = dsPropName;
 268:  
 269:             if (map.ConnectionName != connectionName)
 270:                 map.ConnectionName = connectionName;
 271:  
 272:             if (map.AssociationName != associationName)
 273:                 map.AssociationName = associationName;
 274:  
 275:             map.Commit();
 276:         }
 277:     }
 278:     else if (propertyMapping[prop.Name] != null)
 279:     {
 280:         Log("Progress: Removing import mapping for '{0}'.", prop.Name);
 281:         propertyMapping.Remove(prop.Name);
 282:     }
 283: }

So I mentioned that there was one caveat that I discovered - turns out that it's more of a bug.  If you do an export and then immediately import using this command you will likely get an exception when it comes to importing the SPS-ProxyAddresses field.  Here's the specific exception you will see:

Progress: Setting properties for 'SPS-ProxyAddresses'.
Progress: Updating import mapping for 'SPS-ProxyAddresses'.
ERROR: The character length specified is invalid.
   at Microsoft.Office.Server.UserProfiles.Property._TypeValidate(DBAction action)
   at Microsoft.Office.Server.UserProfiles.Property._TypeValidate(IEnumerable enumAddPropertyList, IEnumerable enumUpdatePropertyList)
   at Microsoft.Office.Server.UserProfiles.Property._Update(SRPSite site, IEnumerable enumAddPropertyList, IEnumerable enumUpdatePropertyList, IEnumerable enumRemovePropertyList)
   at Microsoft.Office.Server.UserProfiles.Property.Commit()
   at Lapointe.SharePoint.STSADM.Commands.UserProfiles.ImportProfileProperties.Execute(String command, StringDictionary keyValues, String& output)

So what's the deal with this?  Simple - it's a bug.  Here's another way you can expose the bug - via the browser go to your user profile properties page and then click to edit the SPS-ProxyAddresses field.  Then, on the edit screen, simply click the "OK" button without making any changes - you'll get the following error:

clip_image002

The problem is that this particular field is a multi-value field which can only have a maximum length of 400 but the field length is set to 2048 and because it's a built-in system field we can't change the length to fix it and therefore we can't change anything about the property.

So, to deal with this I made it so that my code simply dumps out errors like this and moves on to the next item - but you will see this error every time you attempt an import - there's no way around it (you can't change the length of an existing field so you can't fix the problem to prevent the error).

The help for the command is shown below:

C:\>stsadm -help gl-importprofileproperties

stsadm -o gl-importprofileproperties


Imports user profile properties using the output of gl-enumprofileproperties.

Parameters:
        -inputfile <file to import results from>
        [-sspname <name of the SSP>]
        [-removemissing (removes properties missing from the import)

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-importprofileproperties MOSS 2007 Released: 8/14/2008
Updated: 8/22/2008

Parameter Name Short Form Required Description Example Usage
sspname ssp No The name of the SSP that the profile properties are associated with.  If omitted the default SSP will be used. -sspname SSP1

-ssp SSP1
inputfile input Yes The path to the XML file to use for import.  The XML must correspond to the XML generated by the gl-enumprofileproperties command. -inputfile c:\properties.xml

-input c:\properties.xml
removemissing rm No Deletes any properties that were not defined in the import file.  It's recommended that if you use this flag that you first backup the existing properties using the gl-enumprofileproperties command. -removemissing

-rm

The following is an example of how to import user profile properties:

stsadm -o gl-importprofileproperties -inputfile c:\properties.xml

Update 8/22/2008: I fixed a few bugs that were preventing the importing of new properties.  I also fixed it so that data mappings can be reassigned without having to run multiple times.  I also added support for setting the display order and for removing properties that do not exist in the import by passing in the -removemissing parameter.

14Aug/080

Deleting an Audience via STSADM

Using the commands I've created so far you can now create audiences, add rules, and display those rules all via STSADM.  If you're like me and you do a lot of testing of this stuff before you push out the final version then you'll want a way to also easily delete audiences that you've created.  Of course you could easily do this via the browser but what if you've added complex rules to your audience using my gl-addaudiencerule command?  You'll find that you can't delete it from the browser.  To deal with this I've created an additional command called, simply enough, gl-deleteaudience.

One thing I added to the command was an option to delete only the rules and not the audience itself.  This allows you to essentially revert the audience back to a pure state so that you can manage it via the browser if you so desire.  The code to delete an audience is real simple - essentially one line: manager.Audiences.Remove(audience.AudienceID); - the rest is just error handling and clearing of the rules:

   1: /// <summary>
   2: /// Deletes the specified audience or all audience rules for the specified audience.
   3: /// </summary>
   4: /// <param name="sspName">Name of the SSP.</param>
   5: /// <param name="audienceName">Name of the audience.</param>
   6: /// <param name="deleteRulesOnly">if set to <c>true</c> [delete rules only].</param>
   7: private void Delete(string sspName, string audienceName, bool deleteRulesOnly)
   8: {
   9:     ServerContext context;
  10:     if (string.IsNullOrEmpty(sspName))
  11:         context = ServerContext.Default;
  12:     else
  13:         context = ServerContext.GetContext(sspName);
  14:  
  15:     AudienceManager manager = new AudienceManager(context);
  16:  
  17:     if (!manager.Audiences.AudienceExist(audienceName))
  18:     {
  19:         throw new SPException("Audience name does not exist");
  20:     }
  21:  
  22:     Audience audience = manager.Audiences[audienceName];
  23:  
  24:     if (audience.AudienceRules != null && deleteRulesOnly)
  25:     {
  26:         audience.AudienceRules = new ArrayList();
  27:         audience.GroupOperation = AudienceGroupOperation.AUDIENCE_OR_OPERATION;
  28:         audience.Commit();
  29:         return;
  30:     }
  31:     if (!deleteRulesOnly)
  32:         manager.Audiences.Remove(audience.AudienceID);
  33: }

The help for the command is shown below:

C:\>stsadm -help gl-deleteaudience

stsadm -o gl-deleteaudience


Deletes an audience.  To delete only the rules associated with the audience pass in the -rulesonly parameter.

Parameters:
        -name <audience name>
        [-ssp <SSP name>]
        [-rulesonly (delete only the rules)]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-deleteaudience MOSS 2007 8/6/2008

Parameter Name Short Form Required Description Example Usage
name n Yes The name of the audience to delete. -name "IT"

-n "IT"
ssp   No The name of the SSP that the audience is associated with.  If omitted the default SSP will be used. -ssp SSP1
-rulesonly ro No If specified then only the audience rules will be deleted and not the audience itself. -rulesonly

-ro

The following is an example of how to delete an audience:

stsadm -o gl-deleteaudience -name "IT"

The following is an example of how to delete only the rules associated with an audience

stsadm -o gl-deleteaudience -name "IT" -rulesonly

9Aug/080

Displaying Audience Rules via STSADM

Okay, so you've created your audience and used my gl-addaudiencerule command to add some complex rules to the audience.  Three months later you've forgotten what those rules are and need to see them again - unfortunately you can't do that via the browser, so what do you do?  Simple, you run gl-enumaudiencerules, another command I've created to help manage audiences via STSADM.

This command is really simple so I'll be brief - I simply loop through all the rules associated with an audience and display back an XML structure representing those rules.  The nice thing about this is that you can use it to help build your rules to assign using the gl-addaudiencerule command - you'd create all the rules you need via the browser, with no groupings, and then use this command to get the XML and then simply add your grouping elements into the XML - this way you're sure the field names are correct.

Here's a snippet of the code:

   1:  
   2: /// <summary>
   3: /// Returns an XML structure containing all the rules associated with the audience.
   4: /// </summary>
   5: /// <param name="sspName">Name of the SSP.</param>
   6: /// <param name="audienceName">Name of the audience.</param>
   7: /// <param name="includeAllAttributes">if set to <c>true</c> [include all attributes].</param>
   8: /// <returns></returns>
   9: private string EnumRules(string sspName, string audienceName, bool includeAllAttributes)
  10: {
  11:     ServerContext context;
  12:     if (string.IsNullOrEmpty(sspName))
  13:         context = ServerContext.Default;
  14:     else
  15:         context = ServerContext.GetContext(sspName);
  16:  
  17:     AudienceManager manager = new AudienceManager(context);
  18:  
  19:     if (!manager.Audiences.AudienceExist(audienceName))
  20:     {
  21:         throw new SPException("Audience name does not exist");
  22:     }
  23:  
  24:     Audience audience = manager.Audiences[audienceName];
  25:  
  26:     ArrayList audienceRules = audience.AudienceRules;
  27:  
  28:     if (audienceRules == null || audienceRules.Count == 0)
  29:         return "The audience contains no rules.";
  30:  
  31:     string rulesXml = "<rules>\r\n";
  32:     foreach (AudienceRuleComponent rule in audienceRules)
  33:     {
  34:         if (includeAllAttributes)
  35:         {
  36:             rulesXml += string.Format("\t<rule field=\"{1}\" op=\"{0}\" value=\"{2}\" />\r\n", rule.Operator, rule.LeftContent, rule.RightContent);
  37:         }
  38:         else
  39:         {
  40:             switch (rule.Operator.ToLowerInvariant())
  41:             {
  42:                 case "=":
  43:                 case ">":
  44:                 case ">=":
  45:                 case "<":
  46:                 case "<=":
  47:                 case "contains":
  48:                 case "<>":
  49:                 case "not contains":
  50:                     rulesXml += string.Format("\t<rule field=\"{1}\" op=\"{0}\" value=\"{2}\" />\r\n", rule.Operator, rule.LeftContent, rule.RightContent);
  51:                     break;
  52:                 case "reports under":
  53:                 case "member of":
  54:                     rulesXml += string.Format("\t<rule op=\"{0}\" value=\"{1}\" />\r\n", rule.Operator, rule.RightContent);
  55:                     break;
  56:                 case "and":
  57:                 case "or":
  58:                 case "(":
  59:                 case ")":
  60:                     rulesXml += string.Format("\t<rule op=\"{0}\" />\r\n", rule.Operator);
  61:                     break;
  62:             }
  63:         }
  64:     }
  65:     rulesXml += "</rules>";
  66:     return rulesXml;
  67: }

The help for the command is shown below:

C:\>stsadm -help gl-enumaudiencerules

stsadm -o gl-enumaudiencerules


Outputs the rules for an audience.

Parameters:
        -name <audience name>
        [-ssp <SSP name>]
        [-explicit (shows field and value attributes for every rule)]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-enumaudiencerules MOSS 2007 8/6/2008

Parameter Name Short Form Required Description Example Usage
name n Yes The name of the audience -name "IT Department"

-n "IT Department"
ssp   No The name of the SSP that the audience is associated with.  If not specified then the default SSP is used. -ssp SSP1
explicit ex No If specified then all attributes will be returned in the XML - this is only really useful if you wish to see the field names of the special operators, "member of" and "reports under". -explicit

-ex

The following is an example of how to display the rules associated with an audience:

stsadm -o gl-enumaudiencerules -name "IT Department"

The following is an example of the XML that would be displayed:

   1: <rules>
   2:     <rule op="(" />
   3:     <rule field="Department" op="=" value="IT" />
   4:     <rule op="or" />
   5:     <rule op="reports under" value="domain\glapointe" />
   6:     <rule op=")" />
   7:     <rule op="and" />
   8:     <rule field="IsContractor" op="=" value="false" />
   9: </rules>

7Aug/0810

Setting List Content Types using STSADM

Sometimes when I'm working on a new Feature I find it easier to take certain snippets that I need to test and pull them out into custom STSADM commands.  This enables me to quickly and easily test the core code and without having to go through all the deployment steps.  It was for this purpose that I originally created this new command, gl-setlistcontenttypes.  I did find later that the command ended up being real useful in a scripted deployment that I'm working on so I managed to knock out a well tested new command and some reusable code for a Feature that I'm working on.

The code is really simple - I've got a primary method, SetContentTypes, which takes in 3 string arrays containing the content types to remove and add as well as another array for the new button order.  There's also a couple of helper methods that are called.  I then just loop through each array and call the necessary methods (Add or Delete) on the ContentTypes property which returns an SPContentTypeCollection object.  The only tricky part is setting the order of the content types - this is done via RootFolder property.  There's a UniqueContentTypeOrder property that must be set to an IList<SPContentType> object.  Manipulating the IList object directly will not work as it does not set the dirty flag and therefore the Update() method will appear to do nothing so you must create a new object and reset the property:

   1: /// <summary>
   2: /// Sets the content types.
   3: /// </summary>
   4: /// <param name="web">The web.</param>
   5: /// <param name="list">The list.</param>
   6: /// <param name="contentTypesToAdd">The content types to add.</param>
   7: /// <param name="contentTypesToDelete">The content types to delete.</param>
   8: /// <param name="contentTypeOrder">The content type order.</param>
   9: public static void SetContentTypes(SPWeb web, SPList list, string[] contentTypesToAdd, string[] contentTypesToDelete, string[] contentTypeOrder)
  10: {
  11:     foreach (string ct in contentTypesToAdd)
  12:     {
  13:         AddContentTypeToList(web, list, ct.Trim());
  14:     }
  15:     foreach (string ct in contentTypesToDelete)
  16:     {
  17:         foreach (SPContentType contentType in list.ContentTypes)
  18:         {
  19:             if (contentType.Name.ToLowerInvariant() == ct.Trim().ToLowerInvariant())
  20:             {
  21:                 list.ContentTypes.Delete(contentType.Id);
  22:                 break;
  23:             }
  24:         }
  25:     }
  26:     ChangeContentTypeOrder(list, contentTypeOrder);
  27: }
  28:  
  29: /// <summary>
  30: /// Changes the content type order.
  31: /// </summary>
  32: /// <param name="list">The list.</param>
  33: /// <param name="contentTypes">The content types.</param>
  34: public static void ChangeContentTypeOrder(SPList list, string[] contentTypes)
  35: {
  36:     if (contentTypes.Length == 0)
  37:         return;
  38:  
  39:     IList<SPContentType> contentTypeOrder = new List<SPContentType>();
  40:     foreach (string contentTypeName in contentTypes)
  41:     {
  42:         SPContentType contentType = null;
  43:         try
  44:         {
  45:             contentType = list.ContentTypes[contentTypeName.Trim()];
  46:         }
  47:         catch (ArgumentException)
  48:         {
  49:             Log("WARNING: Unable to set content type order for '{0}'.  Content type was not found.",
  50:                 contentTypeName);
  51:         }
  52:         if (contentType != null)
  53:             contentTypeOrder.Add(contentType);
  54:         else
  55:             Log("WARNING: Unable to set content type order for '{0}'.  Content type was not found.",
  56:                 contentTypeName);
  57:     }
  58:     list.RootFolder.UniqueContentTypeOrder = contentTypeOrder;
  59:     list.RootFolder.Update();
  60:  
  61: }
  62:  
  63:  
  64: /// <summary>
  65: /// Adds the content type to list.
  66: /// </summary>
  67: /// <param name="web">The web.</param>
  68: /// <param name="list">The list.</param>
  69: /// <param name="contentTypeName">Name of the content type.</param>
  70: /// <returns></returns>
  71: public static SPContentType AddContentTypeToList(SPWeb web, SPList list, string contentTypeName)
  72: {
  73:     SPContentType contentType = null;
  74:     try
  75:     {
  76:         contentType = list.ContentTypes[contentTypeName];
  77:     }
  78:     catch (ArgumentException)
  79:     { }
  80:     if (contentType == null)
  81:     {
  82:         try
  83:         {
  84:             // Get the content type from the web and add to the list.
  85:             contentType = web.ContentTypes[contentTypeName];
  86:         }
  87:         catch (ArgumentException)
  88:         {
  89:         }
  90:         if (contentType == null)
  91:             throw new SPException(string.Format("Unable to find content type '{0}'", contentTypeName));
  92:  
  93:         return list.ContentTypes.Add(contentType);
  94:     }
  95:     return null;
  96: }

The help for the command is shown below:

C:\>stsadm -help gl-setlistcontenttypes

stsadm -o gl-setlistcontenttypes


Adds or removes content typews associated with the given list.

Parameters:
        -url <list view url>
        [-add <comma separated list of content type names to add to the list>]
        [-remove <comma separated list of content type names to remove from the list>]
        [-order <new button order, comma separated, first will be the default>]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-setlistcontenttypes WSS v3, MOSS 2007 8/6/2008

Parameter Name Short Form Required Description Example Usage
url   Yes The URL to the list of which the actions will be performed -url http://portal/Lists/Events
add a No A comma separated list of content type names to add to the list.  It's recommended to wrap the value in quotes if more than one. -add "Holiday,Vacation,Company Event"

-a "Holiday,Vacation,Company Event"
remove r No A comma separated list of content type names to remove from the list.  It's recommended to wrap the value in quotes if more than one. -remove Event

-r Event
order   No A comma separated list of content type names.  Sets the order of the items in the "New" button of the list.  The first item will be the default content type. -order "Vacation,Holiday,Company Event"

The following is an example of how to set the content types for a list:

stsadm -o gl-setlistcontenttypes -url http://portal/Lists/Events -add "Holiday,Vacation,Company Events" -remove "Event" -order "Vacation,Holiday,Company Event"

7Aug/081

Assigning Rules to Audiences via STSADM

If you read my last post, Creating Audiences via STSADM, then you know that I've been working on a project which requires me to be able to script out the creation of audiences via STSADM.  My last post covered the creation of the audience itself, but an audience with no rules isn't all that useful, so for this post I'll be covering my next custom command, gl-addaudiencerule, which enables you to add complex rules to an audience.

I'll reiterate a couple of things regarding creating rules from my last post.  First off, when you create rules via the browser you are limited to just simple rules - in other words, you may have multiple rules but the boolean logic is limited to all rules matching or any rules matching - there is no combination or complex boolean logic with grouping.  This is not the case if you create the rules programmatically - by programmatically creating the rules we can use grouping (up to 3 levels deep) and any combination of boolean logic.  The catch is that as soon as you add any complex rules to an audience you will now no longer be able to manage that audience via the browser - you'll still be able to compile the audience and view memberships but you won't be able to manage or even view any rules associated with the audience and you won't be able to delete the audience.  I created two more commands that allow you to see the rules and delete the audience via STSADM but I'll talk about them in follow-up posts.

Microsoft took an interesting approach to storing the rules - they basically use an ArrayList of objects of type AudienceRuleComponent.  Each object represents a part of the rule, including the the parentheses and logic operators (AND, OR).  So a rule like the following would consist of 7 objects:

(Department == "IT" OR Reports Under == "domain\glapointe") AND IsContractor == false

The above would be broken down into objects in the following fashion:

  1. new AudienceRuleComponent(null, "(", null);
  2. new AudienceRuleComponent("Department", "=", "IT");
  3. new AudienceRuleComponent(null, "OR", null);
  4. new AudienceRuleComponent("Everyone", "Reports Under", "domain\glapointe");
  5. new AudienceRuleComponent(null, ")", null);
  6. new AudienceRuleComponent(null, "AND", null);
  7. new AudienceRuleComponent("IsContractor", "=", "false");

The objects created above would be added to the rules collection array list in the order listed.  One thing you may have noticed above is that the field for the "reports under" operation is "Everyone" - the field for the "member of" operation is actually "DL".  It's important to note that if you change the rules you must reassign the AudienceRules property rather than manipulate the items via the property:

  • audience.AudienceRules.Add(new AudienceRuleComponent(null, "(", null)); // This will not work as the property will not be marked as dirty and will therefore not be saved when Commit is called.
  • ArrayList rules = audience.AudienceRules;
    rules..Add(new AudienceRuleComponent(null, "(", null));
    audience.AudienceRules = rules; // This assignment marks the audience rules as dirty and will thus be saved.

The way I decided to handle the creation of these rules was to allow a simple XML structure to be passed into the command either directly via a parameter or indirectly by passing in a file containing the rules.  The structure of the XML is similar to the structure of the above code - you create one or more <rule /> elements which are wrapped in a <rules /> element.  The <rule /> element contains one required attribute, "op", and two optional (depending on the operation) attributes, "field" and "value".  Grouping operations do not require the field and value attributes and member of and reports under operations do not require the field attribute.  Here's an example of the above:

   1: <rules>
   2:     <rule op="(" />
   3:     <rule field="Department" op="=" value="IT" />
   4:     <rule op="or" />
   5:     <rule op="reports under" value="domain\glapointe" />
   6:     <rule op=")" />
   7:     <rule op="and" />
   8:     <rule field="IsContractor" op="=" value="false" />
   9: </rules>
You could easily pass this same XML structure in as a parameter by removing the line breaks and replacing the quotes with tick marks.  By using this simple XML approach I was able to write the code very quickly.  The only part that tripped me up was the "member of" operation.  The value of this operation must be a fully distinguished AD name and not the login name as depicted - but I wanted to be able to use just the login name.  What I found was that the API provides a handy little method for converting the login name (you can also pass in an email address) to a distinguished name, as seen in this snippet:
   1: case "member of":
   2:     field = "DL";
   3:     val = rule.GetAttribute("value");
   4:     ArrayList path = AudienceManager.GetADsPath(val);
   5:     if (path.Count == 0)
   6:         throw new ArgumentException(string.Format("Security group or distribution list was not found: {0}", val));
   7:     val = ((string)path[0]).Replace("LDAP://", "");
   8:     break;
The complete code for the command can be seen below:
   1: #if MOSS
   2: using System;
   3: using System.Collections;
   4: using System.Collections.Specialized;
   5: using System.IO;
   6: using System.Text;
   7: using System.Xml;
   8: using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
   9: using Lapointe.SharePoint.STSADM.Commands.SPValidators;
  10: using Microsoft.Office.Server;
  11: using Microsoft.Office.Server.Audience;
  12: using Microsoft.Office.Server.Search.Administration;
  13: using Microsoft.SharePoint;
  14:  
  15: namespace Lapointe.SharePoint.STSADM.Commands.Audiences
  16: {
  17:     public class AddAudienceRule : SPOperation
  18:     {
  19:         public enum AppendOp
  20:         {
  21:             AND, OR
  22:         }
  23:  
  24:         /// <summary>
  25:         /// Initializes a new instance of the <see cref="AddAudienceRule"/> class.
  26:         /// </summary>
  27:         public AddAudienceRule()
  28:         {
  29:             SPEnumValidator appendOpValidator = new SPEnumValidator(typeof(AppendOp));
  30:  
  31:             SPParamCollection parameters = new SPParamCollection();
  32:             parameters.Add(new SPParam("name", "n", true, null, new SPNonEmptyValidator()));
  33:             parameters.Add(new SPParam("ssp", "ssp", false, null, new SPNonEmptyValidator()));
  34:             parameters.Add(new SPParam("rules", "r", false, null, new SPNonEmptyValidator()));
  35:             parameters.Add(new SPParam("rulesfile", "rf", false, null, new SPFileExistsValidator()));
  36:             parameters.Add(new SPParam("clear", "cl"));
  37:             parameters.Add(new SPParam("compile", "co"));
  38:             parameters.Add(new SPParam("groupexisting", "group"));
  39:             parameters.Add(new SPParam("appendop", "op", false, "and", appendOpValidator));
  40:  
  41:             StringBuilder sb = new StringBuilder();
  42:             sb.Append("\r\n\r\nAdds simple or complex rules to an existing audience.  The rules XML should be in the following format: ");
  43:             sb.Append("<rules><rule op='' field='' value='' /></rules>\r\n");
  44:             sb.Append("Values for the \"op\" attribute can be any of \"=,>,>=,<,<=,<>,Contains,Not contains,Reports Under,Member Of,AND,OR,(,)\"\r\n");
  45:             sb.Append("The \"field\" attribute is not required if \"op\" is any of \"Reports Under,Member Of,AND,OR,(,)\"\r\n");
  46:             sb.Append("The \"value\" attribute is not required if \"op\" is any of \"AND,OR,(,)\"\r\n");
  47:             sb.Append("Note that if your rules contain any grouping or mixed logic then you will not be able to manage the rule via the browser.\r\n");
  48:             sb.Append("Example: <rules><rule op='Member of' value='sales department' /><rule op='AND' /><rule op='Contains' field='Department' value='Sales' /></rules>");
  49:             sb.Append("\r\n\r\nParameters:");
  50:             sb.Append("\r\n\t-name <audience name>");
  51:             sb.Append("\r\n\t-rules <rules xml> | -rulesfile <xml file containing the rules>");
  52:             sb.Append("\r\n\t[-ssp <SSP name>]");
  53:             sb.Append("\r\n\t[-clear (clear existing rules)]");
  54:             sb.Append("\r\n\t[-compile]");
  55:             sb.Append("\r\n\t[-groupexisting (wraps any existing rules in parantheses)]");
  56:             sb.Append("\r\n\t[-appendop <and (default) | or> (operator used to append to existing rules)]");
  57:             Init(parameters, sb.ToString());
  58:         }
  59:  
  60:         /// <summary>
  61:         /// Gets the help message.
  62:         /// </summary>
  63:         /// <param name="command">The command.</param>
  64:         /// <returns></returns>
  65:         public override string GetHelpMessage(string command)
  66:         {
  67:             return HelpMessage;
  68:         }
  69:  
  70:         /// <summary>
  71:         /// Executes the specified command.
  72:         /// </summary>
  73:         /// <param name="command">The command.</param>
  74:         /// <param name="keyValues">The key values.</param>
  75:         /// <param name="output">The output.</param>
  76:         /// <returns></returns>
  77:         public override int Execute(string command, StringDictionary keyValues, out string output)
  78:         {
  79:             output = string.Empty;
  80:  
  81:             string rules;
  82:             if (Params["rules"].UserTypedIn)
  83:                 rules = Params["rules"].Value;
  84:             else
  85:                 rules = File.ReadAllText(Params["rulesfile"].Value);
  86:  
  87:             AddRules(Params["ssp"].Value,
  88:                      Params["name"].Value,
  89:                      rules,
  90:                      Params["clear"].UserTypedIn,
  91:                      Params["compile"].UserTypedIn,
  92:                      Params["groupexisting"].UserTypedIn,
  93:                      (AppendOp)Enum.Parse(typeof(AppendOp), Params["appendop"].Value, true));
  94:  
  95:             return OUTPUT_SUCCESS;
  96:         }
  97:  
  98:         /// <summary>
  99:         /// Validates the specified key values.
 100:         /// </summary>
 101:         /// <param name="keyValues">The key values.</param>
 102:         public override void Validate(StringDictionary keyValues)
 103:         {
 104:             SPBinaryParameterValidator.Validate("rules", Params["rules"].Value, "rulesfile", Params["rulesfile"].Value);
 105:             
 106:             if (Params["clear"].UserTypedIn && (Params["appendop"].UserTypedIn || Params["groupexisting"].UserTypedIn))
 107:                 throw new SPSyntaxException("The -clear parameter cannot be used with the -appendop or -groupexisting parameters.");
 108:  
 109:             base.Validate(keyValues);
 110:         }
 111:  
 112:         /// <summary>
 113:         /// Adds the rules.
 114:         /// </summary>
 115:         /// <param name="sspName">Name of the SSP.</param>
 116:         /// <param name="audienceName">Name of the audience.</param>
 117:         /// <param name="rules">The rules.</param>
 118:         /// <param name="clearExistingRules">if set to <c>true</c> [clear existing rules].</param>
 119:         /// <param name="compile">if set to <c>true</c> [compile].</param>
 120:         /// <param name="groupExisting">if set to <c>true</c> [group existing].</param>
 121:         /// <param name="appendOp">The append op.</param>
 122:         public static void AddRules(string sspName, string audienceName, string rules, bool clearExistingRules, bool compile, bool groupExisting, AppendOp appendOp)
 123:         {
 124:             ServerContext context;
 125:             if (string.IsNullOrEmpty(sspName))
 126:                 context = ServerContext.Default;
 127:             else
 128:                 context = ServerContext.GetContext(sspName);
 129:  
 130:             AudienceManager manager = new AudienceManager(context);
 131:  
 132:             if (!manager.Audiences.AudienceExist(audienceName))
 133:             {
 134:                 throw new SPException("Audience name does not exist");
 135:             }
 136:  
 137:             Audience audience = manager.Audiences[audienceName];
 138:             /*
 139:             Operator        Need left and right operands (not a group operator) 
 140:             =               Yes 
 141:             >               Yes 
 142:             >=              Yes 
 143:             <               Yes 
 144:             <=              Yes 
 145:             Contains        Yes 
 146:             Reports Under   Yes (Left operand must be 'Everyone') 
 147:             <>              Yes 
 148:             Not contains    Yes 
 149:             AND             No 
 150:             OR              No 
 151:             (               No 
 152:             )               No 
 153:             Member Of       Yes (Left operand must be 'DL') 
 154:             */
 155:             XmlDocument rulesDoc = new XmlDocument();
 156:             rulesDoc.LoadXml(rules);
 157:  
 158:             ArrayList audienceRules = audience.AudienceRules;
 159:             bool ruleListNotEmpty = false;
 160:  
 161:             if (audienceRules == null || clearExistingRules)
 162:                 audienceRules = new ArrayList();
 163:             else
 164:                 ruleListNotEmpty = true;
 165:  
 166:             //if the rule is not emply, start with a group operator 'AND' to append
 167:             if (ruleListNotEmpty)
 168:             {
 169:                 if (groupExisting)
 170:                 {
 171:                     audienceRules.Insert(0, new AudienceRuleComponent(null, "(", null));
 172:                     audienceRules.Add(new AudienceRuleComponent(null, ")", null));
 173:                 }
 174:  
 175:                 audienceRules.Add(new AudienceRuleComponent(null, appendOp.ToString(), null));
 176:             }
 177:  
 178:             if (rulesDoc.SelectNodes("//rule") == null || rulesDoc.SelectNodes("//rule").Count == 0)
 179:                 throw new ArgumentException("No rules were supplied.");
 180:  
 181:             foreach (XmlElement rule in rulesDoc.SelectNodes("//rule"))
 182:             {
 183:                 string op = rule.GetAttribute("op").ToLowerInvariant();
 184:                 string field = null;
 185:                 string val = null;
 186:                 bool valIsRequired = true;
 187:                 bool fieldIsRequired = false;
 188:  
 189:                 switch (op)
 190:                 {
 191:                     case "=":
 192:                     case ">":
 193:                     case ">=":
 194:                     case "<":
 195:                     case "<=":
 196:                     case "contains":
 197:                     case "<>":
 198:                     case "not contains":
 199:                         field = rule.GetAttribute("field");
 200:                         val = rule.GetAttribute("value");
 201:                         fieldIsRequired = true;
 202:                         break;
 203:                     case "reports under":
 204:                         field = "Everyone";
 205:                         val = rule.GetAttribute("value");
 206:                         break;
 207:                     case "member of":
 208:                         field = "DL";
 209:                         val = rule.GetAttribute("value");
 210:                         ArrayList path = AudienceManager.GetADsPath(val);
 211:                         if (path.Count == 0)
 212:                             throw new ArgumentException(string.Format("Security group or distribution list was not found: {0}", val));
 213:                         val = ((string)path[0]).Replace("LDAP://", "");
 214:                         break;
 215:                     case "and":
 216:                     case "or":
 217:                     case "(":
 218:                     case ")":
 219:                         valIsRequired = false;
 220:                         break;
 221:                     default:
 222:                         throw new ArgumentException(string.Format("Rule operator is invalid: {0}", rule.GetAttribute("op")));
 223:                 }
 224:                 if (valIsRequired && string.IsNullOrEmpty(val))
 225:                     throw new ArgumentNullException(string.Format("Rule value attribute is missing or invalid: {0}", rule.GetAttribute("value")));
 226:  
 227:                 if (fieldIsRequired && string.IsNullOrEmpty(field))
 228:                     throw new ArgumentNullException(string.Format("Rule field attribute is missing or invalid: {0}", rule.GetAttribute("field")));
 229:                 
 230:                 AudienceRuleComponent r0 = new AudienceRuleComponent(field, op, val);
 231:                 audienceRules.Add(r0);
 232:             }
 233:  
 234:             audience.AudienceRules = audienceRules;
 235:             audience.Commit();
 236:             if (compile)
 237:                 CompileAudience(context, audience.AudienceName);
 238:         }
 239:  
 240:         /// <summary>
 241:         /// Compiles the audience.
 242:         /// </summary>
 243:         /// <param name="context">The context.</param>
 244:         /// <param name="audienceName">Name of the audience.</param>
 245:         public static void CompileAudience(ServerContext context, string audienceName)
 246:         {
 247:             SearchContext searchContext = SearchContext.GetContext(context);
 248:  
 249:             string[] args = new string[4];
 250:             args[0] = searchContext.Name;
 251:             args[1] = "1"; //"1" = start job, "0" = stop job 
 252:             args[2] = "1"; //"1" = full compilation, "0" = incremental compilation (optional, default = 0) 
 253:             args[3] = audienceName;
 254:  
 255:             AudienceJob.RunAudienceJob(args);
 256:         }
 257:     }
 258: }
 259: #endif

The help for the command is shown below:

C:\>stsadm -help gl-addaudiencerule

stsadm -o gl-addaudiencerule


Adds simple or complex rules to an existing audience.  The rules XML should be in the following format: <rules><rule op='' field='' value='' /></rules>
Values for the "op" attribute can be any of "=,>,>=,<,<=,<>,Contains,Not contains,Reports Under,Member Of,AND,OR,(,)"
The "field" attribute is not required if "op" is any of "Reports Under,Member Of,AND,OR,(,)"
The "value" attribute is not required if "op" is any of "AND,OR,(,)"
Note that if your rules contain any grouping or mixed logic then you will not be able to manage the rule via the browser.
Example: <rules><rule op='Member of' value='sales department' /><rule op='AND' /><rule op='Contains' field='Department'value='Sales' /></rules>

Parameters:
        -name <audience name>
        -rules <rules xml> | -rulesfile <xml file containing the rules>
        [-ssp <SSP name>]
        [-clear (clear existing rules)]
        [-compile]
        [-groupexisting (wraps any existing rules in parantheses)]
        [-appendop <and (default) | or> (operator used to append to existing rules)]

The following table summarizes the command and its various parameters:

Command Name Availability Build Date
gl-addaudiencerule MOSS 2007 8/6/2008

Parameter Name Short Form Required Description Example Usage
name n Yes This is the name of the audience for which to apply the rules. -name "IT Department"

-n "IT Department"
ssp   No The name of the SSP that the audience is associated with.  If not specified then the default SSP is used. -ssp SSP1
rules r Yes - unless rulesfile provided The XML rules that are to be created.  Use tick marks instead of quotes.  Using this parameter, as opposed to the rulesfile parameter, is convenient when using a batch script in which you'd like to pass variables into the XML. -rules "<rules><rule op='(' /><rule field='Department' op='=' value='IT' /><rule op='or' /><rule op='reports under' value='domain\glapointe' /><rule op=')' /><rule op='and' /><rule field='IsContractor' op='=' value='false' /></rules>"
rulesfile rf Yes - unless rules provided Specifies the path to an XML file containing the rules to be created.  The file extension does not matter.  Using this parameter, as opposed to the rules parameter, is convenient when you'd like to save your rules for later reference or recreation in other environments as well as easy modification. -rulesfile c:\Audiences\ITDepartment.rules
clear cl No If provided then any existing rules will be removed from the audience. -clear

-cl
compile co No If provided then the audience will be compiled after adding the rules. -compile

-co
groupexisting group No If provided then any existing rules will be grouped within parentheses. -groupexisting

-group
appendop op No Specifies how the passed in rules will be appended to any existing rules.  Valid values are "and" or "or".  The default, if omitted, is "and". -appendop or

-op or

The following is an example of how to add rules to an audience named "IT Department":

stsadm -o gl-addaudiencerule -name "IT Department" -rules "<rules><rule op='(' /><rule field='Department' op='=' value='IT' /><rule op='or' /><rule op='reports under' value='domain\glapointe' /><rule op=')' /><rule op='and' /><rule field='IsContractor' op='=' value='false' /></rules>" -clear -compile