Monitor SharePoint User Profile Changes
Today I got my copy of the May 2011 edition of SharePoint Pro magazine and was pleased to find the “Monitor SharePoint User Profile Changes” article that I co-wrote with my buddy Matthew McDermott! The cool thing for me is that this is my first ever magazine article so I’m very excited! You can find the article online at the SharePoint Pro site: http://www.sharepointpromag.com/article/sharepoint/monitor-sharepoint-user-profile-changes-129846.
Note that for some reason the online version of the article only shows my picture for the authors and you have to look closely to see Matt’s name which makes it look, at first glance, like there was just one author, me – I’m really bummed about this for if it wasn’t for Matt I wouldn’t even have gotten the opportunity to write this article – fortunately, the print version gives better props to both of us; so, Matt, if you’re reading this, thanks for helping me to achieve this long standing goal!
-Gary
Upgrading User Profile Choice Fields to SharePoint 2010
I’m working on an upgrade (database attach) for a client of mine and I ran into something somewhat unexpected with some User Profile properties – properties that used a choice field in 2007 (fields that allowed the administrator to define a list of values that the user could pick from) were migrated to Managed Term Store based fields when upgraded. At first glance this was pretty cool; the only problem was that, though the field was converted to a Managed Term Store based field (essentially a Taxonomy field), the Terms themselves were not migrated and the user-specific values were lost. Now, I may have done something wrong during the upgrade process and if someone knows what I did wrong please let me know – that said, I decided to just throw together a couple quick scripts that I could use to export the field information (along with the users’ values for those fields) and then import the information in the new Farm.
To accomplish this I created two separate scripts, one for the 2007 Farm and one for the new 2010 Farm. The 2007 script loops through all the User Profile properties and, if the property has choices defined then it creates an XML structure that defines the property name and the associated choices; I put this in a function called Get-SPChoiceProperties. It then loops through all User Profiles and, for each identified property, creates another XML structure which defines the property name and associated values along with the account name; I put this in a function called Get-SPUserProperties. I then merge these two XML structures to create a single structure which is saved to disk.
Here’s the complete code which I store in a file called Export-UserProfileProperties.ps1 (note that the function names and script name are not really important and the code is by no means perfect – I threw this whole thing together *very* quickly so that I could just get this task done and move on to the next task):
[System.Reflection.Assembly]::LoadWithPartialName("Microsoft.SharePoint") | Out-Null [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server") | Out-Null [System.Reflection.Assembly]::LoadWithPartialName("Microsoft.Office.Server.UserProfiles") | Out-Null function Get-SPChoiceProperties() { $context = [Microsoft.Office.Server.ServerContext]::Default $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileConfigManager $context $props = $upm.GetProperties() [xml]$xml = "<Properties></Properties>" foreach ($prop in $props) { if ($prop.ChoiceList -eq $null) { continue } $propXml = $xml.CreateElement("Property") $xml.DocumentElement.AppendChild($propXml) | out-null $propXml.SetAttribute("Name", $prop.Name) $choicesXml = $xml.CreateElement("ChoiceList") $propXml.AppendChild($choicesXml) | out-null foreach ($choice in $Prop.ChoiceList.GetAllTerms($true)) { $choiceXml = $xml.CreateElement("Choice") $choicesXml.AppendChild($choiceXml) | out-null $choiceXml.InnerText = $choice } } $xml } function Get-SPUserProperties([string[]]$propertyNames) { $context = [Microsoft.Office.Server.ServerContext]::Default $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileManager $context [xml]$xml = "<Users></Users>" foreach ($profile in $upm) { $userXml = $xml.CreateElement("User") $xml.DocumentElement.AppendChild($userXml) | out-null $userXml.SetAttribute("Account", $profile["AccountName"].Value) $propertyNames | % { $val = ($profile[$_] -as [string[]]) -join ";" $userXml.SetAttribute($_, $val) } } $xml } $propertyXml = Get-SPChoiceProperties [string[]]$properties = $propertyXml.Properties.Property | % {$_.Name} $userXml = Get-SPUserProperties $properties [xml]$xml = "<ProfileData>$($propertyXml.OuterXml)$($userXml.OuterXml)</ProfileData>" $xml.Save("c:\userdata.xml")
The next thing I needed to do was to create the script for the 2010 Farm. This script had three steps for which I created a separate function for each step:
- Create the Term Store Group if not present and, for each User Profile field identified in the exported XML create the Term Set and associated Terms. I store the field name and associated Term Set in a hash table that I can then use for the next steps. The function I created for this is called Create-SPProfileTermSets.
- For each User Profile property and associated Term Set returned by step 1, update the property with the new Term Set. The function I created for this is called Set-SPUserProfileProperties.
- Loop through all the users defined in the exported XML and, for each user, update the User Profile property with the appropriate Term value. The function I created for this is called Set-SPUserProfileValues.
I then load the exported XML and call these three functions in order, passing in the appropriate information as required. Here’s the completed code (again, it’s rough but it works – well, it met my needs):
function Create-SPProfileTermSets($session, [string]$groupName, [xml]$data) { $group = $session.DefaultSiteCollectionTermStore.Groups[$groupName] if ($group -eq $null) { $group = $ts.DefaultSiteCollectionTermStore.CreateGroup($groupName) $group.TermStore.CommitAll() } $termSets = @{} $data.ProfileData.Properties.Property | % { $name = $_.Name $termSet = $group.TermSets[$name] if ($termSet -eq $null) { $termSet = $group.CreateTermSet($name) } $termSets += @{$name=$termSet} $_.ChoiceList.Choice | % { $termValue = $_.Replace("&", "&") $term = $termSet.Terms[$termValue] if ($term -eq $null) { Write-Host "Adding $termValue" $termSet.CreateTerm($termValue, 1033) | Out-Null } } $group.TermStore.CommitAll() } $termSets } function Set-SPUserProfileProperties($termSets) { $sa = Get-SPServiceApplication | ?{$_.TypeName -eq "User Profile Service Application"} $context = [Microsoft.SharePoint.SPServiceContext]::GetContext($sa.ServiceApplicationProxyGroup, [Microsoft.SharePoint.SPSiteSubscriptionIdentifier]::Default) $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileConfigManager $context $properties = $upm.ProfilePropertyManager.GetCoreProperties() foreach ($key in $termSets.Keys) { $prop = $properties.GetPropertyByName($key) $prop.TermSet = $termSets[$key] $prop.Commit() } } function Set-SPUserProfileValues($termSets, [xml]$data) { $sa = Get-SPServiceApplication | ?{$_.TypeName -eq "User Profile Service Application"} $context = [Microsoft.SharePoint.SPServiceContext]::GetContext($sa.ServiceApplicationProxyGroup, [Microsoft.SharePoint.SPSiteSubscriptionIdentifier]::Default) $upm = New-Object Microsoft.Office.Server.UserProfiles.UserProfileManager $context $data.ProfileData.Users.User | % { if ($upm.UserExists($_.Account)) { $up = $upm.GetUserProfile($_.Account) Write-Host "Evaluating $($_.Account)..." foreach ($key in $termSets.Keys) { Write-Host " Setting $key..." $prop = $up[$key] $prop.Clear() [string[]]$term = $_.GetAttribute($key).Split(';') $term | % { if (![string]::IsNullOrEmpty($_)) { $prop.Add($termSets[$key].Terms[$_].Name) } } } $up.Commit() } } } $ts = New-Object Microsoft.SharePoint.Taxonomy.TaxonomySession (Get-SPSite "http://<ENTER YOUR SITE URL HERE>"),$true [xml]$xml = Get-Content C:\userdata.xml $termSets = Create-SPProfileTermSets $ts "Profile Properties" $xml Set-SPUserProfileProperties $termSets Set-SPUserProfileValues $termSets $xml
One note about the Taxonomy Session – notice that I don’t use the Get-SPTaxonomySession cmdlet – this is because I can’t stand how this cmdlet works; when you create a TaxonomySession object using the API you have the ability to force a reload from the database, thereby ignoring locally cached data, but when you use the cmdlet you don’t have this option. This can cause all kinds of issues when you are running scripts/functions like this repeatedly as what is cached may not reflect what is current and this will inevitably cause you headaches. (If you decide to use this code don’t forget to set the URL for the Taxonomy Session).
So that was my adventure for the day – as I said in the beginning, if there’s a way to do the upgrade that negates the need to do any of this please let me know; otherwise, perhaps others will benefit from today’s upgrade headaches
.
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:

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.
Profile Import Timer Job
I was recently trying to debug some issues that I was having with the people picker that is shown when creating audiences and I found that I needed a way to manually trigger the distribution list import quickly but I didn't always want to have to wait for the user profile import to finish. If you run an import using the SSP admin site you might notice that it imports all the user profile information and then it imports the distribution list information but there's no way (at least that I could find) to do just the distribution list. So I created a new command which I called gl-runprofileimportjob which allows me to run the profile import.
Fortunately it turned out that the code to do this is really simple - you just need an instance of the UserProfileConfigManager and you call either StartImport() or StartDLImport(). To keep the command from exiting before the import is complete I check the IsImportInProgress method and loop until it is set to false:
1: public class RunProfileImportJob : SPOperation
2: {
3: /// <summary>
4: /// Initializes a new instance of the <see cref="RunProfileImportJob"/> class.
5: /// </summary>
6: public RunProfileImportJob()
7: {
8: SPParamCollection parameters = new SPParamCollection();
9: parameters.Add(new SPParam("sspname", "ssp", false, null, new SPNonEmptyValidator()));
10: parameters.Add(new SPParam("distributionlistonly", "dl"));
11: parameters.Add(new SPParam("incremental", "inc"));
12:
13: StringBuilder sb = new StringBuilder();
14: sb.Append("\r\n\r\nExecutes a Profile Import.\r\n\r\nParameters:");
15: sb.Append("\r\n\t-sspname <SSP Name>");
16: sb.Append("\r\n\t[-distributionlistonly]");
17: sb.Append("\r\n\t[-incremental]");
18:
19: Init(parameters, sb.ToString());
20: }
21:
22: /// <summary>
23: /// Gets the help message.
24: /// </summary>
25: /// <param name="command">The command.</param>
26: /// <returns></returns>
27: public override string GetHelpMessage(string command)
28: {
29: return HelpMessage;
30: }
31:
32: /// <summary>
33: /// Executes the specified command.
34: /// </summary>
35: /// <param name="command">The command.</param>
36: /// <param name="keyValues">The key values.</param>
37: /// <param name="output">The output.</param>
38: /// <returns></returns>
39: public override int Execute(string command, System.Collections.Specialized.StringDictionary keyValues, out string output)
40: {
41: output = string.Empty;
42:
43: string sspName = Params["sspname"].Value;
44: bool dlOnly = Params["distributionlistonly"].UserTypedIn;
45: bool incremental = Params["incremental"].UserTypedIn;
46:
47: UserProfileConfigManager manager = new UserProfileConfigManager(ServerContext.GetContext(sspName));
48: if (!manager.IsImportInProgress())
49: {
50: if (dlOnly)
51: manager.StartDLImport();
52: else
53: manager.StartImport(incremental);
54:
55: Console.Write("Executing import...");
56: while (manager.IsImportInProgress())
57: {
58: Thread.Sleep(500);
59: Console.Write(".");
60: }
61: Console.WriteLine();
62: Console.WriteLine();
63: }
64: else
65: {
66: Console.WriteLine("Import is already running.");
67: }
68:
69: return OUTPUT_SUCCESS;
70: }
71:
72: }
The syntax of the command can be seen below:
C:\>stsadm -help gl-runprofileimportjob
stsadm -o gl-runprofileimportjob
Executes a Profile Import.
Parameters:
-sspname <SSP Name>
[-distributionlistonly]
[-incremental]
|
Here's an example of how to run the command to import just the distribution lists:
stsadm -o gl-runprofileimportjob -sspname SSP1 -distributionlistonly
Note that the "-incremental" flag is only relevant when the "-distributionlistonly" flag is not provided.
User Profile Properties
After running our upgrade and inspecting the user profile properties on the SSP that we created it was quickly discovered that some properties needed to be modified. Some of the changes were as simple as determining whether or not the property should be visible on the profile page or whether the user should be allowed to edit the property and some of them needed to be mapped to the corresponding field in Active Directory.
As part of my upgrade script I needed to make sure that these fields were set appropriately. You can set these fields via the browser by going to the SSP administration site and selecting "User profiles and properties" under the "User Profiles and My Sites" section. From there click "View profile properties" to see and edit the various properties.
In order to make these changes in batch during the upgrade I created two commands - the first was just to help me debug and is useful if you wish to get a dump of all the existing properties: gl-enumprofileproperties. The second command actually handles the editing of the properties: gl-editprofileproperty. The code for this is similar in nature to that of the gl-enumprofileprivacypolicies and gl-setprofileprivacypolicy commands - in fact I started with the code from those commands as a template and modified as needed. The one simplification was that I now only had one place to look for objects of interest - UserProfileConfigManager. The commands I created are detailed below.
1. gl-enumprofileproperties
This command works exactly like the gl-enumprofileprivacypolicies command but removes the privacy policy objects and adds additional information specific to the Property object:
1: public override int Run(string command, StringDictionary keyValues, out string output)
2: {
3: output = string.Empty;
4:
5: InitParameters(keyValues);
6:
7: ServerContext serverContext = ServerContext.GetContext(Params["sspname"].Value);
8: UserProfileConfigManager manager = new UserProfileConfigManager(serverContext);
9: PropertyCollection properties = manager.GetProperties();
10:
11: StringBuilder sb = new StringBuilder();
12:
13: XmlTextWriter xmlWriter = new XmlTextWriter(new StringWriter(sb));
14: xmlWriter.Formatting = Formatting.Indented;
15:
16: xmlWriter.WriteStartElement("Properties");
17: xmlWriter.WriteAttributeString("ssp", Params["sspname"].Value);
18:
19: foreach (Property item in properties)
20: {
21: AddXml(xmlWriter, item, manager);
22: }
23:
24: xmlWriter.WriteEndElement();
25: xmlWriter.Flush();
26:
27: output += sb.ToString();
28:
29: if (Params["outputfile"].UserTypedIn)
30: {
31: File.WriteAllText(Params["outputfile"].Value, output);
32: output = string.Empty;
33: }
34:
35: return 1;
36: }
37:
38: private static void AddXml(XmlTextWriter xmlWriter, Property item, UserProfileConfigManager manager)
39: {
40: xmlWriter.WriteStartElement("Property");
41:
42: xmlWriter.WriteElementString("Name", item.Name);
43: xmlWriter.WriteElementString("AllowPolicyOverride", item.AllowPolicyOverride.ToString());
44: xmlWriter.WriteStartElement("ChoiceList");
45: if (item.ChoiceList != null)
46: {
47: foreach (string s in item.ChoiceList.GetAllTerms(true))
48: {
49: xmlWriter.WriteElementString("Choice", s);
50: }
51: }
52: xmlWriter.WriteEndElement();
53: xmlWriter.WriteElementString("ChoiceType", item.ChoiceType.ToString());
54: xmlWriter.WriteElementString("DefaultPrivacy", item.DefaultPrivacy.ToString());
55: xmlWriter.WriteElementString("Description", item.Description);
56: xmlWriter.WriteElementString("DisplayName", item.DisplayName);
57: xmlWriter.WriteElementString("DisplayOrder", item.DisplayOrder.ToString());
58: xmlWriter.WriteElementString("IsAdminEditable", item.IsAdminEditable.ToString());
59: xmlWriter.WriteElementString("IsAlias", item.IsAlias.ToString());
60: xmlWriter.WriteElementString("IsColleagueEventLog", item.IsColleagueEventLog.ToString());
61: xmlWriter.WriteElementString("IsImported", item.IsImported.ToString());
62: xmlWriter.WriteElementString("IsMultivalued", item.IsMultivalued.ToString());
63: xmlWriter.WriteElementString("IsReplicable", item.IsReplicable.ToString());
64: xmlWriter.WriteElementString("IsRequired", item.IsRequired.ToString());
65: xmlWriter.WriteElementString("IsSearchable", item.IsSearchable.ToString());
66: xmlWriter.WriteElementString("IsSection", item.IsSection.ToString());
67: xmlWriter.WriteElementString("IsSystem", item.IsSystem.ToString());
68: xmlWriter.WriteElementString("IsUpgrade", item.IsUpgrade.ToString());
69: xmlWriter.WriteElementString("IsUpgradePrivate", item.IsUpgradePrivate.ToString());
70: xmlWriter.WriteElementString("IsUserEditable", item.IsUserEditable.ToString());
71: xmlWriter.WriteElementString("IsVisibleOnEditor", item.IsVisibleOnEditor.ToString());
72: xmlWriter.WriteElementString("IsVisibleOnViewer", item.IsVisibleOnViewer.ToString());
73: xmlWriter.WriteElementString("Length", item.Length.ToString());
74: xmlWriter.WriteElementString("ManagedPropertyName", item.ManagedPropertyName);
75: xmlWriter.WriteElementString("MaximumShown", item.MaximumShown.ToString());
76: xmlWriter.WriteElementString("PrivacyPolicy", item.PrivacyPolicy.ToString());
77: xmlWriter.WriteElementString("Separator", item.Separator.ToString());
78: xmlWriter.WriteElementString("Type", item.Type);
79: xmlWriter.WriteElementString("URI", item.URI);
80: xmlWriter.WriteElementString("UserOverridePrivacy", item.UserOverridePrivacy.ToString());
81:
82: xmlWriter.WriteStartElement("ImportMapping");
83: PropertyMapCollection propertyMapping = manager.GetDataSource().PropertyMapping;
84: PropertyMap map = propertyMapping[item.Name];
85:
86: if (map != null)
87: {
88: xmlWriter.WriteElementString("DSPropName", map.DSPropName);
89: xmlWriter.WriteElementString("ConnectionName", map.ConnectionName);
90: xmlWriter.WriteElementString("AssociationName", map.AssociationName);
91: }
92: xmlWriter.WriteEndElement();
93:
94: xmlWriter.WriteEndElement();
95: }
The syntax of the command can be seen below:
C:\>stsadm -help gl-enumprofileproperties
stsadm -o gl-enumprofileproperties
Lists the user profile properties associated with the SSP.
Parameters:
-sspname <name of the SSP>
[-outputfile <file to output results to>]
Here's an example of how to output all the profile information to a file:
stsadm –o gl-enumprofileproperties -sspname "SSP1" -outputfile "c:\properties.xml"
Running the above command will produce results similar to the following (I've abbreviated the output for the sake of space):
<Properties ssp="SSP1">
...
<Property>
<Name>CollegeMajor</Name>
<AllowPolicyOverride>True</AllowPolicyOverride>
<ChoiceList />
<ChoiceType>Off</ChoiceType>
<DefaultPrivacy>Public</DefaultPrivacy>
<Description />
<DisplayName>College Major</DisplayName>
<DisplayOrder>5202</DisplayOrder>
<IsAdminEditable>True</IsAdminEditable>
<IsAlias>False</IsAlias>
<IsColleagueEventLog>False</IsColleagueEventLog>
<IsImported>False</IsImported>
<IsMultivalued>False</IsMultivalued>
<IsReplicable>False</IsReplicable>
<IsRequired>False</IsRequired>
<IsSearchable>True</IsSearchable>
<IsSection>False</IsSection>
<IsSystem>False</IsSystem>
<IsUpgrade>False</IsUpgrade>
<IsUpgradePrivate>False</IsUpgradePrivate>
<IsUserEditable>True</IsUserEditable>
<IsVisibleOnEditor>True</IsVisibleOnEditor>
<IsVisibleOnViewer>False</IsVisibleOnViewer>
<Length>255</Length>
<ManagedPropertyName>CollegeMajor</ManagedPropertyName>
<MaximumShown>10</MaximumShown>
<PrivacyPolicy>OptIn</PrivacyPolicy>
<Separator>Comma</Separator>
<Type>string</Type>
<URI>urn:schemas-microsoft-com:sharepoint:portal:profile:CollegeMajor</URI>
<UserOverridePrivacy>False</UserOverridePrivacy>
<ImportMapping />
</Property>
...
</Properties>
2. gl-editprofileproperty
This command actually gave me a bit of trouble because I couldn't get large parts of the internal Microsoft code disassembled to see what exactly they were doing (Reflector seems to choke on large chunks of the SharePoint API). The piece that gave me the most trouble is the mapping of a property to a data sources fields. If you have only one connection (Active Directory specifically is what I tested against) then the code and command is really simple. What I'm uncomfortable with is the use of the connectionname and associationname parameters which I use to build the property map based on what I could gleam from the IL code. I do feel like there's something I'm missing with the usage of these properties (PropertyMap.ConnectionName and PropertyMap.AssociationName) - if anyone finds any issues with their use please forward on to me - my test environment only included Active Directory as a source (which was the master source) so I wasn't able to test the usage of these properties.
1: public override int Run(string command, StringDictionary keyValues, out string output)
2: {
3: output = string.Empty;
4:
5: InitParameters(keyValues);
6:
7: string name = Params["name"].Value;
8:
9: ServerContext serverContext = ServerContext.GetContext(Params["sspname"].Value);
10: UserProfileConfigManager manager = new UserProfileConfigManager(serverContext);
11: PropertyCollection properties = manager.GetProperties();
12: Property property = properties.GetPropertyByName(name);
13:
14: if (property == null)
15: {
16: // We couldn't find using the system name so try using the display name.
17: foreach (Property item in properties)
18: {
19: if (item.DisplayName.ToLowerInvariant() == name.ToLowerInvariant())
20: {
21: if (property != null)
22: throw new ArgumentException(string.Format("Duplicate properties found matching the name {0}. Use the system name instead (use enumprofileprivacypolicies to get system names).", name));
23: property = item;
24: }
25: }
26: }
27:
28: if (property == null)
29: {
30: throw new ArgumentException(string.Format("Could not find User Profile property '{0}'", name));
31: }
32:
33:
34: if (Params["description"].UserTypedIn)
35: property.Description = Params["description"].Value;
36:
37: if (Params["displayname"].UserTypedIn)
38: property.DisplayName = Params["displayname"].Value;
39:
40: if (Params["alias"].UserTypedIn)
41: property.IsAlias = bool.Parse(Params["alias"].Value);
42:
43: if (Params["showchangesincolleaguetrackerwebpart"].UserTypedIn)
44: property.IsColleagueEventLog = bool.Parse(Params["showchangesincolleaguetrackerwebpart"].Value);
45:
46: if (Params["indexed"].UserTypedIn)
47: property.IsSearchable = bool.Parse(Params["indexed"].Value);
48:
49: if (Params["isusereditable"].UserTypedIn)
50: property.IsUserEditable = bool.Parse(Params["isusereditable"].Value);
51:
52: if (Params["isvisibleoneditor"].UserTypedIn)
53: property.IsVisibleOnEditor = bool.Parse(Params["isvisibleoneditor"].Value);
54:
55: if (Params["isvisibleonviewer"].UserTypedIn)
56: property.IsVisibleOnViewer = bool.Parse(Params["isvisibleonviewer"].Value);
57:
58: if (Params["maximumshown"].UserTypedIn)
59: property.MaximumShown = int.Parse(Params["maximumshown"].Value);
60:
61: if (Params["separator"].UserTypedIn)
62: property.Separator = (MultiValueSeparator)Enum.Parse(typeof(MultiValueSeparator), Params["separator"].Value, true);
63:
64:
65: if (Params["mapping"].UserTypedIn)
66: {
67: PropertyMapCollection propertyMapping = manager.GetDataSource().PropertyMapping;
68:
69: if (!string.IsNullOrEmpty(Params["mapping"].Value))
70: {
71: PropertyMap map = propertyMapping[property.Name];
72:
73: if (map == null)
74: {
75: string connectionName = string.Empty;
76: string associationName = string.Empty;
77: if (Params["connectionname"].UserTypedIn)
78: connectionName = Params["connectionname"].Value;
79: if (Params["associationname"].UserTypedIn)
80: associationName = Params["associationname"].Value;
81:
82: propertyMapping.Add(property.Name, Params["mapping"].Value, connectionName, associationName);
83: }
84: else
85: {
86: map.DSPropName = Params["mapping"].Value;
87: if (Params["connectionname"].UserTypedIn)
88: map.ConnectionName = Params["connectionname"].Value;
89: if (Params["associationname"].UserTypedIn)
90: map.AssociationName = Params["associationname"].Value;
91: map.Commit();
92: }
93: }
94: else
95: propertyMapping.Remove(property.Name);
96: }
97:
98: property.Commit();
99:
100: return 1;
101: }
The syntax of the command can be seen below:
C:\>stsadm -help gl-editprofileproperty
stsadm -o gl-editprofileproperty
Edits a profile property.
Parameters:
-sspname <name of the SSP>
-name <name of the profile property to edit>
[-description <property description>]
[-displayname <display name>]
[-alias <true | false>]
[-showchangesincolleaguetrackerwebpart <true | false>]
[-indexed <true | false>]
[-isusereditable <true | false>]
[-isvisibleoneditor <true | false>]
[-isvisibleonviewer <true | false>]
[-separator <comma | semicolon>]
[-maximumshown <maximum number of values to show before displaying ellip
sis for multi-value property>]
[-mapping <data source field to map to (case sensitive) - leave empty to
clear mapping>]
[-connectionname <source data connection>]
[-associationname <data source associated entity>]
Here's an example of how to edit the CollegeMajor profile property identified above:
stsadm –o gl-editprofileproperty -sspname "SSP1" -name "CollegeMajor" -description "College Major" -displayname "College Major" -alias false -showchangesincolleaguetrackerwebpart false -indexed false -isusereditable true -isvisibleoneditor true -isvisibleonviewer true -mapping collegeMajor
Note that the above command assumes that "collegeMajor" exists in Active Directory (case matters).
User Profile Privacy Policies
One of the new features with SharePoint 2007 is the ability to determine who can see different profile properties when looking at a users profile page. This is a great and necessary feature as it makes sure that various privacy concerns are addressed. My company's privacy policies state that no personal information is to be displayed on our intranet - that includes things such as the birth date and home phone number. By default when we ran the SharePoint upgrade it set some of these sensitive fields to be viewable by everyone.
As part of my upgrade script I needed to make sure that these fields were set appropriately. You can set these values by going to the SSP admin pages and clicking "Profile services policies" located under the "User Profiles and My Sites" section. In order to make these changes in batch during the upgrade I created two commands - the first was just to help me debug and is useful if you wish to get a dump of all the settings: gl-enumprofileprivacypolicies. The second command actually handles the settings of policies (edit only): gl-setprofileprivacypolicy.
In writing the code to handle all this there was one piece that threw me for a loop. You actually have to look in two different places for the policies. Most of the information will be obtained by getting a UserProfileConfigManager object and getting the collection of properties - the privacy policy information is associated with these individual properties which represent the actual profile data properties. The second place to find the data is by getting a collection of PrivacyPolicyItem objects via the PrivacyPolicyManager object. My first thought was that the later should be able to provide me with all the privacy policy information - unfortunately that's not the case - the later just provides those policies which don't have a direct profile property associated with them. Both the Property object and the PrivacyPolicyItem object implement the IPrivacyPolicyItem interface which is what enables the information to be stored in multiple locations. The commands I created are detailed below.
1. gl-enumprofileprivacypolicies
This command is really quite simple. It basically has two loops, one for each collection type, and a helper method which converts the objects to XML:
1: public override int Run(string command, StringDictionary keyValues, out string output)
2: {
3: output = string.Empty;
4:
5: InitParameters(keyValues);
6:
7: ServerContext serverContext = ServerContext.GetContext(Params["sspname"].Value);
8: UserProfileConfigManager manager = new UserProfileConfigManager(serverContext);
9: PropertyCollection properties = manager.GetProperties();
10:
11: StringBuilder sb = new StringBuilder();
12:
13: XmlTextWriter xmlWriter = new XmlTextWriter(new StringWriter(sb));
14: xmlWriter.Formatting = Formatting.Indented;
15:
16: xmlWriter.WriteStartElement("PrivacyPolicies");
17: xmlWriter.WriteAttributeString("ssp", Params["sspname"].Value);
18:
19: foreach (IPrivacyPolicyItem item in properties)
20: {
21: AddXml(xmlWriter, item);
22: }
23:
24: PrivacyPolicyManager privacyPolicyManager = ProfileLoader.GetProfileLoader(serverContext).GetUserProfileManager().GetPrivacyPolicy();
25: foreach (PrivacyPolicyItem item in privacyPolicyManager.GetAllItems())
26: {
27: AddXml(xmlWriter, item);
28: }
29:
30: xmlWriter.WriteEndElement();
31: xmlWriter.Flush();
32:
33: output += sb.ToString();
34:
35: if (Params["outputfile"].UserTypedIn)
36: {
37: File.WriteAllText(Params["outputfile"].Value, output);
38: output = string.Empty;
39: }
40:
41: return 1;
42: }
43:
44: private static void AddXml(XmlTextWriter xmlWriter, IPrivacyPolicyItem item)
45: {
46: xmlWriter.WriteStartElement("Policy");
47:
48: xmlWriter.WriteAttributeString("isProperty", (item is Property).ToString());
49:
50: if (item is PrivacyPolicyItem)
51: {
52: xmlWriter.WriteElementString("Id", ((PrivacyPolicyItem)item).Id.ToString());
53: xmlWriter.WriteElementString("Group", ((PrivacyPolicyItem)item).Group);
54: xmlWriter.WriteElementString("FilterPrivacyItems", ((PrivacyPolicyItem)item).FilterPrivacyItems.ToString());
55: }
56: else if (item is Property)
57: {
58: xmlWriter.WriteElementString("Name", ((Property)item).Name);
59: xmlWriter.WriteElementString("Description", ((Property)item).Description);
60: xmlWriter.WriteElementString("DisplayOrder", ((Property)item).DisplayOrder.ToString());
61: xmlWriter.WriteElementString("IsReplicable", ((Property)item).IsReplicable.ToString());
62: xmlWriter.WriteElementString("IsSection", ((Property)item).IsSection.ToString());
63: xmlWriter.WriteElementString("URI", ((Property)item).URI);
64: }
65:
66: xmlWriter.WriteElementString("DisplayName", item.DisplayName);
67: xmlWriter.WriteElementString("AllowPolicyOverride", item.AllowPolicyOverride.ToString());
68: xmlWriter.WriteElementString("DefaultPrivacy", item.DefaultPrivacy.ToString());
69: xmlWriter.WriteElementString("PrivacyPolicy", item.PrivacyPolicy.ToString());
70: xmlWriter.WriteElementString("UserOverridePrivacy", item.UserOverridePrivacy.ToString());
71:
72: xmlWriter.WriteEndElement();
73: }
The syntax of the command can be seen below:
C:\>stsadm -help gl-enumprofileprivacypolicies
stsadm -o gl-enumprofileprivacypolicies
Lists the user profile privacy policies associated with the SSP.
Parameters:
-sspname <name of the SSP>
[-outputfile <file to output results to>]
Here's an example of how to output all the privacy policy information to a file:
stsadm –o gl-enumprofileprivacypolicies -sspname "SSP1" -outputfile "c:\privacypolicies.xml"
Running the above command will produce results similar to the following (I've abbreviated the output for the sake of space):
<PrivacyPolicies ssp="SSP1">
...
<Policy isProperty="True">
<Name>CollegeMajor</Name>
<Description />
<DisplayOrder>5202</DisplayOrder>
<IsReplicable>False</IsReplicable>
<IsSection>False</IsSection>
<URI>urn:schemas-microsoft-com:sharepoint:portal:profile:CollegeMajor</URI>
<DisplayName>College Major</DisplayName>
<AllowPolicyOverride>True</AllowPolicyOverride>
<DefaultPrivacy>Public</DefaultPrivacy>
<PrivacyPolicy>OptIn</PrivacyPolicy>
<UserOverridePrivacy>False</UserOverridePrivacy>
</Policy>
<Policy isProperty="False">
<Id>861d8fb6-7012-4cd9-a7a0-a615aed038b3</Id>
<Group>My Links</Group>
<FilterPrivacyItems>True</FilterPrivacyItems>
<DisplayName>Links on My Site</DisplayName>
<AllowPolicyOverride>True</AllowPolicyOverride>
<DefaultPrivacy>Public</DefaultPrivacy>
<PrivacyPolicy>Mandatory</PrivacyPolicy>
<UserOverridePrivacy>True</UserOverridePrivacy>
</Policy>
...
</PrivacyPolicies>
2. gl-setprofileprivacypolicy
This command's code logic is similar to that of the enum command but includes more logic to handle the different types of changes that can be made. I probably could have cleaned the code up a bit to reduce some of the redundancy but in the end I decided it made more sense to keep the handling of the two object types separate (that and time isn't something I have a lot of at the moment so I didn't see the need to spend a lot of time on something that was working).
1: public override int Run(string command, StringDictionary keyValues, out string output)
2: {
3: output = string.Empty;
4:
5: InitParameters(keyValues);
6:
7: string name = Params["name"].Value;
8:
9: ServerContext serverContext = ServerContext.GetContext(Params["sspname"].Value);
10: UserProfileConfigManager manager = new UserProfileConfigManager(serverContext);
11: PropertyCollection properties = manager.GetProperties();
12: IPrivacyPolicyItem property = properties.GetPropertyByName(name);
13:
14: if (property == null)
15: {
16: // We couldn't find using the system name so try using the display name.
17: foreach (IPrivacyPolicyItem item in properties)
18: {
19: if (item.DisplayName.ToLowerInvariant() == name.ToLowerInvariant())
20: {
21: if (property != null)
22: throw new ArgumentException(string.Format("Duplicate properties found matching the name {0}. Use the system name instead (use enumprofileprivacypolicies to get system names).", name));
23: property = item;
24: }
25: }
26: }
27:
28: if (property == null)
29: {
30: PrivacyPolicyManager privacyPolicyManager = ProfileLoader.GetProfileLoader(serverContext).GetUserProfileManager().GetPrivacyPolicy();
31:
32: foreach (PrivacyPolicyItem item in privacyPolicyManager.GetAllItems())
33: {
34: if (item.DisplayName.ToLowerInvariant() == name.ToLowerInvariant())
35: {
36: property = item;
37: break;
38: }
39: }
40: if (property == null)
41: throw new ArgumentException(
42: string.Format("Could not find a policy for the User Profile property '{0}'", name));
43:
44:
45: if (Params["privacy"].UserTypedIn)
46: property.PrivacyPolicy = (PrivacyPolicy)Enum.Parse(typeof(PrivacyPolicy), Params["privacy"].Value, true);
47:
48: if (((PrivacyPolicyItem)property).Id != PrivacyPolicyIdConstants.MyColleaguesRecommendations)
49: {
50: if (Params["defaultprivacy"].UserTypedIn)
51: property.DefaultPrivacy = (Privacy)Enum.Parse(typeof(Privacy), Params["defaultprivacy"].Value, true);
52:
53: if (Params["allowuseroverride"].UserTypedIn)
54: property.UserOverridePrivacy = bool.Parse(Params["allowuseroverride"].Value);
55: }
56:
57: if (Params["replicable"].UserTypedIn)
58: Console.WriteLine("WARNING: Property does not support replication. \"replicable\" parameter ignored.\r\n");
59:
60: property.Commit();
61: }
62: else
63: {
64: if (Params["privacy"].UserTypedIn)
65: {
66: if (property.AllowPolicyOverride)
67: property.PrivacyPolicy =(PrivacyPolicy) Enum.Parse(typeof (PrivacyPolicy), Params["privacy"].Value, true);
68: else
69: Console.WriteLine("WARNING: Property does not allow policy override. \"privacy\" parameter ignored.\r\n");
70: }
71:
72: if (Params["defaultprivacy"].UserTypedIn)
73: property.DefaultPrivacy = (Privacy)Enum.Parse(typeof(Privacy), Params["defaultprivacy"].Value, true);
74:
75: if (Params["replicable"].UserTypedIn)
76: ((Property)property).IsReplicable = bool.Parse(Params["replicable"].Value);
77:
78: if (Params["allowuseroverride"].UserTypedIn)
79: {
80: if (property.AllowPolicyOverride && !((Property)property).IsReplicable)
81: property.UserOverridePrivacy = bool.Parse(Params["allowuseroverride"].Value);
82: else
83: {
84: if (((Property)property).IsReplicable)
85: Console.WriteLine("WARNING: Property does not allow policy override because it is marked as Replicable. \"allowuseroverride\" parameter ignored.\r\n");
86: else
87: Console.WriteLine("WARNING: Property does not allow policy override. \"allowuseroverride\" parameter ignored.\r\n");
88: }
89: }
90:
91: property.Commit();
92: }
93: return 1;
94: }
The syntax of the command can be seen below:
C:\>stsadm -help gl-setprofileprivacypolicy
stsadm -o gl-setprofileprivacypolicy
Sets the privacy policy settings for a profile property.
Parameters:
-sspname <name of the SSP>
-name <name of the property to edit>
[-defaultprivacy <public | contacts | organization | manager | private | notset>]
[-privacy <mandatory | optin | optout | disabled>]
[-allowuseroverride <true | false>]
[-replicable <true | false>]
Here's an example of how to set the privacy policy of the CollegeMajor profile property identified above:
stsadm –o gl-setprofileprivacypolicy -sspname "SSP1" -name "CollegeMajor" -defaultprivacy manager -privacy optin -allowuseroverride true -replicable
My Site Settings
This command replaces the gl-setmysitesnamingformat command as it includes the same functionality along with additional capabilities.
I had originally created the gl-setmysitesnamingformat command to address a need to set the naming format of personal sites and I originally didn't think I'd have to worry about the other settings on the My Sites Settings page but turns out that I did need to set one other field. So rather than create another command for just one field I decided to create a command that would allow me to set any value on that particular page.
I was originally thinking I would exclude the naming format as I already had a command for that but then decided that I'd just include it so that this command became a one-stop-shop and the other could be deprecated (I'll leave it in but there's not much need for it now that this command exists).
Like the original command I had to use reflection to set the internal properties (I just don't understand why Microsoft chose not to make the UserProfileApplication object public). Beyond using the internal UserProfileApplication class the command also uses an internal method called CommitPersonalSiteSettings which is part of the UserProfileManager class (I had to use reflection to call this method but at least I'm not accessing the database directly).
I'm hoping that Microsoft will either make these methods and objects public or will create the ability to set these values via the existing properties. The core code is shown below (if using reflection scares you I'd suggest clicking away):
1: /// <summary>
User Profile Default Access Account
This ended up being one of those simple commands to create that took way to long to figure out how to create. Fortunately though there is an API available for setting this property (unlike most of the profile import related tasks such as the timer jobs). You can set this manually by going here: Shared Services Administration: SSPName > User Profile and Properties > Configure Profile Import. Or you can do the same thing programmatically using UserProfileConfigManager object - you need to get a DataSource object from an instance of the UserProfileConfigManager and then set the value using the SetDefaultImportAccount() method of the DataSource object:
1: public static void UpdateAccount(string sspname, string username, string password)
Set User Profile Import Schedule
Update 9/18/2007: I've modified this command so that it no longer manipulates the database directly. The content below has been updated to reflect the changes.
This particular command which I called gl-setuserprofileimportschedule, really drove me nuts. As far as I could find there is no way to set this information using any Microsoft provided public API. If you disassemble the code that is doing this you'll find lots of great classes that allow programmatic manipulation of this as well as other SSP and Profile related configurations - unfortunately those classes are all marked internal so we can't use them easily.
The two main ones are UserProfileApplication and SharedResourcesProvider. Microsoft uses these two classes for most of the more complex configuration settings. Because manipulating the database directly is not supported by Microsoft I chose to rewrite this command from it's original incarnation so that I now utilize the internal classes, methods, and properties that Microsoft is using when setting the schedule via the browser. Keep in mind that this approach is also not supported by Microsoft but it is their recommended approach over manipulating the database and is generally less frowned upon.
The syntax of the command can be seen below (note that if you were using this command prior to 9/18/2007 then the syntax has changed):
C:\>stsadm -help gl-setuserprofileimportschedule
stsadm -o gl-setuserprofileimportschedule
Sets the profile import schedule.
Parameters:
-sspname <SSP name>
-type <incremental|full>
-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-setuserprofileimportschedule | MOSS 2007 | Release: 8/9/2007
Updated: 8/14/2008 |
| Parameter Name | Short Form | Required | Description | Example Usage |
|---|---|---|---|---|
| sspname | ssp | No | The name of the SSP that the user profiles are associated with. If omitted the default SSP will be used. | -sspname SSP1
-ssp SSP1 |
| type | t | Yes | The type of schedule to set. Valid values are "incremental" and "full". | -type full
-t full |
| occurrence | oc | Yes | Specifies how frequently the import should occur. Valid values are "daily", "weekly", and "monthly". | -occurrence daily
-oc monthly |
| hour | Yes | The hour in which to run the import job. 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 import job. Valid values are between 1 and 31. | -day 1 | |
| dayofweek | No, unless occurrence is weekly | The day of the week to run the import job. Valid values are "sunday", "monday", "tuesday", "wednesday", "thursday", and "saturday". | -dayofweek saturday | |
| enabled | No | "true" to enable the import schedule, "false" to disable it. If not specified then the import schedule will be enabled. | -enabled true | |
| runjob | run | No | If specified then the import job will be immediately executed after setting the schedule. | -runjob
-run |
Here’s an example of how to set the full import schedule to every Saturday at 3:00AM:
Please note that because this command uses internal only classes, methods, and properties directly it could, according to Microsoft, put your environment into an un-supported state (though this is Microsoft's recommended approach over directly manipulating the database). Please make sure you understand what the command is doing and what your support options with Microsoft are.stsadm –o gl-setuserprofileimportschedule –sspname SSP1 -type full -occurrence weekly -hour 3 -dayofweek Saturday
Set Picture URL Property
Within our SPS 2003 environment we stored employee pictures in a picture library. We then set the users Picture URL profile property to point to their picture in this library. The problem was that as soon as we upgraded the paths to these pictures no longer worked. Our pictures were previously found under the path "http://intranet/HumanResources/EmployeePictures/" but after the upgrade the path changed to http://intranet/Topics/Divisions/HumanResources/EmployeePictures/.
So we needed a way to reset the path for all the users in the profile database to reflect this new path (note that in the end we intend to move the photos to a different location as the Topics and Divisions sites that were created will be dumped). My solution was a fairly simple command, called gl-setpictureurlnewpath, which takes in the path to the new picture library and resets all profile users PictureUrl property to reflect this new path. I assume that all the filenames are not changing and that all the photos are in this single library (if your environment has users with photos in different libraries then you'll want to modify this code to take in another variable to help you determine which ones to change). The core code that does this is shown below:
1: public int Run(string command, StringDictionary keyValues, out string output)
2: {
3: ...
4: UserProfileManager profManager = new UserProfileManager(ServerContext.GetContext(sspname));
5: foreach (UserProfile profile in profManager)
6: {
7: if (SetPicture(profile["PictureURL"], librarypath))
8: profile.Commit();
9: }
10: ...
11: }
12: private static bool SetPicture(UserProfileValueCollection val, string libraryPath)
13: {
14: string currentUrl;
15: if (val.Value == null)
16: return false;
17: else
18: currentUrl = val.Value.ToString();
19:
20: if (string.IsNullOrEmpty(currentUrl))
21: return false;
22:
23: string newUrl = libraryPath.TrimEnd('/') + "/";
24: if (currentUrl.IndexOf('/') < 0)
25: newUrl += currentUrl.Trim();
26: else
27: {
28: string file = currentUrl.Substring(currentUrl.LastIndexOf('/'), currentUrl.Length - currentUrl.LastIndexOf('/'));
29: newUrl += file.Trim('/');
30: }
31: val.Value = newUrl;
32:
33: return true;
34: }
The syntax of the command I created to do this can be seen below.
C:\>stsadm -help gl-setpictureurlnewpath
stsadm -o gl-setpictureurlnewpath
Fixes the pathing for all user photos or a single user's photo as the result of moving a picture library (assumes file names have not changed).
Parameters:
-sspname <name of the 2007 SSP>
-librarypath <path to new photo library (i.e., "http://intranet/HumanResources/Employee%20Pictures/")>
[-username <NT account name>]
Here’s an example of how to change the picture url path for all users:
stsadm –o gl-setpictureurlnewpath –sspname “SSP1” –librarypath "http://intranet/hr/EmployeePictures"
Check out the books I've contributed to at Amazon.com:
Categories
Tags
Archives
Copyright © 2013
Go to top ↑