Retrieving SharePoint 2010 Feature Activations Using Windows PowerShell
During my PowerShell for Developers presentation in London last week I promised to show and demonstrate a script for retrieving Feature activations; unfortunately I ran out of time and was not able to show this script to the degree that I’d intended so I decided to throw together this blog post.
When developing custom Features it is very common to expect that there will need to be some level of update required for those Features. Typically this means that, after deploying the Feature via a Solution Package, you will need to re-activate that Feature in order to trigger any additional code to run (or, if you are using the new SharePoint 2010 Feature upgrade capabilities you will need to run the Upgrade(Boolean) method of the SPFeature object). The problem is knowing where the Feature is activated throughout the Farm. Using PowerShell there are two ways to do this – you can use the Get-SPFeature cmdlet and test the results against the appropriate scope or you can use the various “Query” methods that have been provided for each scope. I don’t recommend that you use the Get-SPFeature cmdlet as it is very inefficient, and as such, I won’t bother showing an example of that here. Instead I’ll focus on the “Query” methods approach.
Whether your Feature is scoped to the Farm, Web Application, Site Collection, or Site, there is a method that you can call to get an SPFeature object which effectively corresponds to a Feature activation. For Farm scoped Features you use the QueryFeatures(Guid, Boolean) method of the SPWebService class, obtainable via the SPWebService class’ static AdministrationService property; for Web Application scoped Features you use the static QueryFeaturesInAllWebServices(Guid, Boolean) method of the SPWebService class; for Site Collection scoped Features you use the QueryFeatures(Guid, Boolean) method of the SPWebApplication class; and for Site scoped Features you use the QueryFeatures(Guid, Boolean) method of the SPSite class.
To create our PowerShell function we’ll simply take in a SPFeatureDefinition object and use a switch statement to call the appropriate method based on the scope of the Feature. To make the function more versatile we can use the Microsoft.SharePoint.PowerShell.SPFeatureDefinitionPipeBind type which will allow the caller to pass in either the name of the Feature, its ID, or an actual SPFeatureDefinition object; additionally, we can use parameter attributes to easily allow the value to be passed in via the object pipeline. And finally, we’ll add an additional parameter stating that we wish to retrieve only those activations that require upgrading and we’ll add some basic help for the function.
The following code listing represents the completed function – I recommend that you save this to a file named Get-SPFeatureActivations.ps1. Note that I plan on adding this as a cmdlet to my downloadable extensions thereby making the need for this script unnecessary, however, I believe that this example provides a great template to use for creating professional looking, production ready scripts that both IT administrators and developers can use.
function Get-SPFeatureActivations() { <# .Synopsis Retrieves Feature activations for the given Feature Definition. .Description Retrieves the SPFeature object for each activation of the SPFeatureDefinition object. .Example Get-SPFeatureActivations TeamCollab .Parameter Identity The Feature name, ID, or SPFeatureDefinition object whose activations will be retrieved. .Parameter NeedsUpgrade If specified, only Feature activations needing upgrading will be retrieved. .Link Get-SPFeature #> [CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)] [Alias("Feature")] [ValidateNotNullOrEmpty()] [Microsoft.SharePoint.PowerShell.SPFeatureDefinitionPipeBind]$Identity, [Parameter(Mandatory=$false, Position=1)] [switch]$NeedsUpgrade ) begin { } process { $fd = $Identity.Read() switch ($fd.Scope) { "Farm" { [Microsoft.SharePoint.Administration.SPWebService]::AdministrationService.QueryFeatures($fd.ID, $NeedsUpgrade.IsPresent) break } "WebApplication" { [Microsoft.SharePoint.Administration.SPWebService]::QueryFeaturesInAllWebServices($fd.ID, $NeedsUpgrade.IsPresent) break } "Site" { foreach ($webApp in Get-SPWebApplication) { $webApp.QueryFeatures($fd.ID, $NeedsUpgrade.IsPresent) } break } "Web" { foreach ($site in Get-SPSite -Limit All) { $site.QueryFeatures($fd.ID, $NeedsUpgrade.IsPresent) $site.Dispose() } break } } } end { } }
Assuming you’ve saved the file to the root of the C drive (not recommended but its what I do when I’m doing demos) then you can load the function into memory using dot sourcing as shown in the following example (note that the help for the function shows the help information specified by the block comment help):
Once the function is loaded into memory you can start using it. In the following example I’m returning back all the locations where the MyCustomFeature Feature is activated; I then use the Select-Object cmdlet to return just the URL for each activation:
Get-SPFeatureActivations MyCustomFeature | select @{Expression={$_.Parent.Url};Label="Url"}In this next example, instead of simply outputting the URL of each activation, I’m forcing the Feature to be reactivated using the Enable-SPFeature cmdlet (use the -Force parameter to force the Feature to be reactivated – you could also change the code to deactivate the Feature using the Disable-SPFeature cmdlet and then activate using the Enable-SPFeature cmdlet):
Get-SPFeatureActivations MyCustomFeature | ForEach-Object {
Enable-SPFeature -Identity MyCustomFeature -Url $_.Parent.Url -Force
}
Similarly you can retrieve only those Features needing upgrade and then call the Upgrade() method, as shown in this next example:
Get-SPFeatureActivations MyCustomFeature -NeedsUpgrade | ForEach-Object {$_.Upgrade($false)
}
I strongly recommend that, before you re-deploy a Feature that may be activated at an unknown number of scopes, you run this function (or something similar to it) so that you fully understand the impact of upgrading your Feature. One more thing to watch out for, if your environment is very large you may wish to modify this function so that it does not return the SPFeature object but instead just returns the URL corresponding to the activation – you can then use the Get-SPFeature cmdlet to retrieve the SPFeature object; the benefit of this is that you can immediately dispose of the parent object and prevent potential out of memory errors (I’m particularly concerned with Site Collection and Site scoped Features here where the Parent property of the SPFeature object corresponds to an SPSite or SPWeb object which must be disposed).
That’s all I’ve got for now; hopefully you’ve found this useful!
-Gary
Deploying SharePoint Files Not Handled by the WSP Solution Schema
I was working on a project recently where I had to deploy a settings file to the root of my web applications folder (where the web.config file resides). If you've ever had to do something like this before then you know that you cannot do this declaratively using the WSP's Solution schema. The Solution schema is really quite limiting as to where you can actually deploy files - as a result your only option is to create a custom Feature that runs some code when executed (because we certainly don't want to go the xcopy route).
To do this we're going to create a custom Feature which contains all the files that we need to copy and then we'll provision a one-time timer job to copy the file to the target location on each server.
Here's our Feature.xml file:
<?xml version="1.0" encoding="utf-8"?>
<Feature
Id="1960C4A0-7A47-42A8-A382-F7A91214BA39"
Title="Settings Provisioner"
Description="This Feature deploys a settings file to a the web application root."
Version="1.0.0.0"
Scope="WebApplication"
Hidden="false"
ReceiverAssembly="MyCustomFeature, Version=1.0.0.0, Culture=neutral, PublicKeyToken=39b13c54ceef5193"
ReceiverClass="MyCustomFeature.FeatureReceivers.SettingsFeatureReceiver" xmlns="http://schemas.microsoft.com/sharepoint/">
<ElementManifests>
<ElementFile Location="Files\settings.config" />
</ElementManifests>
</Feature>
As you can see we are including a "settings.config" file which is located in a folder called "Files" directly under the Feature folder. You could easily have any number of files here by simply adding additional ElementFile elements. Also note that we are linking a feature receiver to the Feature which will execute upon activation and deactivation.
Here's our feature receiver class:
1: public class SettingsFeatureReceiver : SPFeatureReceiver
2: {
3: /// <summary>
4: /// Occurs after a Feature is activated.
5: /// </summary>
6: /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
7: public override void FeatureActivated(SPFeatureReceiverProperties properties)
8: {
9: SPWebApplication webApp = properties.Feature.Parent as SPWebApplication;
10: try
11: {
12: TimerJobs.CopySettingsJob job = new TimerJobs.CopySettingsJob(webApp);
13: job.SubmitJob(true, properties.Feature.Definition.RootDirectory);
14: }
15: catch (Exception ex)
16: {
17: Logger.WriteException(ex);
18: }
19:
20:
21: }
22:
23: /// <summary>
24: /// Occurs when a Feature is deactivated.
25: /// </summary>
26: /// <param name="properties">An <see cref="T:Microsoft.SharePoint.SPFeatureReceiverProperties"></see> object that represents the properties of the event.</param>
27: public override void FeatureDeactivating(SPFeatureReceiverProperties properties)
28: {
29: try
30: {
31: TimerJobs.CopySettingsJob job = new TimerJobs.CopySettingsJob(webApp);
32: job.SubmitJob(false, properties.Feature.Definition.RootDirectory);
33: }
34: catch (Exception ex)
35: {
36: Logger.WriteException(ex);
37: }
38: }
39:
40: public override void FeatureInstalled(SPFeatureReceiverProperties properties)
41: {
42: /* no op */
43: }
44: public override void FeatureUninstalling(SPFeatureReceiverProperties properties)
45: {
46: /* no op */
47: }
48: }
Notice that within the FeatureActivated method I'm getting a reference to the SPWebApplication object and passing that to a CopySettingsJob class which is our timer job that will do all the work. On the FeatureDeactivating event you can see similar code but I'm passing in false instead of true to the Submit method. The Boolean value indicates whether we are activating or deactivating our Feature. I'm also passing in the path to the Feature folder in the 12 hive as that is where our source files are located.
Lets look at the timer job class now:
1: public class CopySettingsJob : SPJobDefinition
2: {
3: private const string KEY_ACTIVATING = "Activating";
4: private const string KEY_FEATUREFOLDER = "FeatureFolder";
5: private const string JOB_NAME = "job-settings-copy-";
6: private static readonly string jobId = Guid.NewGuid().ToString();
7:
8: public CopySettingsJob() : base() { }
9:
10: /// <summary>
11: /// Initializes a new instance of the <see cref="CopySettingsJob"/> class.
12: /// </summary>
13: /// <param name="webApp">The web app.</param>
14: public CopySettingsJob(SPWebApplication webApp)
15: : base(JOB_NAME + jobId, webApp, null, SPJobLockType.None)
16: {
17: Title = "Copy Settings Job";
18: }
19:
20: /// <summary>
21: /// Executes the job definition.
22: /// </summary>
23: /// <param name="targetInstanceId">For target types of <see cref="T:Microsoft.SharePoint.Administration.SPContentDatabase"></see> this is the database ID of the content database being processed by the running job. This value is Guid.Empty for all other target types.</param>
24: public override void Execute(Guid targetInstanceId)
25: {
26: Logger.WriteInformation(string.Format("Starting {0} timer job.", Name));
27:
28: try
29: {
30: string settingsFilePath = Path.Combine(Properties[KEY_FEATUREFOLDER].ToString(), "Files\\Settings.config");
31: string targetPath = Path.Combine(WebApplication.IisSettings[SPUrlZone.Default].Path.ToString(), "Settings.config");
32: if ((bool)Properties[KEY_ACTIVATING])
33: {
34: Logger.WriteInformation(string.Format("Copying file from \"{0}\" to \"{1}\".", settingsFilePath, targetPath));
35: File.Copy(settingsFilePath, targetPath, true);
36: }
37: else
38: {
39: Logger.WriteInformation(string.Format("Deleting file from \"{0}\"", targetPath));
40: File.Delete(targetPath);
41: }
42: }
43: catch (Exception ex)
44: {
45: Logger.WriteException(ex);
46: return;
47: }
48: Logger.WriteSuccessAudit(string.Format("Timer job {0} completed successfully", Name));
49: }
50:
51: /// <summary>
52: /// Submits the job.
53: /// </summary>
54: /// <param name="activating">if set to <c>true</c> [activating].</param>
55: public void SubmitJob(bool activating, string featureFolder)
56: {
57: Properties[KEY_ACTIVATING] = activating;
58: Properties[KEY_FEATUREFOLDER] = featureFolder;
59: Schedule = new SPOneTimeSchedule(DateTime.Now);
60: Title += " (" + jobId + ")";
61: Update();
62: }
63: }
As you can see the code simply stores the Feature folder as a property and then sets a one-time schedule. When the code runs it copies the source file to the target. Because we're using an SPJobLockType value of "None" in the constructor the code will execute on every server (set it to "Job" if you want it to run on just the server in which the Feature was actually activated).
Of course the code above isn't very generic as it hard codes the settings.config file which isn't very reusable but I wanted to keep this sample nice and simple. A better approach would be to require an either an XML file to be stored in the Feature folder and then read by the timer job or have the SubmitJob method take in parameters that describe what files to move and where to move them.
One key thing to remember is that this code will run once on each server for every web application on which the Feature has been activated. If you need a Farm scoped Feature because perhaps you are copying the noise words file for instance then you'll want to change the constructor of the timer job to take in an SPService object and change the FeatureActivated method as shown below:
1: try
2: {
3: string featurePath = properties.Feature.Definition.RootDirectory;
4:
5: SPTimerService timerService = SPFarm.Local.TimerService;
6: if (null == timerService)
7: {
8: throw new SPException("The Farms timer service cannot be found.");
9: }
10: TimerJobs.CopySettingsJob job = timerService.JobDefinitions.GetValue<TimerJobs.CopySettingsJob>(TimerJobs.CopySettingsJob.JOB_NAME);
11: if (null == job)
12: {
13: job = new TimerJobs.CopySettingsJob(timerService);
14: }
15: job.SubmitJob(true, properties.Feature.Definition.RootDirectory);
16: }
17: catch (Exception ex)
18: {
19: Logger.WriteException(ex);
20: }
Hopefully this simple example helps you to solve your file deployment challenges.
Ensuring a Valid SPContext via Feature Activation
I’ve been meaning to blog about this for a while but just haven’t gotten around to it. Have you ever needed to add a web part to a page during Feature activation? Of course you can do this declaratively using CAML but I usually prefer to do this stuff via code. The challenge is that occasionally you will need to activate the Feature outside the context of a web request – such as via STSADM – this becomes critical for certain web parts, such as earlier versions of the KPI List Web Part, which required a valid SPContext in order to be added to a page (this web part was fixed in the August 2008 Cumulative Update (or thereabouts) so that it no longer requires a valid SPContext object).
If you’re faced with adding a web part (or any other artifact) which requires a valid SPContext object outside of a web request than you can create your own context with the following three lines of code:
1: HttpRequest httpRequest = new HttpRequest("", web.Url, "");
2: HttpContext.Current = new HttpContext(httpRequest, new HttpResponse(new StringWriter()));
3: SPControl.SetContextWeb(HttpContext.Current, web);
public static void AddWebPart(SPWeb web, string page, string webPartXmlFile, string zone, int zoneId, bool deleteExistingIfFound) { bool cleanupContext = false; try { if (HttpContext.Current == null) { cleanupContext = true; HttpRequest httpRequest = new HttpRequest("", web.Url, ""); HttpContext.Current = new HttpContext(httpRequest, new HttpResponse(new StringWriter())); SPControl.SetContextWeb(HttpContext.Current, web); } using (SPLimitedWebPartManager manager = web.GetLimitedWebPartManager(page, PersonalizationScope.Shared)) { string err; XmlTextReader reader = null; System.Web.UI.WebControls.WebParts.WebPart wp; try { string webPartXml = File.ReadAllText(webPartXmlFile); webPartXml = webPartXml.Replace("${siteCollection}", web.Site.Url); webPartXml = webPartXml.Replace("${site}", web.Url); webPartXml = webPartXml.Replace("${webTitle}", HttpUtility.HtmlEncode(web.Title)); reader = new XmlTextReader(new StringReader(webPartXml)); wp = manager.ImportWebPart(reader, out err); if (!string.IsNullOrEmpty(err)) throw new Exception(err); } finally { if (reader != null) reader.Close(); } // Delete existing web part with same title so that we only have the latest version on the page foreach (System.Web.UI.WebControls.WebParts.WebPart wpTemp in manager.WebParts) { if (wpTemp.Title == wp.Title) { if (deleteExistingIfFound) manager.DeleteWebPart(wpTemp); else { wpTemp.Dispose(); return; } break; } wpTemp.Dispose(); } manager.AddWebPart(wp, zone, zoneId); } } finally { if (HttpContext.Current != null && cleanupContext) { HttpContext.Current = null; } } }
Deactivating Features at Different Scopes Using STSADM
I just posted about one of my new commands, gl-activatefeature, which covers activating features at different scopes using STSADM. That article covers all the code necessary to not only implement the activation, but also the deactivation. So creating the next command, gl-deactivatefeature, was as simple as copying the container code for the activation command and changing a few simple lines of code. This command works exactly like the other except that it deactivates the Feature instead of activating it.
The help for the command is shown below:
C:\>stsadm -help gl-deactivatefeature
stsadm -o gl-deactivatefeature
Deactivates a feature at a given scope.
Parameters:
{-filename <relative path to Feature.xml> |
-name <feature folder> |
-id <feature Id>}
[-scope <farm | webapplication | site | web | feature> (defaults to Feature)]
[-url <url>]
[-force]
[-ignorenonactive]
|
The following table summarizes the command and its various parameters:
| Command Name | Availability | Build Date |
|---|---|---|
| gl-deactivatefeature | WSS v3, MOSS 2007 | Released: 11/15/2008
|
| Parameter Name | Short Form | Required | Description | Example Usage |
|---|---|---|---|---|
| filename | f | Yes, if -name or -id is not provided |
Path to feature must be a relative path to the 12\Template\Features directory. Can be any standard character that the Windows system supports for a file name. Note: If the feature file is not found on disk, the following error message is displayed: “Failed to find the XML file at location '12\Template\Features\<file path>'.” |
-filename "MyFeature\feature.xml"
-f "MyFeature\feature.xml" |
| name | n | Yes, if -filename or -id is not provided | Name of the feature folder located in the 12\Template\Features directory | -name "MyFeature"
-n "MyFeature" |
| id | Yes, if -filename or -name is not provided |
GUID that identifies the feature to activate Note: If the ID is specified but the feature does not exist, the following error message is returned: "Feature '<id>' is not installed in this farm, and cannot be added to this scope." |
-id "21d186e1-7036-4092-a825-0eb6709e9280" | |
| scope | s | No | The scope to look at when deactivating the Feature. Valid values are "Farm", "WebApplication", "Site", "Web", or "Feature". If "Feature" is specified then the scope of the Feature will be used.
Note: Be careful when using a scope of "Web" (or "Feature" when the Feature is scoped to Web) as this will work recursively upon not just the single web but all sub-webs as well. |
-scope site
-scope s |
| url | No |
URL of the Web application, site collection, or Web site to which the feature is being deactivated with respect to the provided scope. So if the Feature is scoped to Web and you pass in a scope of Site then all webs within the Site Collection of the provided URL will have the Feature deactivated. If the scope is Farm then an URL is not required. |
-url http://portal | |
| force | No |
Forces the deactivation of the feature if already deactivated. |
-force | |
| ignorenonactive | ignore | No | This will prevent the Feature from attempting a deactivation if it is not already activated thus avoiding errors about the Feature not being activated at the particular scope. | -ignorenonactive
-ignore |
The following is an example of how to deactivate a Site Collection scoped Feature on every site collection within a web application:
stsadm -o gl-deactivatefeature -name MyCustomFeature -scope webapplication -url http://mysites -force
The following is an example of how to deactivate a Web scoped Feature on every web within a web application where the Feature is already activated:
stsadm -o gl-deactivatefeature -name MyCustomFeature -scope webapplication -url http://portal -ignorenonactive
Activating Features at Different Scopes Using STSADM
How many times have you had a Feature, either out-of-the-box or custom, that you have needed to activate at lots of different scopes or re-activate at lots of different scopes? To do this you may have found a way to get the list of site collections or webs and then somehow used that list in conjunction with the STSADM activatefeature command or worse you manually went to every site or web and manually activated or re-activated the Feature - this is extremely tedious and error prone as you may miss a site or web. Another common scenario has to do with "My Sites" - perhaps you've written a custom Feature that configures a users my site when created and now you've made a change to that Feature and need to reactivate the Feature on all existing My Sites. Doing this in the past was a pain - but not any more thanks to my new command: gl-activatefeature.
The specific scenario that prompted me to write this command was the need to re-activate a custom Feature everywhere it was currently activated without activating it anywhere it wasn't already activated. I needed to do this because I had added a couple of event receivers to an already deployed Feature but I didn't know where that Feature was already activated and didn't wish to activate it if not already activated. So I had two three core issues to solve - the first was to enable the activation (and eventually deactivation) of a Feature at the various scopes (Farm, Web Application, Site, and Web); the second was to be able to conditionally force a re-activation only if the Feature is already activated and not do anything if not activated; the third was to be able to iterate over various scopes - Farm, Web Application, Site, or Web (so if the Feature is scoped to Site and the user passes in a scope of Web Application then I need to look at every Site Collection within the specified Web Application). I also wanted to have the parameters of the command work just like the OOTB command (along with any additional parameters I'd need).
So the first thing I need to do was make sure that I could get the Feature ID which would be used throughout the code - but I wanted the user to be able to pass in the ID (the easy part), the name, or the filename - just like the OOTB command. I took a look at how the OOTB command worked by using Reflector and found a simple method that I was able to refactor slightly:
internal static Guid GetFeatureIdFromParams(SPParamCollection Params)
{
Guid empty = Guid.Empty;
if (!Params["id"].UserTypedIn)
{
SPFeatureScope scope;
if (!Params["filename"].UserTypedIn)
{
if (Params["name"].UserTypedIn)
{
SPFeatureScope scope2;
SPFeatureDefinition.GetFeatureIdAndScope(Params["name"].Value + @"\feature.xml", out empty, out scope2);
}
return empty;
}
SPFeatureDefinition.GetFeatureIdAndScope(Params["filename"].Value, out empty, out scope);
return empty;
}
return new Guid(Params["id"].Value);
}
Once I had the Feature ID I could now use this to conditionally add or remove (activate or deactivate) the Feature from the appropriate scope. The way you activate a Feature programmatically is to simply call the Add or Remove methods of an SPFeatureCollection object. You can get this object from either the SPFarm, SPWebApplication, SPSite, or SPWeb objects - each containing a "Features" property which exposes the collection object.
private SPFeature ActivateDeactivateFeature(SPFeatureCollection features, bool activate, Guid featureId, string urlScope, bool force, bool ignoreNonActive)
{
if (features[featureId] == null && ignoreNonActive)
return null;
if (!activate)
{
if (features[featureId] != null || force)
{
Log("Progress: Deactivating Feature {0} from {1}.", featureId.ToString(), urlScope);
try
{
features.Remove(featureId, force);
}
catch (Exception ex)
{
Log("WARNING: {0}", ex.Message);
}
}
else
{
Log("WARNING: " + SPResource.GetString("FeatureNotActivatedAtScope", new object[] { featureId }) + " Use the -force parameter to force a deactivation.");
}
return null;
}
if (features[featureId] == null)
Log("Progress: Activating Feature {0} on {1}.", featureId.ToString(), urlScope);
else
{
if (!force)
{
SPFeatureDefinition fd = features[featureId].Definition;
Log("WARNING: " + SPResource.GetString("FeatureAlreadyActivated", new object[] { fd.DisplayName, fd.Id, urlScope }) + " Use the -force parameter to force a reactivation.");
return features[featureId];
}
Log("Progress: Re-Activating Feature {0} on {1}.", featureId.ToString(), urlScope);
}
return features.Add(featureId, force);
}
One of the first things I do in this method is check if the user has chosen to ignore situations when the Feature is not already active - I do that by checking the item indexer and seeing if it returns null: features[featureId] == null. If null is returned then the Feature is not activated.
Once I had the two methods above I could then create all the support code which basically just determines which scopes to consider based on the Feature scope and the user provided scope. I also use my cool SPEnumerator class that I created a while back to help make iterating nice and easy. I wrapped all this code up into a single helper class that I could then use with both my gl-activatefeature and gl-deactivatefeature commands (I'll cover the gl-deactivatefeature command in the next post).
using System;
using System.IO;
using Lapointe.SharePoint.STSADM.Commands.OperationHelpers;
using Microsoft.SharePoint;
using Microsoft.SharePoint.Administration;
namespace Lapointe.SharePoint.STSADM.Commands.Features
{
public enum ActivationScope
{
Farm, WebApplication, Site, Web, Feature
}
public class FeatureHelper
{
private string m_Url;
private bool m_Force;
private Guid m_FeatureId = Guid.Empty;
private bool m_IgnoreNonActive;
private bool m_Activate;
/// <summary>
/// Logs the specified message.
/// </summary>
/// <param name="message">The message.</param>
/// <param name="args">The args.</param>
protected virtual void Log(string message, params string[] args)
{
SPOperation.Log(message, args);
}
/// <summary>
/// Gets the feature id from params.
/// </summary>
/// <param name="Params">The params.</param>
/// <returns></returns>
internal static Guid GetFeatureIdFromParams(SPParamCollection Params)
{
Guid empty = Guid.Empty;
if (!Params["id"].UserTypedIn)
{
SPFeatureScope scope;
if (!Params["filename"].UserTypedIn)
{
if (Params["name"].UserTypedIn)
{
SPFeatureScope scope2;
SPFeatureDefinition.GetFeatureIdAndScope(Params["name"].Value + @"\feature.xml", out empty, out scope2);
}
return empty;
}
SPFeatureDefinition.GetFeatureIdAndScope(Params["filename"].Value, out empty, out scope);
return empty;
}
return new Guid(Params["id"].Value);
}
/// <summary>
/// Activates or deactivates the feature at the specified scope.
/// </summary>
/// <param name="scope">The scope.</param>
/// <param name="featureId">The feature id.</param>
/// <param name="activate">if set to <c>true</c> [activate].</param>
/// <param name="url">The URL.</param>
/// <param name="force">if set to <c>true</c> [force].</param>
/// <param name="ignoreNonActive">if set to <c>true</c> [ignore non active].</param>
public void ActivateDeactivateFeatureAtScope(ActivationScope scope, Guid featureId, bool activate, string url, bool force, bool ignoreNonActive)
{
SPOperation.Verbose = true;
m_FeatureId = featureId;
m_Url = url;
m_Force = force;
m_IgnoreNonActive = ignoreNonActive;
m_Activate = activate;
if (m_FeatureId.Equals(Guid.Empty))
throw new SPException("Unable to locate Feature.");
SPFeatureDefinition feature = SPFarm.Local.FeatureDefinitions[m_FeatureId];
if (feature == null)
throw new SPException("Unable to locate Feature.");
if (scope == ActivationScope.Feature)
scope = (ActivationScope)Enum.Parse(typeof(ActivationScope), feature.Scope.ToString().ToLowerInvariant(), true);
if (feature.Scope == SPFeatureScope.Farm)
{
if (scope != ActivationScope.Farm)
throw new SPSyntaxException("The Feature specified is scoped to the Farm. The -scope parameter must be \"Farm\".");
ActivateDeactivateFeatureAtFarm(activate, m_FeatureId, m_Force, m_IgnoreNonActive);
}
else if (feature.Scope == SPFeatureScope.WebApplication)
{
if (scope != ActivationScope.Farm && scope != ActivationScope.WebApplication)
throw new SPSyntaxException("The Feature specified is scoped to the Web Application. The -scope parameter must be either \"Farm\" or \"WebApplication\".");
if (scope == ActivationScope.Farm)
{
SPEnumerator enumerator = new SPEnumerator(SPFarm.Local);
enumerator.SPWebApplicationEnumerated += enumerator_SPWebApplicationEnumerated;
enumerator.Enumerate();
}
else
{
if (string.IsNullOrEmpty(m_Url))
throw new SPSyntaxException("The -url parameter is required if the scope is \"WebApplication\".");
SPWebApplication webApp = SPWebApplication.Lookup(new Uri(m_Url));
ActivateDeactivateFeatureAtWebApplication(webApp, m_FeatureId, activate, m_Force, m_IgnoreNonActive);
}
}
else if (feature.Scope == SPFeatureScope.Site)
{
if (scope == ActivationScope.Web)
throw new SPSyntaxException("The Feature specified is scoped to Site. The -scope parameter cannot be \"Web\".");
SPSite site = null;
SPEnumerator enumerator = null;
try
{
if (scope == ActivationScope.Farm)
enumerator = new SPEnumerator(SPFarm.Local);
else if (scope == ActivationScope.WebApplication)
{
SPWebApplication webApp = SPWebApplication.Lookup(new Uri(m_Url));
enumerator = new SPEnumerator(webApp);
}
else if (scope == ActivationScope.Site)
{
site = new SPSite(m_Url);
ActivateDeactivateFeatureAtSite(site, activate, m_FeatureId, m_Force, m_IgnoreNonActive);
}
if (enumerator != null)
{
enumerator.SPSiteEnumerated += enumerator_SPSiteEnumerated;
enumerator.Enumerate();
}
}
finally
{
if (site != null)
site.Dispose();
}
}
else if (feature.Scope == SPFeatureScope.Web)
{
SPSite site = null;
SPWeb web = null;
SPEnumerator enumerator = null;
try
{
if (scope == ActivationScope.Farm)
enumerator = new SPEnumerator(SPFarm.Local);
else if (scope == ActivationScope.WebApplication)
{
SPWebApplication webApp = SPWebApplication.Lookup(new Uri(m_Url));
enumerator = new SPEnumerator(webApp);
}
else if (scope == ActivationScope.Site)
{
site = new SPSite(m_Url);
enumerator = new SPEnumerator(site);
}
else if (scope == ActivationScope.Web)
{
site = new SPSite(m_Url);
web = site.AllWebs[Utilities.GetServerRelUrlFromFullUrl(m_Url)];
ActivateDeactivateFeatureAtWeb(site, web, activate, m_FeatureId, m_Force, m_IgnoreNonActive);
}
if (enumerator != null)
{
enumerator.SPWebEnumerated += enumerator_SPWebEnumerated;
enumerator.Enumerate();
}
}
finally
{
if (web != null)
web.Dispose();
if (site != null)
site.Dispose();
}
}
}
#region Event Handlers
/// <summary>
/// Handles the SPWebEnumerated event of the enumerator control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="Lapointe.SharePoint.STSADM.Commands.OperationHelpers.SPEnumerator.SPWebEventArgs"/> instance containing the event data.</param>
private void enumerator_SPWebEnumerated(object sender, SPEnumerator.SPWebEventArgs e)
{
ActivateDeactivateFeatureAtWeb(e.Site, e.Web, m_Activate, m_FeatureId, m_Force, m_IgnoreNonActive);
}
/// <summary>
/// Handles the SPSiteEnumerated event of the enumerator control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="Lapointe.SharePoint.STSADM.Commands.OperationHelpers.SPEnumerator.SPSiteEventArgs"/> instance containing the event data.</param>
private void enumerator_SPSiteEnumerated(object sender, SPEnumerator.SPSiteEventArgs e)
{
ActivateDeactivateFeatureAtSite(e.Site, m_Activate, m_FeatureId, m_Force, m_IgnoreNonActive);
}
/// <summary>
/// Handles the SPWebApplicationEnumerated event of the enumerator control.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">The <see cref="Lapointe.SharePoint.STSADM.Commands.OperationHelpers.SPEnumerator.SPWebApplicationEventArgs"/> instance containing the event data.</param>
private void enumerator_SPWebApplicationEnumerated(object sender, SPEnumerator.SPWebApplicationEventArgs e)
{
ActivateDeactivateFeatureAtWebApplication(e.WebApplication, m_FeatureId, m_Activate, m_Force, m_IgnoreNonActive);
}
#endregion
/// <summary>
/// Activates or deactivates the feature.
/// </summary>
/// <param name="features">The features.</param>
/// <param name="activate">if set to <c>true</c> [activate].</param>
/// <param name="featureId">The feature id.</param>
/// <param name="urlScope">The URL scope.</param>
/// <param name="force">if set to <c>true</c> [force].</param>
/// <param name="ignoreNonActive">if set to <c>true</c> [ignore non active].</param>
/// <returns></returns>
private SPFeature ActivateDeactivateFeature(SPFeatureCollection features, bool activate, Guid featureId, string urlScope, bool force, bool ignoreNonActive)
{
if (features[featureId] == null && ignoreNonActive)
return null;
if (!activate)
{
if (features[featureId] != null || force)
{
Log("Progress: Deactivating Feature {0} from {1}.", featureId.ToString(), urlScope);
try
{
features.Remove(featureId, force);
}
catch (Exception ex)
{
Log("WARNING: {0}", ex.Message);
}
}
else
{
Log("WARNING: " + SPResource.GetString("FeatureNotActivatedAtScope", new object[] { featureId }) + " Use the -force parameter to force a deactivation.");
}
return null;
}
if (features[featureId] == null)
Log("Progress: Activating Feature {0} on {1}.", featureId.ToString(), urlScope);
else
{
if (!force)
{
SPFeatureDefinition fd = features[featureId].Definition;
Log("WARNING: " + SPResource.GetString("FeatureAlreadyActivated", new object[] { fd.DisplayName, fd.Id, urlScope }) + " Use the -force parameter to force a reactivation.");
return features[featureId];
}
Log("Progress: Re-Activating Feature {0} on {1}.", featureId.ToString(), urlScope);
}
return features.Add(featureId, force);
}
/// <summary>
/// Activates or deactivates the farm scoped feature.
/// </summary>
/// <param name="activate">if set to <c>true</c> [activate].</param>
/// <param name="featureId">The feature id.</param>
/// <param name="force">if set to <c>true</c> [force].</param>
/// <param name="ignoreNonActive">if set to <c>true</c> [ignore non active].</param>
/// <returns></returns>
public SPFeature ActivateDeactivateFeatureAtFarm(bool activate, Guid featureId, bool force, bool ignoreNonActive)
{
SPWebService service = SPFarm.Local.Services.GetValue<SPWebService>(string.Empty);
return ActivateDeactivateFeature(service.Features, activate, featureId, "Farm", force, ignoreNonActive);
}
/// <summary>
/// Activates or deactivates the web application scoped feature.
/// </summary>
/// <param name="activate">if set to <c>true</c> [activate].</param>
/// <param name="featureId">The feature id.</param>
/// <param name="urlScope">The URL scope.</param>
/// <param name="force">if set to <c>true</c> [force].</param>
/// <param name="ignoreNonActive">if set to <c>true</c> [ignore non active].</param>
/// <returns></returns>
public SPFeature ActivateDeactivateFeatureAtWebApplication(bool activate, Guid featureId, string urlScope, bool force, bool ignoreNonActive)
{
SPWebApplication application = SPWebApplication.Lookup(new Uri(urlScope));
if (application == null)
{
throw new FileNotFoundException(SPResource.GetString("WebApplicationLookupFailed", new object[] { urlScope }));
}
return ActivateDeactivateFeatureAtWebApplication(application, featureId, activate, force, ignoreNonActive);
}
/// <summary>
/// Activates or deactivates the web application scoped feature.
/// </summary>
/// <param name="application">The application.</param>
/// <param name="featureId">The feature id.</param>
/// <param name="activate">if set to <c>true</c> [activate].</param>
/// <param name="force">if set to <c>true</c> [force].</param>
/// <param name="ignoreNonActive">if set to <c>true</c> [ignore non active].</param>
/// <returns></returns>
public SPFeature ActivateDeactivateFeatureAtWebApplication(SPWebApplication application, Guid featureId, bool activate, bool force, bool ignoreNonActive)
{
return ActivateDeactivateFeature(application.Features, activate, featureId, application.GetResponseUri(SPUrlZone.Default).ToString(), force, ignoreNonActive);
}
/// <summary>
/// Activates or deactivates the site scoped feature.
/// </summary>
/// <param name="activate">if set to <c>true</c> [activate].</param>
/// <param name="featureId">The feature id.</param>
/// <param name="urlScope">The URL scope.</param>
/// <param name="force">if set to <c>true</c> [force].</param>
/// <param name="ignoreNonActive">if set to <c>true</c> [ignore non active].</param>
/// <returns></returns>
public SPFeature ActivateDeactivateFeatureAtSite(bool activate, Guid featureId, string urlScope, bool force, bool ignoreNonActive)
{
using (SPSite site = new SPSite(urlScope))
using (SPWeb web = site.OpenWeb(Utilities.GetServerRelUrlFromFullUrl(urlScope), true))
{
if (web.IsRootWeb)
{
return ActivateDeactivateFeatureAtSite(site, activate, featureId, force, ignoreNonActive);
}
throw new SPException(SPResource.GetString("FeatureActivateDeactivateScopeAmbiguous", new object[] { site.Url }));
}
}
/// <summary>
/// Activates or deactivates the site scoped feature.
/// </summary>
/// <param name="site">The site.</param>
/// <param name="activate">if set to <c>true</c> [activate].</param>
/// <param name="featureId">The feature id.</param>
/// <param name="force">if set to <c>true</c> [force].</param>
/// <param name="ignoreNonActive">if set to <c>true</c> [ignore non active].</param>
/// <returns></returns>
public SPFeature ActivateDeactivateFeatureAtSite(SPSite site, bool activate, Guid featureId, bool force, bool ignoreNonActive)
{
return ActivateDeactivateFeature(site.Features, activate, featureId, site.Url, force, ignoreNonActive);
}
/// <summary>
/// Activates or deactivates the web scoped feature.
/// </summary>
/// <param name="activate">if set to <c>true</c> [activate].</param>
/// <param name="featureId">The feature id.</param>
/// <param name="urlScope">The URL scope.</param>
/// <param name="force">if set to <c>true</c> [force].</param>
/// <param name="ignoreNonActive">if set to <c>true</c> [ignore non active].</param>
/// <returns></returns>
public SPFeature ActivateDeactivateFeatureAtWeb(bool activate, Guid featureId, string urlScope, bool force, bool ignoreNonActive)
{
using (SPSite site = new SPSite(urlScope))
using (SPWeb web = site.OpenWeb())
{
return ActivateDeactivateFeatureAtWeb(site, web, activate, featureId, force, ignoreNonActive);
}
}
/// <summary>
/// Activates or deactivates the web scoped feature.
/// </summary>
/// <param name="site">The site.</param>
/// <param name="web">The web.</param>
/// <param name="activate">if set to <c>true</c> [activate].</param>
/// <param name="featureId">The feature id.</param>
/// <param name="force">if set to <c>true</c> [force].</param>
/// <param name="ignoreNonActive">if set to <c>true</c> [ignore non active].</param>
/// <returns></returns>
public SPFeature ActivateDeactivateFeatureAtWeb(SPSite site, SPWeb web, bool activate, Guid featureId, bool force, bool ignoreNonActive)
{
return ActivateDeactivateFeature(web.Features, activate, featureId, web.Url, force, ignoreNonActive);
}
}
}
The help for the command is shown below:
C:\>stsadm -help gl-activatefeature
stsadm -o gl-activatefeature
Activates a feature at a given scope.
Parameters:
{-filename <relative path to Feature.xml> |
-name <feature folder> |
-id <feature Id>}
[-scope <farm | webapplication | site | web | feature> (defaults to Feature)]
[-url <url>]
[-force]
[-ignorenonactive]
|
The following table summarizes the command and its various parameters:
| Command Name | Availability | Build Date |
|---|---|---|
| gl-activatefeature | WSS v3, MOSS 2007 | Released: 11/15/2008
|
| Parameter Name | Short Form | Required | Description | Example Usage |
|---|---|---|---|---|
| filename | f | Yes, if -name or -id is not provided |
Path to feature must be a relative path to the 12\Template\Features directory. Can be any standard character that the Windows system supports for a file name. Note: If the feature file is not found on disk, the following error message is displayed: “Failed to find the XML file at location '12\Template\Features\<file path>'.” |
-filename "MyFeature\feature.xml"
-f "MyFeature\feature.xml" |
| name | n | Yes, if -filename or -id is not provided | Name of the feature folder located in the 12\Template\Features directory | -name "MyFeature"
-n "MyFeature" |
| id | Yes, if -filename or -name is not provided |
GUID that identifies the feature to activate Note: If the ID is specified but the feature does not exist, the following error message is returned: "Feature '<id>' is not installed in this farm, and cannot be added to this scope." |
-id "21d186e1-7036-4092-a825-0eb6709e9280" | |
| scope | s | No | The scope to look at when activating the Feature. Valid values are "Farm", "WebApplication", "Site", "Web", or "Feature". If "Feature" is specified then the scope of the Feature will be used.
Note: Be careful when using a scope of "Web" (or "Feature" when the Feature is scoped to Web) as this will work recursively upon not just the single web but all sub-webs as well. |
-scope site
-s site |
| url | No |
URL of the Web application, site collection, or Web site to which the feature is being activated with respect to the provided scope. So if the Feature is scoped to Web and you pass in a scope of Site then all webs within the Site Collection of the provided URL will have the Feature activated. If the scope is Farm then an URL is not required. |
-url http://portal | |
| force | No |
Forces the re-activation of the feature if already activated. This causes any custom code associated with the feature to rerun. |
-force | |
| ignorenonactive | ignore | No | This will prevent the Feature from being activated if it is not already activated thus triggering a reactivation where already activated. | -ignorenonactive
-ignore |
The following is an example of how to activate a Site Collection scoped Feature on every site collection within a web application:
stsadm -o gl-activatefeature -name MyCustomFeature -scope webapplication -url http://mysites -force
The following is an example of how to re-activate a Web scoped Feature on every web within a web application where the Feature is already activated:
stsadm -o gl-activatefeature -name MyCustomFeature -scope webapplication -url http://portal -ignorenonactive
Customizing MySites
This posting doesn't actually have anything to do with stsadm (unless you count the fact that you need stsadm to install Features) but I figured I'd post this here regardless as I've seen a lot of questions out there regarding how to do this. My specific requirement was to remove the "Create Blog" button on the My Sites home page. There are two ways to accomplish this - one that's supported and one that's not. The easiest approach (and the unsupported approach) is to modify the default.aspx file located at "C:\Program Files\Common Files\Microsoft Shared\web server extensions\12\TEMPLATE\SiteTemplates\SPSPERS". In this file simply remove or comment out the CreateBlogButton web control (ID="TBBCreateBlogButton"). Note that I don't recommend doing this as modifying these files will violate your support with Microsoft but I figure it's at least worth mentioning that it's possible.
The other approach is to use a Feature. There's a great article by Steve Peschka on customizing My Sites which explains how to do all of this. There's also a template project which you can find on CodePlex. I used this project as a starting place and tweaked it to meet my needs. There's three approaches you can take to remove the create blog button (I won't bother to address the other customizations you can do as they are documented on the above sites and in the readme files provided with the download).
The first is to modify the master page and make the PlaceHolderMiniConsole control no longer visible. The problem with this approach is that it also hides any other buttons that may be loaded into this control. The second approach is to put some code into the master page in a server script tag - that code would then do a series of FindControl calls to get the TBBCreateBlogButton control and then hide it. The problem with this is that it requires you to make some web.config changes to allow code blocks - something I'd strongly recommend against doing unless you had no other choice. The third option (which is the one I took) is to have the MySiteCreatePart control look for and remove the button (again, using FindControl). The one downside of this is that the code to hide the button has to run every time the page is loaded.
There may be other ways to do this but I was not able to identify one. Just a quick overview of what Steve's code is doing - there are three components: the first is the MySiteStaplee which contains the custom master page and the MySiteStaplee.xml file which has all the settings that the MySiteCreatePart code utilizies. The custom master page is exactly like the default master page with the addition of two lines - one registers the MySiteCreatePart web control and the other adds the web control to the page in the Head section (you can modify this further to your heart's content). The second part is the MySiteStapler which simply associates the MySiteStaplee feature with the SPSPERS#0 site template (so when a site is created with this site template the MySiteStaplee feature will be activated). The third piece is the web control (MySiteCreatePart) which gets loaded up by the master page.
This approach of using a web control allows the code to manipulate the running page without having to embed server side script blocks which would open up security issues (note that this is a simple object inheriting from WebControl - it is not a WebPart object - it really is about as basic as you can get so it's real easy to work with). And finally, there's a web.config change that is necessary to mark the web control as a safe control. In order to do what I needed I had to modify the code that Steve provided.
The first thing I did was to clean it up a bit - I broke the main method into a couple of smaller methods to help in readability and I modified the code so that it properly disposed of the objects such as the SPLimitedWebPartManager object and the various WebPart objects (minor issue but this should help with memory management). The next thing I added was a new attribute to the MySiteStaplee.xml file. I wanted to make it so that I could control whether the button appeared or not via the XML file so I added a "removeBlogButton" attribute to the document element. This way the value can be changed in the future so that any new MySites created would reflect the change (to address existing MySites one would simply need to loop through all the sites and set a custom property to true or false (assuming of course that the feature has been installed)). With this attribute in place I was then able to set a property on the SPWeb object which I then check whenever the page is loaded. The final code changes I made was to support an additional action and handle deactivation of the feature - Steve's version only allowed Delete, Move, Add, and SetProperties, but I needed to be able to do a Close as well so I added that logic in. I also wanted it so that if the MySiteStaplee feature was deactivated that the original master page would be reassociated so I added that code in. The other changes I made to the MySiteStaplee.xml file were to simply move a couple of web parts around.
Download the original version for additional examples (I also included the original version within my download). Feel free to download my version of the MySiteCreate Feature and let me know if you have any issues.
Update 1/15/2008: I've fixed the batch files and updated the FeatureActivating event so that it won't activate the feature at the root site and in case it does it will only switch the master page if the template is SPSPERS.
Enumerate Features
One of the tasks I needed to do for my upgrade was to convert a sub site to a site collection. This is something I'm still trying to get right as of this writing but figured I'd share a new command I had to create to help with that. To convert a site to a site collection at a managed path you need to create the managed path, create a site with no site template, export your existing site, and then import to the new location all using STSADM. Before you can do the import though you have to make sure that certain required features are enabled.
I found that STSADM had the ability to activate and deactivate features but there was no way for me to tell which features were currently set (without using the web tool). I wanted/needed an easy way to identify existing features associated with a site collection as well as what the actual name is (if you've ever tried to match the descriptive name to the feature name it's not always obvious). So my solution was to create a simple command which would list out all features along with their actual name (not the user friendly name).
I snagged most of the code from the Microsoft.SharePoint.WebControls.FeatureActivator server control in the Microsoft.SharePoint.ApplicationPages assembly. This is the main control that SharePoint uses when you wish to activate or deactivate features on a site (or farm or web application). The long and short of it is that I'm building a DataTable of features by looping through a SPFeatureDefinitionCollection which I get by calling SPFarm.Local.FeatureDefinitions. Beyond that I'm merely filtering based on passed in parameters. In order to show which features are active at the given scope I check for existence in the active features collection which I get by calling either SPSite's Features property, SPWeb's Features property or SPSite's WebApplication.Features property. I then either return it as XML or a flat list based on a passed in parameter. The core code is shown below:
1: private static void AddFeaturesToTable(ref DataTable dtblFeatureList, string url, SPFeatureScope scope, bool showHidden)
2: {
3: CultureInfo info;
4: Dictionary activeFeatures = new Dictionary();
5:
6: // We need to get the SPSite and SPWeb so that we can get the culture info and any active features if a scope was passed in.
7: using (SPSite site = new SPSite(url))
8: {
9: using (SPWeb web = site.OpenWeb())
10: {
11: info = new CultureInfo((int)web.Language, false);
12:
13: activeFeatures[SPFeatureScope.Farm] = SPWebService.ContentService.Features;
14: activeFeatures[SPFeatureScope.WebApplication] = site.WebApplication.Features;
15: activeFeatures[SPFeatureScope.Site] = site.Features;
16: activeFeatures[SPFeatureScope.Web] = web.Features;
17: }
18: }
19:
20: foreach (SPFeatureDefinition definition in SPFarm.Local.FeatureDefinitions)
21: {
22: try
23: {
24: Guid featureID = definition.Id;
25: // If the scope is marked as invalid then that's our flag that it wasn't provided so we're going to
26: // list everything regardless of scope and show those that are active for the Web scope.
27: if (definition.Scope != scope && scope != SPFeatureScope.ScopeInvalid)
28: continue;
29:
30: if (definition.Hidden && !showHidden)
31: {
32: continue;
33: }
34: if (!definition.SupportsLanguage(info))
35: {
36: continue;
37: }
38:
39: bool isActive = false;
40: if (activeFeatures[definition.Scope] != null)
41: isActive = (activeFeatures[definition.Scope][featureID] != null);
42:
43: DataRow row = BuildDataRowFromFeatureDefinition(dtblFeatureList, info, definition, isActive);
44: dtblFeatureList.Rows.Add(row);
45: }
46: catch (SPException)
47: {
48: continue;
49: }
50:
51: }
52: }
The syntax of the command can be seen below:
C:\>stsadm -help gl-enumfeatures
stsadm -o gl-enumfeatures
Returns the list of features and their activation status.
Parameters:
-url <site collection url>
[-scope <Farm | Site | Web | WebApplication>]
[-showhidden]
[-xml]
Here’s an example of how to return the features for a publishing site site collection at the managed path "/hr":
stsadm –o gl-enumfeatures –url "http://intranet/hr/Pages/" -scope Site -xml
The results of running the above command can be seen below (note that the node names are somewhat confusing as the node DisplayName implies that this is what you would see in the web browser - in reality it's the Title node that shows in the browser - I thought about changing the name to something else but in the end I decided to be consistent with the property names on the objects):
1: <Features>
2: <Feature>
3: <FeatureId>02464c6a-9d07-4f30-ba04-e9035cf54392</FeatureId>
4: <Title>Routing Workflows</Title>
5: <DisplayName>ReviewWorkflows</DisplayName>
6: <Description>Workflows that send a document for feedback or approval.</Description>
7: <Status>Active</Status>
8: </Feature>
9: <Feature>
10: <FeatureId>6c09612b-46af-4b2f-8dfc-59185c962a29</FeatureId>
11: <Title>Collect Signatures Workflow</Title>
12: <DisplayName>SignaturesWorkflow</DisplayName>
13: <Description>Gathers signatures needed to complete a Microsoft Office document.</Description>
14: <Status>Active</Status>
15: </Feature>
16: <Feature>
17: <FeatureId>7094bd89-2cfe-490a-8c7e-fbace37b4a34</FeatureId>
18: <Title>Reporting</Title>
19: <DisplayName>Reporting</DisplayName>
20: <Description>Creates reports about information in Windows SharePoint Services.</Description>
21: <Status>Active</Status>
22: </Feature>
23: <Feature>
24: <FeatureId>8581a8a7-cf16-4770-ac54-260265ddb0b2</FeatureId>
25: <Title>Office SharePoint Server Enterprise Site Collection features</Title>
26: <DisplayName>PremiumSite</DisplayName>
27: <Description>Features such as the business data catalog, forms services, and Excel Services, included in the Office SharePoint Server Enterprise License</Description>
28: <Status>Active</Status>
29: </Feature>
30: <Feature>
31: <FeatureId>b21b090c-c796-4b0f-ac0f-7ef1659c20ae</FeatureId>
32: <Title>Office SharePoint Server Standard Site Collection features</Title>
33: <DisplayName>BaseSite</DisplayName>
34: <Description>Features such as user profiles and search, included in the Office SharePoint Server Standard License</Description>
35: <Status>Active</Status>
36: </Feature>
37: <Feature>
38: <FeatureId>c6561405-ea03-40a9-a57f-f25472942a22</FeatureId>
39: <Title>Translation Management Workflow</Title>
40: <DisplayName>TranslationWorkflow</DisplayName>
41: <Description>Manages document translation by creating copies of the document to be translated and assigning translation tasks to translators.</Description>
42: <Status>Active</Status>
43: </Feature>
44: <Feature>
45: <FeatureId>c85e5759-f323-4efb-b548-443d2216efb5</FeatureId>
46: <Title>Disposition Approval Workflow</Title>
47: <DisplayName>ExpirationWorkflow</DisplayName>
48: <Description>Manages document expiration and retention by allowing participants to decide whether to retain or delete expired documents.</Description>
49: <Status>Active</Status>
50: </Feature>
51: <Feature>
52: <FeatureId>eaf6a128-0482-4f71-9a2f-b1c650680e77</FeatureId>
53: <Title>Office SharePoint Server Search Web Parts</Title>
54: <DisplayName>SearchWebParts</DisplayName>
55: <Description>This feature uploads all web parts required for Search Center</Description>
56: <Status>Inactive</Status>
57: </Feature>
58: <Feature>
59: <FeatureId>f6924d36-2fa8-4f0b-b16d-06b7250180fa</FeatureId>
60: <Title>Office SharePoint Server Publishing Infrastructure</Title>
61: <DisplayName>PublishingSite</DisplayName>
62: <Description>Provides centralized libraries, content types, master pages and page layouts and enables page scheduling and other publishing functionality for a site collection.</Description>
63: <Status>Active</Status>
64: </Feature>
65: <Feature>
66: <FeatureId>fde5d850-671e-4143-950a-87b473922dc7</FeatureId>
67: <Title>Three-state workflow</Title>
68: <DisplayName>IssueTrackingWorkflow</DisplayName>
69: <Description>Use this workflow to track items in a list.</Description>
70: <Status>Inactive</Status>
71: </Feature>
72: </Features>