Friday, August 26, 2011

Reproducing STSADM Functionality in PowerShell – Enumallwebs

The other day I was on Twitter (@CraigToThePoint) having a debate with Joel Olson, Lori Gowin, Wictor Wilén, and others about whether or not STSADM is truly “deprecated” in SharePoint 2010. As you can imagine, I was of the opinion that any you can do with STSADM you can do with PowerShell.

image

Autocorrect aside, I was pretty confident in this statement. As a result of my confidence I was promptly challenged.

image

Still I was confident and accepted the challenge.

To keep a long story (took about 6 hours before I gave in) short, I was unable to get ALL of the functionality. What I WAS able to get is the equivalent of:

stsadm -o enumallwebs -includefeatures –includeeventreceivers

For those keeping score at home that is 2 out of the 4 available switches from the 2010 version of the STSADM command. It is important to note that I could have done them all but it would have required querying the database. While this is entirely possible and pretty easy from PowerShell, it is a no no for SharePoint.

The script below is what I came up with.

# Joel Olson Challenge
 
#Region Function Definition: EnumAllWebs
function EnumAllWebs
{
    param
    (
        [switch]$IncludeEventReceivers,
        [switch]$IncludeFeatures,
        [switch]$IncludeSetupFiles,
        [switch]$IncludeWebParts,
        [switch]$IncludeCustomListView,
        [string]$DatabaseName,
        [string]$DatabaseServer
    )
    #Region Function Definition: FindSiteTemplateReference
    # Checks to see if the specified template is installed in the farm
    # Returns null if template is not found, returns the template if it is
    function FindSiteTemplateReference
    {
        param
        (
            [int]$lcid,
            [string]$TemplateName
        )
        foreach ($template in $templates)
        {
            if (($template.LCID -eq $lcid) -and ($template.Name -eq $TemplateName))
            {
                return $template
            }
        }
        return $null
    }
    #EndRegion Function Definition: FindSiteTemplateReference
    #Region Function Definition: ProcessOneContentDatabase
    # Runs for each database
    function ProcessOneContentDatabase
    {
        param
        (
            [System.Xml.XmlTextWriter]$writer,
            [Microsoft.SharePoint.Administration.SPContentDatabase]$db,
            [bool]$IncludeFeatures,
            [bool]$IncludeWebParts,
            [bool]$IncludeSetupFiles,
            [bool]$IncludeEventReceivers,
            [bool]$IncludeCustomListView
        )
        #Region Function Definition: OutputSiteXml
        # Generates the XML for each site
        function OutputSiteXml
        {
            param
            (
                [System.Xml.XmlTextWriter]$writer,
                [Microsoft.SharePoint.SPSite]$site,
                [bool]$IncludeFeatures,
                [bool]$IncludeWebParts,
                [bool]$IncludeSetupFiles,
                [bool]$IncludeEventReceivers,
                [bool]$IncludeCustomListView
            )
            #Region Function Definition: OutputFeatureXml
            # Generates the XML for each feature
            function OutputFeatureXml
            {
                param
                (
                    [System.Xml.XmlTextWriter]$writer,
                    [string]$web
                )
                                
                # Only process features if there are any
                if ($web.Features.Count -gt 0)
                {
                    $writer.WriteStartElement("Features")
                    $features = $web.Features
                    foreach ($feature in $features)
                    {
                        $writer.WriteStartElement("Feature")
                        $writer.WriteAttributeString("Id", $feature.DefinitionId)
                        
                        # Check if feature is correctly installed
                        if ($feature.Definition -ne $null)
                        {
                            $definition = $feature.Definition
                            $writer.WriteAttributeString("DisplayName", $definition.DisplayName)
                            $writer.WriteAttributeString("InstallPath", $definition.RootDirectory)
                            $writer.WriteAttributeString("Status", "Installed")
                        }
                    
                        else
                        {
                            $writer.WriteAttributeString("Status", "Missing")
                        }
                        # End element for Feature node
                        $writer.WriteEndElement()
                    }
                    # End element for Features node
                    $writer.WriteEndElement()
                }
            }
            #EndRegion Function Definition: OutputFeatureXml
            #Region Function Definition: OutputEventReceiverXml
            # Generates the XML for each Event Receiver Assembly
            function OutputEventReceiverXml
            {
                param
                (
                    [System.Xml.XmlTextWriter]$writer,
                    [string]$web
                )
                # Only process event receivers if there are any
                if ($web.EventReceivers.Count -gt 0)
                {
                    $writer.WriteStartElement("EventReceiverAssemblies")
                    # Limit to unique assembly names
                    $evAssemblies = $web.EventReceivers | Select-Object -Unique Class
                    foreach ($evAssembly in $evAssemblies)
                    {
                        $assemblyString = $evAssembly.Class.ToString()
                        
                        # Check if event receiver is installed properly
                        try
                        {
                            if ([System.Reflection.Assembly]::LoadWithPartialName($assemblyString) -ne $null)
                            {
                                $status = "Installed"
                            }
                        }
                        catch [Exception]
                        {
                            $status = "Missing"
                        }
                        $writer.WriteStartElement("EventReceiverAssembly")
                        $writer.WriteAttributeString("Name", $assemblyString)
                        $writer.WriteAttributeString("Status", $status)
                        # End element for EventReceiverAssembly
                        $writer.WriteEndElement()
                    }
                    # End element for EventReceiverAssemblies
                    $writer.WriteEndElement()
                }
            }
            #EndRegion Function Definition: OutputEventReceiverXml
            #Region Function Definition: OutputWebPartXml
            function OutputWebPartXml
            {
                param
                (
                    [System.Xml.XmlTextWriter]$writer,
                    [string]$web
                )
                ## This would require running SQL queries directly against the database
            }
            #EndRegion Function Definition: OutputWebPartXml
            #Region Function Definition: OutputCustomListViewXml
            function OutputCustomListViewXml
            {
                param
                (
                    [System.Xml.XmlTextWriter]$writer,
                    [string]$web
                )
                ## This would require running SQL queries directly against the database
            }
            #EndRegion Function Definition: OutputCustomListViewXml
            #Region Function Definition: OutputSetupFileXml
            function OutputSetupFileXml
            {
                param
                (
                    [System.Xml.XmlTextWriter]$writer,
                    [string]$web
                )
                ## This would require running SQL queries directly against the database
            }
            #EndRegion Function Definition: OutputSetupFileXml
            $writer.WriteStartElement("Site")
            $writer.WriteAttributeString("Id", $site.ID)
            $writer.WriteAttributeString("OwnerLogin", $site.Owner.LoginName)
            # Check if it is a host header site collection
            if ($site.HostHeaderIsSiteName)
            {
                $writer.WriteAttributeString("HostHeader", $site.HostName)
            }
            if ($site.AllWebs.Count -gt 0)
            {
                $writer.WriteStartElement("Webs")
                $writer.WriteAttributeString("Count", $site.AllWebs.Count)
                foreach ($web in $site.AllWebs)
                {
                    try
                    {
                        $reference = FindSiteTemplateReference -lcid $web.Language -TemplateName "$($web.WebTemplate)#$($web.Configuration)"
                        # Check if web template is properly installed
                        if ($reference -eq $null)
                        {
                            $str = "Unknown"
                        }
                        elseif ($web.Configuration -eq -1)
                        {
                            $str = [string]::Empty
                        }
                        else
                        {
                            $str = "$($web.WebTemplate)#$($web.Configuration)"
                        }
                        $writer.WriteStartElement("Web")
                        $writer.WriteAttributeString("Id", $web.ID)
                        $writer.WriteAttributeString("Url", $web.Url)
                        $writer.WriteAttributeString("LanguageId", $web.Language)
                        # Handle cases where str var not set or set to empty string
                        if (($str -ne $null) -and ($str -ne [string]::Empty))
                        {
                            $writer.WriteAttributeString("TemplateName", $str);
                        }
                        if ($web.Configuration -ne -1)
                        {
                            $writer.WriteAttributeString("TemplateId", $web.WebTemplate);
                        }
                        # Process features
                        if ($includeFeatures)
                        {
                            OutputFeatureXml -writer $writer -web $web
                        }
                        # Process event reciever assemblies
                        if ($includeEventReceivers)
                        {
                            OutputEventReceiverXml -writer $writer -web $web
                        }
                        # Process web parts NOT USED!!!!
                        if ($includeWebParts)
                        {
                            OutputWebPartXml -writer $writer -web $web
                        }
                        # Process custom list views NOT USED!!!!
                        if ($includeCustomListView)
                        {
                            OutputCustomListViewXml -writer $writer -web $web
                        }
                        # Process setup files NOT USED!!!!
                        if ($includeSetupFiles)
                        {
                            OutputSetupFileXml -writer $writer -web $web
                        }
                        # End element for web
                        $writer.WriteEndElement()
                    }
                    catch [Exception]
                    {
                    
                    }
                    finally
                    {
                        $web.Dispose()
                    }
                }
                # End element for webs
                $writer.WriteEndElement()
            }
            # End element for site
            $writer.WriteEndElement()
        }
        #EndRegion Function Definition: OutputSiteXml
        $writer.WriteStartElement("Database")
        $writer.WriteAttributeString("SiteCount", $db.CurrentSiteCount)
        $writer.WriteAttributeString("Name", $db.Name)
        $writer.WriteAttributeString("DataSource", $db.ServiceInstance.NormalizedDataSource)
        # Only process sites if any exist
        if ($db.Sites.Count -gt 0)
        {
            $writer.WriteStartElement("Sites")
            foreach ($site in $db.Sites)
            {
                try
                {
                    OutputSiteXml -writer $writer -site $site -IncludeEventReceivers $includeEventReceivers -IncludeFeatures $includeFeatures -IncludeWebParts $includeWebParts -IncludeSetupFiles $includeSetupFiles -IncludeCustomListView $includeCustomListView
                }
                catch [Exception]
                {
                
                }
                finally
                {
                    $site.Dispose()
                }
            }
            # End element for sites
            $writer.WriteEndElement()
        }
        #End element for database
        $writer.WriteEndElement()
    }
    #EndRegion Function Definition: ProcessOneContentDatabase
    
    # Create bool vars for passing to sub functions
    if ($IncludeFeatures){ $blIncludeFeatures = $true} else { $blIncludeFeatures = $false}
    if ($IncludeSetupFiles){ $blIncludeSetupFiles = $true} else { $blIncludeSetupFiles = $false}
    if ($IncludeWebParts){ $blIncludeWebParts = $true} else { $blIncludeWebParts = $false}
    if ($IncludeCustomListView){ $blIncludeCustomListView = $true} else { $blIncludeCustomListView = $false}
    if ($IncludeEventReceivers){ $blIncludeEventReceivers = $true} else { $blIncludeEventReceivers = $false}
    $local = Get-SPFarm
    $services = New-Object Microsoft.SharePoint.Administration.SPWebServiceCollection $local
    $cs = [Microsoft.SharePoint.Administration.SPWebService]::ContentService
    $templates = Get-SPWebTemplate
    $StringWriter = New-Object System.IO.StringWriter
    $writer = New-Object System.XMl.XmlTextWriter $StringWriter
    $writer.Formatting = "indented"
    $writer.WriteStartElement("Databases")
    # if database was specifed process only that database
    if ($DatabaseName)
    {
        # if databaseserver was not specified use default
        if ($DatabaseServer -eq $null) { $DatabaseServer = $cs.DefaultDatabaseInstance.DisplayName }
        $db = Get-SPContentDatabase -DatabaseName $DatabaseName -DatabaseServer $DatabaseServer
        # Process database
        ProcessOneContentDatabase -writer $writer -db $db -includeFeatures $blIncludeFeatures -includeWebParts $blIncludeWebParts -includeSetupFiles $blIncludeSetupFiles -includeCustomListView $blIncludeCustomListView -includeEventReceivers $blIncludeEventReceivers
    }
    # otherwise process all databases
    else
    {
        foreach ($service in $services)
        {
            foreach ($application in $service.WebApplications)
            {
                foreach ($database2 in $application.ContentDatabases)
                {
                    # Process database
                    ProcessOneContentDatabase -writer $writer -db $database2 -includeFeatures $blIncludeFeatures -includeWebParts $blIncludeWebParts -includeSetupFiles $blIncludeSetupFiles -includeCustomListView $blIncludeCustomListView -includeEventReceivers $blIncludeEventReceivers
                }
            }
        }
    }
    # End element for databases
    $writer.WriteEndElement()
    # Return output as string
    $StringWriter.ToString()
    $StringWriter.Flush()
    $writer.Flush()
    
}
#EndRegion Function Definition: EnumAllWebs
# Call function
enumallwebs -IncludeEventReceivers -IncludeFeatures -IncludeSetupFiles -IncludeWebParts -IncludeCustomListView

Note: You will notice that the other parameters are still in there. They do not work but I wanted to show the full structure for those who are interested.


All in all what it comes down to is that until Microsoft decides to give us an API to get to some of these items they are out of reach via PowerShell while following best practices.


It hurts me to say it but: STSADM does still have its purposes.


Let me know what you think or if you have any questions on the script. I hope to dive deeper into some of the elements in later blog posts.


Thanks!

No comments: