Tag Archives: SharePoint

Creating SharePoint Site Collection through PowerShell CSOM

This is based on another blog post here: http://blog.scoreman.net/2013/02/create-site-collections-in-sharepoint-online-using-csom/. In that article the author shows how to use the CSOM with C# to create a Site Collection in Office 365.

This script is a near direct translation of that script into a PowerShell version of the code. I’ve also liberally taken inspiration from Chris O’Brien’s excellent series of posts on SharePoint PowerShell and CSOM here: http://www.sharepointnutsandbolts.com/2013/12/Using-CSOM-in-PowerShell-scripts-with-Office365.html

#Add the dlls required for working with Office 365
Add-Type -Path "C:\Program Files\Common Files\microsoft shared\Web Server Extensions\15\ISAPI\Microsoft.SharePoint.Client.dll"  
Add-Type -Path "C:\Program Files\SharePoint Online Management Shell\Microsoft.Online.SharePoint.PowerShell\Microsoft.Online.SharePoint.Client.Tenant.dll"

#URLs and prerequisites
$adminSiteUrl = "<Admin URL>"
$newsiteUrl = "<URL of Site Collection to Create>"
$username = "<username"
$password = Read-Host "Please enter your Password" -AsSecureString

Write-Host "Establishing Connection to Office 365."
#Get the context and feed in the credentials
$ctx = New-Object Microsoft.SharePoint.Client.ClientContext($adminSiteUrl) 
$credentials = New-Object Microsoft.SharePoint.Client.SharePointOnlineCredentials($username, $password)  
$ctx.Credentials = $credentials 

Write-Host "Now configuring the new Site Collection"

#Get the tenant object
$tenant = New-Object Microsoft.Online.SharePoint.TenantAdministration.Tenant($ctx)

#Set the Site Creation Properties values
$properties = New-Object Microsoft.Online.SharePoint.TenantAdministration.SiteCreationProperties
$properties.Url = $newsiteUrl
$properties.Template =  "STS#0"
$properties.Owner = $username
$properties.StorageMaximumLevel = 1000
$properties.UserCodeMaximumLevel = 300

#Create the site using the properties
$tenant.CreateSite($properties) | Out-Null

Write-Host "Creating site collection"
#Create the site in the tennancy
$ctx.ExecuteQuery()
Write-Host "Site Creation request completed. Note that the creation process is asynchronous and provisioning may take a short while."

I’ve tested this on Office 365 but haven’t tried it with On-Premise SharePoint 2013 so far.

SharePoint Surveys and the mystery of the missing partial response

Note: this applies to SharePoint 2010 and 2013, it is not viable for SharePoint online / Office 365.

Fun fact, SharePoint, if you’ve got branching logic in your survey, allows you to save your response to a survey!

This is great news. However on TechNet someone asked if it were possible to find these incomplete posts to remind the user that they haven’t finished?

Normally you’d assume that all you’d need to do is log in with a site collection admin account and you’d magically be able to see all the responses, even those that haven’t been completed. However in this case you’d be wrong. Even with Site Collection admin rights those incomplete responses are hidden from you.

Now that data is still in the SharePoint list and rather than mess about with permissions and settings, which could have unforeseen consequences, let’s see if we can’t pull it out of the list with PowerShell?

The first thing to do is see if we can get the real number of items from the survey. The easiest way to do that is to check the item count.

$web = Get-SPWeb "URL"
$survey = $web.Lists["SurveyName"]
Write-Host "Items in the survey : " $survey.ItemCount

That gives you the number of items, which you can compare to those visible. In my case I had one completed and one partial, leading to a total count of two. Which the script agreed with.

When you look at the object in PowerShell you can see that there’s a few fields that are different between a completed and a partial response. In this case we have two that are of interest.

Image showing different HasPublishedVersion and Level for two items.

The differences between a visible, completed item (1) and an incomplete item (2)

Of the two i’m going to use the ‘HasPublishedVersion’ field. Live the ‘Level’ field it will become true as long as a version has been completed, however unlike Level it will remain true even if the user starts to edit it later and somehow manages to do a partial save.

Let’s extend our script to list out the number in each group, then list the users who created them.

$web = Get-SPWeb "http://portal.tracy.com/sites/GD/"
$survey = $web.lists["Survey"]
$unPublishedEntries = $survey.Items | ? {-not $_.HasPublishedVersion}

Write-Host "Surveys in list: " $survey.ItemCount
Write-Host "Of which entries are incomplete: " $unPublishedEntries.Count

Foreach ($entry in $unPublishedEntries)
{
    Write-Host "User: {0} has not completed their entry" -f $entry["Created By"]
}

And voila, my results give us:

PowerShell results showing the results from a script

Printout from script

Cataloging Choice Columns

It’s been a while but it’s time for a new post.

Someone asked on TechNet () how to get a summary of all the custom choice columns and the options they have. I didn’t have the time to put it together at that moment but I thought It’d be a good exercise.

This will work for 2010 or 2013 but will not work for Office 365 or SharePoint online. For that a different approach would be needed.

“I inherited a SharePoint 2010 Foundation site that contains about 40 custom columns and about 10 of those custom columns are of the type “Choice”. Is there a way using Powershell or something else to export to a .csv file a list of the custom columns and if they are the type “choice” to show the list of what the various choices are for each column?”

So, let’s assume that we’re only interested in the site columns. To do that we’ll have to grab the SPWeb object and loop through the columns there that are of the appropriate type and list them out.

#Get the web
$web = Get-SPWeb "http://sharepoint.domain.com/sites/sitecollection/subsite"

#Get the columns (note, these are named fields)
$Columns = $web.Fields | ? { $_.TypeAsString -eq "Choice"}

#Print out the number of columns
Write-Host "Number of columns found: " $Columns.Count

#Loop through each choice and print the name
foreach ($entry in $columns)
{
Write-Host ("Field Name: {0}" -f $entry.InternalName)
#Loop through the choices and print those out
foreach ($choice in $entry.Choices)
{
Write-Host ("  Choice: {0}" -f $choice)}
}

From here

That’ll list out the columns to the screen but it’s not a great solution. It’s printing out too many columns, it’s also just printing them to the screen. We need it to serialise this into a format that we can use.

Let’s start with serialisation.

There’s loads of ways to do this but my preference is to create custom objects to contain the information we collect, then assign them to an array which we can process later.

For a quick guide to PSObjects have a look here
So, after changing the write-hosts to write verbose and putting in our custom objects we get this!

Add-PSSnapin Microsoft.SharePoint.PowerShell -ea SilentlyContinue

#Get the web
$web = Get-SPWeb "http://portal.tracy.com/sites/cthub"

#Get the columns (note, these are named fields) 
$Columns = $web.Fields | ? { $_.TypeAsString -eq "Choice"}

#Print out the number of columns
Write-Host "Number of columns found: " $Columns.Count

#Create empty array
$ColumnDetailsArray = @()

#Loop through each choice column and create an object to hold it
foreach ($entry in $columns)
{
    $choicesArray = @()
    Write-Verbose ("Field Name: {0}" -f $entry.InternalName)
    
    #Loop through the choices and print those out
    foreach ($choice in $entry.Choices)
    {
        #Add each choice to the (local) array
        Write-Verbose ("  Choice: {0}" -f $choice)
        $choicesArray += $choice
    }
    #Create a result object to hold our data
    $ColumnDetailsArray += New-Object PSObject -Property @{
                        "Name" = $entry.InternalName
                        "Choices" = $choicesArray
                        }
}

Which actually makes things worse as we no longer get any results! Let’s add that in some xml work. I’m still not entirely happy with the way PowerShell and XML work together so this example is a bit clunky but it works.

#Create a starter XML for the system to work with
[xml] $xml = "<root><summary rundate='{0}' web='{1}'/></root>" -f 
    (Get-Date -f ddMMyyyy), 
    $web.Title

#loop through the results and genearate an xml object
foreach ($column in $ColumnDetailsArray)
{
    #Create an element to hold the top level item
    $columnElement = $xml.CreateElement("Choice")
    $columnElement.SetAttribute("Name", $column.Name) 

    #Loop through the choices and add entries for each
    foreach ($choice in $column.Choices)
    {
        $choiceElement = $xml.CreateElement("Choice")
        
        #Note that you need this Pipe Out-Null to prevent it writing to the console
        $choiceElement.InnerText = $choice
        $columnElement.AppendChild($choiceElement) | Out-Null
    }
    #Once it's built add the element to the root node
    $xml.root.AppendChild($columnElement)  | Out-Null
}
$xml.Save("C:\ResultFolder\ColumnSummary.xml")

So, this now dumps out the data we’ve asked for but it also dumps out all the pre-packaged columns. This is a problem that isn’t easily fixed, there isn’t an ‘OOTB’ flag on fields but there are a few we can use to filter them out.

If we grab a column from the results and run Get-Members on it there are a couple of fields that should be useful for filtering the results:

Sealed – This shows if the column is not supposed to be edited by human hands. Note that this could give false negatives in scenarios where a column has been deployed via the CTHub which I think seals columns (it definitely seals Content Types) as it pushes to consuming site collections

Hidden – Not relevant in this case but often handy. In this case we’ll filter out groups that are part of the ‘_hidden’ group earlier.

So if we now add that criteria to the earlier $columns filtering process we get

$Columns = $web.Fields | ? { $_.TypeAsString -eq "Choice" -and 
        -not $_.Sealed -and $_.Group -ne "_Hidden"
    }

But that’s still not perfect, so instead of filtering the terms out right now let’s make it a bit more useful first. When you look at columns in SharePoint they are organised in Groups. We can see those properties in PowerShell and group our elements using that field.

Add-PSSnapin Microsoft.SharePoint.PowerShell -ea SilentlyContinue

#Get the web
$web = Get-SPWeb "http://portal.tracy.com/sites/cthub"

#Get the columns (note, these are named fields) 
#Also filter out the sealed fields
$Columns = $web.Fields | ? { $_.TypeAsString -eq "Choice" -and 
        -not $_.Sealed -and $_.Group -ne "_Hidden"
    }


#Print out the number of columns
Write-Host "Number of columns found: " $Columns.Count

#Create empty array
$ColumnDetailsArray = @()

#Loop through each choice column and create an object to hold it
foreach ($entry in $columns)
{
    $choicesArray = @()
    Write-Verbose ("Field Name: {0}" -f $entry.InternalName)
    
    #Loop through the choices and print those out
    foreach ($choice in $entry.Choices)
    {
        #Add each choice to the (local) array
        Write-Verbose ("  Choice: {0}" -f $choice)
        $choicesArray += $choice
    }
    #Create a result object to hold our data
    $ColumnDetailsArray += New-Object PSObject -Property @{
                        "Name" = $entry.InternalName
                        "Group" = $entry.Group
                        "Choices" = $choicesArray
                    }
}

#Create a starter XML for the system to work with
[xml] $xml = "<root><summary rundate='{0}' web='{1}'/></root>" -f 
    (Get-Date -f ddMMyyyy), 
    $web.Title

#Get a unique list of the groups in use in the site
foreach ($group in $ColumnDetailsArray | select Group -Unique)
{
    $groupText = $group.Group
    Write-Host "Group name: " $groupText
    $groupElement = $xml.CreateElement("Group")
    $groupElement.SetAttribute("Name", $groupText)
    
    #loop through the results and add them to the xml object
    foreach ($column in $ColumnDetailsArray | ? {$_.Group -eq $groupText})
    {
        #Create an element to hold the top level item
        $columnElement = $xml.CreateElement("Choice")
        $columnElement.SetAttribute("Name", $column.Name)

        #Loop through the choices and add entries for each
        foreach ($choice in $column.Choices)
        {
            $choiceElement = $xml.CreateElement("Choice")
        
            #Note that you need this Pipe Out-Null to prevent it writing to the console
            $choiceElement.InnerText = $choice
            $columnElement.AppendChild($choiceElement) | Out-Null
        }
        #Once it's built add the element to the root node
        $groupElement.AppendChild($columnElement)  | Out-Null
    }
    $xml.root.AppendChild($groupElement)  | Out-Null
}
$xml.Save("C:\ResultFolder\ColumnSummary.xml")

Of course once you have the group name you can filter those options out by using a blacklist of groups to avoid reporting on.


Add-PSSnapin Microsoft.SharePoint.PowerShell -ea SilentlyContinue

#BlackList Group Names
#These are the known groups you get in a non publishing team site:
$blackList = @(
    "_Hidden",                                                                         
    "Base Columns",                                                                    
    "Content Feedback",                                                   
    "Core Contact and Calendar Columns",                                               
    "Core Document Columns",                                                         
    "Core Task and Issue Columns",                                                          
    "Custom Columns",                                                            
    "Display Template Columns",                                                          
    "Document and Record Management Columns",                                                
    "Enterprise Keywords Group",                                                             
    "Extended Columns",                                                             
    "JavaScript Display Template Columns",                                                   
    "Reports",                                                                  
    "Status Indicators"
)

#Get the web
$web = Get-SPWeb "http://portal.tracy.com/sites/cthub"

#Get the columns (note, these are named fields) 
#Also filter out the sealed fields
$Columns = $web.Fields | ? { $_.TypeAsString -eq "Choice" -and 
        -not $_.Sealed -and $_.Group -ne "_Hidden"
    }


#Print out the number of columns
Write-Host "Number of columns found: " $Columns.Count

#Create empty array
$ColumnDetailsArray = @()

#Loop through each choice column and create an object to hold it
foreach ($entry in $columns)
{
    $choicesArray = @()
    Write-Verbose ("Field Name: {0}" -f $entry.InternalName)
    
    #Loop through the choices and print those out
    foreach ($choice in $entry.Choices)
    {
        #Add each choice to the (local) array
        Write-Verbose ("  Choice: {0}" -f $choice)
        $choicesArray += $choice
    }
    #Create a result object to hold our data
    $ColumnDetailsArray += New-Object PSObject -Property @{
                        "Name" = $entry.InternalName
                        "Group" = $entry.Group
                        "Choices" = $choicesArray
                    }
}
#Create a starter XML for the system to work with
[xml] $xml = "<root><summary rundate='{0}' web='{1}'/></root>" -f 
    (Get-Date -f ddMMyyyy), 
    $web.Title

foreach ($group in $ColumnDetailsArray | select Group -Unique)
{
    $groupText = $group.Group

    #Check to see if the group name is in our blacklist
    if (-not $blackList.Contains($groupText))
    {
        Write-Verbose "Group name: " $groupText
        $groupElement = $xml.CreateElement("Group")
        $groupElement.SetAttribute("Name", $groupText)
    
        #loop through the results and genearate an xml
        foreach ($column in $ColumnDetailsArray | ? {$_.Group -eq $groupText})
        {
            #Create an element to hold the top level item
            $columnElement = $xml.CreateElement("Choice")
            $columnElement.SetAttribute("Name", $column.Name)

            #Loop through the choices and add entries for each
            foreach ($choice in $column.Choices)
            {
                $choiceElement = $xml.CreateElement("Choice")
        
                $choiceElement.InnerText = $choice
                #Note that you need this Pipe Out-Null to prevent it writing to the console
                $columnElement.AppendChild($choiceElement) | Out-Null
            }
            #Once it's built add the element to the root node
            $groupElement.AppendChild($columnElement)  | Out-Null
        }
        $xml.root.AppendChild($groupElement)  | Out-Null
    }
    else
    {
        Write-Verbose "Group skipped:" $groupText
    }
}
$xml.Save("C:\ResultFolder\ColumnSummary.xml")

And there you have it. A working report that will summarise all custom choice columns in a SPWeb object and save them in an XML file.

Automating SharePoint Search Testing

I was browsing technet, as you do, when i found this comment on Search best practices:

We recommend that you test the crawling and querying functionality of the server farm after you make configuration changes or apply updates

http://technet.microsoft.com/en-us/library/cc850696(v=office.14).aspx

This chimed with me as a client I worked with had failed to do this and had paid the price when their Production search service went down for a day.

The article continues:

An easy way to do this is to create a temporary content source that is used only for this purpose. To test, we recommend that you crawl ten items — for example .txt files on a file share — and then perform search queries for those files. Make sure that the test items are currently not in the index. It is helpful if they contain unique words that will be displayed at the top of the search results page when queried. After the test is complete, we recommend that you delete the content source that you created for this test. Doing this removes the items that you crawled from the index and those test items will not appear in search results after you are finished testing

To put that in bullet point format:

  1. Test the search system doesn’t already have test content
  2. Create some test content to search
  3. Create a new content source
  4. Crawl the test content
  5. Search for the test content
  6. Check that the test content is there
  7. Remove the test content by blowing away the content source
  8. Confirm it’s no longer there

It’s a good test script. It also breaks down nicely into some bullet points. With a bit of thought we can break this down into some simple tasks:

  1. Run a search and test the results
  2. Create some files
  3. Create a new content source
  4. Crawl the test content
  5. Run a search and test the results
  6. Remove the test content by blowing away the content source
  7. Run a search and test the results

The common aspect is to run a search three times and test the results. Of course the results will, hopefully, vary depending on when we run that test but we can manage that.

Let’s go with the big one. Running a search:

Function Check-TestPairValue ()
{
<#
.DESCRIPTION
Takes a pipeline bound collection of test values and search terms and searches for
them using the searchPageURL.
Returns either 'Present' or 'Not Found' depending on the result.
Not currently production grade#> 
    [CmdletBinding()]
    Param (
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]$testPair,
    [Parameter(Mandatory=$true)]$searchPageURL
     )
    BEGIN{
        #Create the IE window in the begin block so if the input is pipelined we don't have to re-open it each time.
        $ie = New-Object -com "InternetExplorer.Application"
        $ie.visible = $true
    }
    PROCESS
    { 
        #Get the test value and the search term from the pair
        $testValue = $testPair[0]
        $searchTerm = $testPair[1]
        
        #Open the navigation page
        $ie.navigate($searchPageURL)

        #Wait for the page to finish loading
        while ($ie.readystate -ne 4)
        {
            start-sleep -Milliseconds 100
        }
        Write-Verbose "Page loaded"
        
        #Get the search box
        $searchTextBoxID = "ctl00_m_g_2f1edfa4_ab03_461a_8ef7_c30adf4ba4ed_SD2794C0C_InputKeywords"
        $document = $ie.Document
        $searchBox = $document.getElementByID($searchTextBoxID)

        #enter the search terms
        $searchBox.innerText = $searchTerm
        Write-Verbose "Searching for: $searchTerm - Expected result: $testValue"    
        
        #Get the search button
        $searchButtonID = "ctl00_m_g_2f1edfa4_ab03_461a_8ef7_c30adf4ba4ed_SD2794C0C_go"
        
        #Run the search
        $btn = $document.getElementByID($searchButtonID)
        $btn.click()
        
        #Wait for the results to be loaded
        while ($ie.locationurl -eq $searchPageURL)
        {
           start-sleep -Milliseconds 100
        }
        
        Write-Verbose "Left the page, waiting for results page to load"
        #Wait for the results page to load
        while ($ie.readystate -ne 4)
        {
            start-sleep -Milliseconds 100
        }
        Write-Verbose "Results page loaded"
        #Once loaded check that the results are correct
        $document = $ie.document

        #Check that the search term results contain the test results:
        $firstSearchTermID = "SRB_g_9acbfa38_98a6_4be5_b860_65ed452b3b09_1_Title"
        $firstSearchResult = $document.getElementByID($firstSearchTermID)
        
        $result =""
        
        #test that the title of the file is equal to the search result
        If ($firstSearchResult.innerHTML -match $testValue)
        {
            $result ="Present"
        }
        else
        {
            $result ="Not Found"
        }
        
        Write-Verbose "Test $result"
        
        #Create a new PS Object for our result and let PowerShell pass it out.
        New-Object PSObject -Property @{
            TestCase = $searchTerm
            Result = $result
        }    
    }
    END {
        #Close the IE window after us
        $ie.Quit()
    }
}

Well to be honest that’s the only tricky bit in the process. From there on in it’s plumbing.
We create some test files:


Function Create-SPTestFiles ()
{
[CmdletBinding()]
Param(
    $filesToCreate,
    [string]$folderPath
    )
    
    If (!(Test-Path $folderPath))
    {
    	#Folder doesn't exist.
        Write-Verbose "Folder does not exist - attempting to create"
    	New-Item $folderPath -type directory
    }

    #if the files don’t exist. Create them
    Foreach ($file in $filesToCreate)
    {
         $fileName = $file[0]
    	$filePath = $folderPath + "\" + $fileName
    	If (Test-Path $filePath)
        {
            Write-Verbose "File $fileName already exists. Skipping"
            Write-EventLog -LogName "Windows PowerShell" -Source "PowerShell" -EventId 103 -EntryType Error -Message "Test content already present."
        }
        else
    	{
            Write-Verbose "Creating $fileName"
    		$file[1] >> $filePath
    	}
    }
    Write-Verbose "All files created"
}

We create a content source (this function isn’t perfect here but i’m stealing it from another script)


Function Ensure-TestContentSourceExists ()
{
    [CmdletBinding()]
    Param(
    $sa,
    [string]$contentSourceName,
    [string]$filePath
    )
    $testCS = $sa | Get-SPEnterpriseSearchCrawlContentSource | ? {$_.Name -eq $contentSourceName}
    if ($testCS)
    {
        Write-Verbose "Content Source $contentSourceName already exists"
    }
    else
    {
        Write-Verbose "Content Source $contentSourceName does not exist, creating"
        New-SPEnterpriseSearchCrawlContentSource -SearchApplication $sa -Type file -name $contentSourceName -StartAddresses $filePath | Out-Null
        $testCS = $sa | Get-SPEnterpriseSearchCrawlContentSource | ? {$_.Name -eq $contentSourceName}
    }
    #Output the content source
    $testCS
}

Run the crawl and wait for it to finish.


Function Run-TestCrawl ()
{
    [CmdletBinding()]
    Param ($contentSource)
    #Run a crawl for that content source
    $contentSource.StartFullCrawl()

    #Set a flag to allow us to abort if the duration is excessive
    $stillNotStupidDuration = $true
    $startTime = Get-Date
    $crawlTimeout = 5
    $crawlInitalTime = 2

    Write-Verbose "Starting crawl. Waiting for 2 minutes (Default SharePoint minimum search duration)"
    Sleep -Seconds 120
    #Wait for it to finish
    while ($contentSource.CrawlStatus -ne "Idle" -AND $stillNotStupidDuration -eq $true)
    {
        Write-Verbose "Crawl still running at $timeDifference, waiting 10 seconds"
        Sleep -Seconds 10
        $timeDifference = (Get-Date) - $startTime
        if ($timeDifference.Minutes -gt $crawlTimeout)
        {
            $stillNotStupidDuration = $false
        }
        
    }
    Write-Verbose "Crawl complete"
}

Then we’re back to searching and clean up! Easy.

Of course there’s a little bit more plumbing to be done to stick it all together: so here’s a fully functioning script.

Param (

    #Name of the search service application to test
    $searchAppName = "Search Service Application",

    #Path to the shared folder
    #NOTE: THIS HAS TO BE SETUP BEFORE RUNNING THE SCRIPT MANUALLY (It can be scripted but i haven't)
    $fileSharePath = "\\spintdev\TestFolder",


    #The search page
    $searchSiteURL = "http://sharepoint/sites/search/Pages/default.aspx",

    #Start generating the report
    $reportFolder = "C:\AutomatedTest",
   
    #Flag to set or reject verbose output
    $printVerbose = $false
)


Add-PSSnapin Microsoft.SharePoint.PowerShell -ea SilentlyContinue


Function Process-ASTPassFail ()
{
<#Internal helper function. Will be used for reporting#>
Param($collectionThatShuldBeEmpty,
    $failText,
    $passText
    )

    if ($collectionThathouldBeEmpty -ne $null)
     {
        Write-Warning $failText
        Write-EventLog -LogName "Windows PowerShell" -Source "PowerShell" -EventId 102 -EntryType Error -Message $failText
        $thisTestText = $failText + "`n"
    }
    else
    {
        $sucsessText =  $passText
        Write-Host $sucsessText
        Write-EventLog -LogName "Windows PowerShell" -Source "PowerShell" -EventId 102 -EntryType Information -Message $passText
        $thisTestText = $passText + "`n"
    }
    $thisTestText
}


Function Create-ASTFiles ()
{
<#Creates sometest files for us to search later#>
[CmdletBinding()]
Param(
    $filesToCreate,
    [string]$folderPath
    )
    
    If (!(Test-Path $folderPath))
    {
    	#Folder doesn't exist.
        Write-Verbose "Folder does not exist - attempting to create"
    	New-Item $folderPath -type directory
    }

    #if the files don’t exist. Create them
    Foreach ($file in $filesToCreate)
    {
        $fileName = $file[0]
    	$filePath = $folderPath + "\" + $fileName
    	If (Test-Path $filePath)
        {
            Write-Verbose "File $fileName already exists. Skipping"
            Write-EventLog -LogName "Windows PowerShell" -Source "PowerShell" -EventId 103 -EntryType Error -Message "Test content already present."
        }
        else
    	{
            Write-Verbose "Creating $fileName"
    		$file[1] >> $filePath
    	}
    }
    Write-Verbose "All files created"
}

Function Test-ContentSourceCountAcceptable()
{
[CmdletBinding()]
Param($searchServiceApplication)
    
    #Check the maximum number of content sources allowed
    #http://technet.microsoft.com/en-us/library/cc262787(v=office.14).aspx
    $maxContentSources = 50
    
    $ContentSources = $sa | Get-SPEnterpriseSearchCrawlContentSource

    #Lazy way to check if there is only one item (note, also works for none)
    if ($ContentSources.Count -ne $null)
    {
        $CTSourceCount = $ContentSources.Count
    }
    else
    {
        #Note that this might be wrong if there are no CTs. Not a problem here but it's not a rigourous number
        $CTSourceCount = 1
    }

    #if it is below the limit. Stop and throw an error
    if ($count -ge $maxContentSources)
    {
        #Throw error and let slip the dogs of war
        Write-Verbose "Warning content type count is higher than Microsoft Boundaries"
        $false
    }
    else
    {
        #If we're under the MS limit then return true
        $true
    }
}

Function Ensure-ASTContentSourceExists ()
{
<#Check if conent source already exists. This should be re-written to delete it but for development purposes this is more efficient#>
    [CmdletBinding()]
    Param(
    $sa,
    [string]$contentSourceName,
    [string]$filePath
    )
    $testCS = $sa | Get-SPEnterpriseSearchCrawlContentSource | ? {$_.Name -eq $contentSourceName}
    if ($testCS)
    {
        Write-Verbose "Content Source $contentSourceName already exists. Deleting it."
        Write-EventLog -LogName "Windows PowerShell" -Source "PowerShell" -EventId 100 -EntryType Warning -Message "Unable to create a Content Source as one already exists"
        $testCS.Delete()     
    }
    else
    {
        Write-Verbose "Content Source $contentSourceName does not exist, creating"
        New-SPEnterpriseSearchCrawlContentSource -SearchApplication $sa -Type file -name $contentSourceName -StartAddresses $filePath | Out-Null
        $testCS = $sa | Get-SPEnterpriseSearchCrawlContentSource | ? {$_.Name -eq $contentSourceName}
    }
    #Output the content source - Note that this could result in an error state as a pre-existing one might be re-used.
    $testCS
}

Function Run-ASTCrawl ()
{
<#
.SYNOPSIS
Runs a crawl for a content source and waits until it is complete.
.DESCRIPTION
Runs a crawl for a content source and waits for it to complete, features abort option that will exit the function if the crawl takes too long.
#>
    [CmdletBinding()]
    Param (
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]$contentSource,
    [Parameter(Mandatory=$false,ValueFromPipeline=$false)]$crawlTimeOut = 5
    )
    #Run a crawl for that content source
    $contentSource.StartFullCrawl()

    
    #Start the stopwatch, Note: replace with stopwatch.
    $startTime = Get-Date
    
    #Inital pause time under which there is no point checking for the crawl to be complete
    $crawlInitalTime = 120
    
    #Set a flag to allow us to abort if the duration is excessive
    $stillNotStupidDuration = $true

    Write-Verbose "Starting crawl. Waiting for $crawlInitalTime seconds (Default SharePoint minimum search duration)"
    Sleep -Seconds $crawlInitalTime
    #Wait for it to finish
    while ($contentSource.CrawlStatus -ne "Idle" -AND $stillNotStupidDuration -eq $true)
    {
        Write-Verbose "Crawl still running at $timeDifference, waiting 10 seconds"
        Sleep -Seconds 10
        $timeDifference = (Get-Date) - $startTime
        if ($timeDifference.Minutes -gt $crawlTimeout)
        {
            $stillNotStupidDuration = $false
        }
        
    }
    if ($stillNotStupidDuration)
    {
        Write-Verbose "Crawl complete"
    }
    else
    {
        Write-Warning "No longer waiting for process to complete. Search not finished, results will be unpredictable"
        Write-EventLog -LogName "Windows PowerShell" -Source "PowerShell" -EventId 103 -EntryType Critical -Message "Crawler took longer than the timeout value of $crawlTimeOut so the function exited early."
    }
}


Function Check-ASTPairValue ()
{
<#
.SYNOPSIS
Tests that a search term returns a file with the appropriate name
.DESCRIPTION
Takes a pipeline bound pair of test values and search terms and searches for
them using the searchPageURL page.
Returns either 'Present' or 'Not Found' depending on the result.
.EXAMPLE
$testContent | Check-ASTPairValue -searchPageURL $searchSiteURL 
#> 
    [CmdletBinding()]
    Param (
    [Parameter(Mandatory=$true,ValueFromPipeline=$true)]$testPair,
    [Parameter(Mandatory=$true)]$searchPageURL
     )
    BEGIN{
        #Create the IE window in the begin block so if the input is pipelined we don't have to re-open it each time.
        $ie = New-Object -com "InternetExplorer.Application"
        $ie.visible = $true
    }
    PROCESS
    { 
        #Get the test value and the search term from the pair
        $testValue = $testPair[0]
        $searchTerm = $testPair[1]
        
        #Open the navigation page
        $ie.navigate($searchPageURL)

        #Wait for the page to finish loading
        while ($ie.readystate -ne 4)
        {
            start-sleep -Milliseconds 100
        }
        Write-Verbose "Page loaded"
        
        #Get the search box
        $searchTextBoxID = "ctl00_m_g_2f1edfa4_ab03_461a_8ef7_c30adf4ba4ed_SD2794C0C_InputKeywords"
        $document = $ie.Document
        $searchBox = $document.getElementByID($searchTextBoxID)

        #enter the search terms
        $searchBox.innerText = $searchTerm
        Write-Verbose "Searching for: $searchTerm - Expected result: $testValue"    
        
        #Get the search button
        $searchButtonID = "ctl00_m_g_2f1edfa4_ab03_461a_8ef7_c30adf4ba4ed_SD2794C0C_go"
        
        #Run the search
        $btn = $document.getElementByID($searchButtonID)
        $btn.click()
        
        #Wait for the results to be loaded
        while ($ie.locationurl -eq $searchPageURL)
        {
           start-sleep -Milliseconds 100
        }
        
        Write-Verbose "Left the page, waiting for results page to load"
        #Wait for the results page to load
        while ($ie.readystate -ne 4)
        {
            start-sleep -Milliseconds 100
        }
        Write-Verbose "Results page loaded"
        #Once loaded check that the results are correct
        $document = $ie.document

        #Check that the search term results contain the test results:
        $firstSearchTermID = "SRB_g_9acbfa38_98a6_4be5_b860_65ed452b3b09_1_Title"
        $firstSearchResult = $document.getElementByID($firstSearchTermID)
        
        $result =""
        
        #test that the title of the file is equal to the search result
        If ($firstSearchResult.innerHTML -match $testValue)
        {
            $result ="Present"
        }
        else
        {
            $result ="Not Found"
        }
        
        Write-Verbose "Test $result"
        
        #Create a new PS Object for our result and let PowerShell pass it out.
        New-Object PSObject -Property @{
            TestCase = $searchTerm
            Result = $result
        }    
    }
    END {
        #Close the IE window after us
        $ie.Quit()
    }
}

######################################################################################
#Execution script begins here
######################################################################################


#Generate the output file location
$reportFilePath = $reportFolder+  "\SearchTest_Results_" + (Get-Date -Format "dd_MM_yyyy") + ".txt"

#Name of the search service application to test
$searchAppName = "Search Service Application"

#Path to the shared folder
#NOTE: THIS HAS TO BE SETUP BEFORE RUNNING THE SCRIPT MANUALLY (It can be scripted but i haven't)
$fileSharePath = "\\spintdev\TestFolder"


#The search page
$searchSiteURL = "http://sharepoint/sites/search/Pages/default.aspx"

#Start generating the report
$reportFilePath = "C:\AutomatedTest\SearchTest_Results_" + (Get-Date -Format "dd_MM_yyyy") + ".txt"



#All items from here on in are internal and do not have to be specified or modified unless you wish it.

#test content - deliberately junk and non sensical rubbish to trim down search results and avoid false negatives.
#Note: I have no particular insight or interest in the dietry foibles of the politicans listed below.
$testContent = @(
    ("FileA.txt","Miliband loves pie"),
    ("FileB.txt","Osbourne despises soup"),
    ("FileC.txt","Cameron tolerates beans"),
    ("FileD.txt","Clegg loathes eggs which is ironic"),
    ("FileE.txt","Benn likes red meat"),
    ("FileF.txt","Balls desires flan"),
    ("FileG.txt","Cable adores sandwiches"),
    ("FileH.txt","Hunt regrets cake")
)

#Junk content for an additional test to exclude false positive results
$itemToConfirmFailure =@(
"sdkfslskjladsflkj", "lflkfdskjlfdskjfdslkjf"
"sdkfslsfdjklfkjladsflkj", "lflskjfdslkjf"
)

#Only used internally.
$testCTName = "TestSearchContentType"

$startDateTime = Get-Date
$currentComputerName = $env:computername

#Header info
"Automated SharePoint Search Testing Results`n" >> $reportFilePath
"Test started at $startDateTime on Computer $currentComputerName" >> $reportFilePath
        
#Write the first test to the report
"Test 1 - Confirm search terms do not retrieve values `n" >> $reportFilePath
"Confirms that there are no files that can generate a false positive in the system.`n" >> $reportFilePath

Write-Host "Starting tests, checking that there is no pre-existing content that might cause false positives"
 
#Run a search for the testcontent
$deliberatelyFailedResults = @()
$deliberatelyFailedResults +=  $testContent | Check-ASTPairValue -searchPageURL $searchSiteURL -Verbose:$printVerbose
$falsePositives = $deliberatelyFailedResults | ? {$_.Result -eq "Present"}

$errorText = "Test failed, files found by search engine. Results not reliable"
$sucsessText =  "Test Passed, moving to next stage"

$testText = (Process-ASTPassFail -collectionThatShuldBeEmpty $falsePositives -passText $sucsessText -failText $errorText)
$testText >> $reportFilePath 
#Create the test files based on the array above
Create-ASTFiles -filesToCreate $testContent -folderPath $fileSharePath

#Get the search app
$sa = Get-SPEnterpriseSearchServiceApplication -Identity $searchAppName
if ($sa -eq $null)
{
    Write-EventLog -LogName "Windows PowerShell" -Source "PowerShell" -EventId 101 -EntryType Error -Message "Could not find search application $searchAppName"
}


Write-Host "Checking that we are within guidelines for number of Content Sources"
#Test the number of content sources already in place
$numberOfContentSourcesBelowThreshold = Test-ContentSourceCountAcceptable -searchServiceApplication $sa -Verbose:$printVerbose

#Only progress if we're not going to breach the content type limit.
if ($numberOfContentSourcesBelowThreshold)
{
    Write-Host "Within the Acceptable number of Site Collections"
    #Get the content source.
    $testCS = Ensure-ASTContentSourceExists -sa $sa -contentSourceName $testCTName -filePath $fileSharePath -Verbose:$printVerbose
    
    Write-Host "Running the crawl - estimated completion in approximately 2 minutes"
    #Run the crawl and wait for it to complete
    Run-ASTCrawl -contentSource $testCS -Verbose:$printVerbose

    $searchResults = @()
    Write-Host "Crawl Complete, testing links"
    
    $searchResults += $testContent | Check-ASTPairValue -searchPageURL $searchSiteURL -Verbose:$printVerbose
    $failures = $deliberatelyFailedResults | ? {$_.Result -ne "Present"}
    
    #Write the  test to the report
    "Test 2 - Test new content`n" >> $reportFilePath
    "Confirms that search works for our new content.`n" >> $reportFilePath
    
    $errorText = "Test failed, files were not found"
    $sucsessText =  "Passed main test."
    $failures += (Process-ASTPassFail -collectionThatShuldBeEmpty $falsePositives -passText $sucsessText -failText $errorText)

    #Confirm that the test will fail given junk input.
    $deliberatelyFailedResults = @()
    $deliberatelyFailedResults +=  $itemToConfirmFailure | Check-ASTPairValue -searchPageURL $searchSiteURL -Verbose:$printVerbose
    $falsePositives = $deliberatelyFailedResults | ? {$_.Result -eq "Present"}
    
    #Write the  test to the report
    "Test 3 - Check for junk terms `n" >> $reportFilePath
    "Confirms that search doens't find some junk values.`n" >> $reportFilePath
    
    $errorText = "Test failed, files found by search engine when given junk data"
    $sucsessText =  "Passed confirmation test - junk values not found"
    $testText = (Process-ASTPassFail -collectionThatShuldBeEmpty $falsePositives -passText $sucsessText -failText $errorText)
    $testText >> $reportFilePath 
    
    
    #Clean up the content source 
    $CSToDelete = $sa | Get-SPEnterpriseSearchCrawlContentSource | ? {$_.Name -eq $testCTName}
    $CSToDelete.Delete()
    
    #Delete the files
    foreach ($combo in $testContent)
    {
        $fileName = $combo[0]
        $file = Get-ChildItem -Path $fileSharePath | ? {$_.name -eq $fileName}
        $file.Delete()
    }
    #Note that the content source may take a minute to be deleted
    Write-Host "Pausing for 1 minute to allow the index to update"
    Sleep -Seconds 60   

    #Run a search for the testcontent
    $deliberatelyFailedResults = @()
    $deliberatelyFailedResults +=  $testContent | Check-ASTPairValue -searchPageURL $searchSiteURL -Verbose:$printVerbose
    $falsePositives = $deliberatelyFailedResults | ? {$_.Result -eq "Present"}

    #Write the  test to the report
    "Test 3 - Confirm search terms are removed`n" >> $reportFilePath
    "Confirms that the test search content is removed from the system.`n" >> $reportFilePath
    
    $errorText = "Test failed, files found by search engine when given junk data"
    $sucsessText =  "Passed confirmation test. Test files are not present"
    $testText = (Process-ASTPassFail -collectionThatShuldBeEmpty $falsePositives -passText $sucsessText -failText $errorText)
    $testText >> $reportFilePath 
}
else
{
    $errorText = "Error - Unable to create a Content Source as the total number of Content Sources is greater than the Microsoft boundary"
    Write-EventLog -LogName "Windows PowerShell" -Source "PowerShell" -EventId 100 -EntryType Warning -Message $errorText
    $errorText >> $reportFilePath 
}

"Automated SharePoint Search Testing Completed at $(Get-Date) `n" >> $reportFilePath 

So there we have it. A fully functioning automated testing process for SharePoint Search. It would be nice if it sent an email but i’m planning on rolling this into some SCOM work i’m playing with.

I haven’t tested this on 2013 yet, it’d need at least some tweaks to field IDs and maybe more structural work to get the Search API right for 2013. If anyone is interested i’ll knock up a new version.

Managed Metadata columns fail to sync between SharePoint and client applications

This issue seems to be cropping up a lot at the moment, one possible fix is below.

Symptoms:

When you set a Managed Metadata Service (MMS) column in SharePoint these values are pushed down to the office document and will be visible on the Document Information Panel (DIP). When these values are changed in an office document however these MMS column changes are not updated in the SharePoint item. Non MMS fields (i.e. Single Line of Text, Choice, Number etc.) are correctly synced. If you close and re-open the office document, even from another computer, any changes made in office to the MMS values will still remain as you set them in the DIP. However as normal any changes to the values in SharePoint will be pushed down to the office document overwriting any values in the DIP.

In summary: SharePoint can write to the office document but MMS values in the document cannot be written to SharePoint by office.

Note: If text, choice or other non MMS fields are not being synced when you save the document then this is probably unrelated to your issue.

Where has this been seen:

We’ve seen it in at least two SharePoint 2010 SP1 environments in the last week, with farms using varying CUs. No obvious cause has been identified.
The main example is in office, at least word and Excel. This has also been seen with Harmon.ie where it is impossible to set the MMS value, it is probable other systems may be effected.

Solution:

Add and remove a MMS column from each list. You can confirm that this fixes your issue by performing a manual update of a single list and then run a bulk correction using PowerShell. Note that you will need to test and re-create any faulty Site Templates.

Cause

Not known at this time, it appears to be related to the document parser. It appears that in some cases the document Parser process fails on MMS values. The value in Word is maintained in the document’s xml fields but is not correctly udpated (at least in our tests) with the correct namespace for the term or termID.
It seems that by adding a new MMS column the issues with the other columns is corrected, we believe this might be due to some version or synchronisation process but have not tracked down the root cause.

Manual steps

In your list or library, open the list settings.

Image of library ribbon with Library Settings highlighted

Library Settings

Click on ‘Create Column’

Image of Create Column highlighted within Library Settings

Create Column

Enter a name, here we will use ‘DummyColumn’ and select ‘Managed Metadata’

Column creation process with Name and Type highlighted

Create Column (specify type and name)

Select a value in the MMS

Image of Managed metadata value selected in column creation

Select Managed Metadata Value

Click OK.

At this point you should be able to confirm that the MMS field is now synchronised between Office and SharePoint. You can then delete the column.

Note: If the process fails then delete the column anyway, unless you’re selling childrens accessories then it will probably be of little use.

Programatic

This can be scritped in several ways but the primary method will be on server PowerShell. An example script is shown below:

<#
Author: Alex Brassington (Trinity Expert Systems)
Date: 26/04/2013
Description:
Adds and removes an MMS colummn to every library in the white list for all sites in a web application. This is to
fix the office => SharePoint managed metadata service sync field issues.
This can be run with either a white list of lists/libraries to update or without, in which case all document libraries will be updated. It is possible that this only needs to run on one document library per site but i have not yet been able to confirm or refute that.
#>

Add-PSSnapin Microsoft.SharePoint.PowerShell -ea SilentlyContinue

#Reference to the content type hub to be used for the MMS Column    
$CTHubURL= "http://sharepoint/sites/cthub"

#Site Collection to modify
$SCURL = "http://sharepoint/sites/cthub"

#Name of the MMS instance to use
$MMSInstance = "Managed Metadata Service"


#A 'white list' of libraries to process. Note that this currently contains 'DOcuments' which should be handled as a special case.
$librariesToCheck =
(
    "Documents",
    "Entertainment",
    "Project Documents",
    "Management Information"
)

    #Setup the termstore object
    $contentTypeHub = Get-SPSite $contentTypeHubURL
    $session = New-Object Microsoft.SharePoint.Taxonomy.TaxonomySession($contentTypeHub)
    $termStore = $session.TermStores | ? {$_.Name -eq $MMSInstance}
    $group = $termStore.Groups["Demo Terms"]
    $termSet = $group.Termsets["Condiments"]


Function Update-LibrariesInSiteCollection ()
{
[CmdletBinding()]
    Param (
    [Parameter(Position=0,Mandatory=$true,ValueFromPipeLine=$true)][string]$siteURL, 
    [Parameter(Position=1,Mandatory=$true)][Microsoft.SharePoint.Taxonomy.TermSet]$termSet,
    [Parameter(Position=2,Mandatory=$true)][Microsoft.SharePoint.Taxonomy.TermStore]$termStore,
    [Parameter(Position=3,Mandatory=$false)][string]$errorFile,
    [Parameter(Position=4,Mandatory=$false)][string[]]$librariesToCheck
    )
    
    
    #No change required, only used internally
    $columnName = "TempColumn"
    
    #Get the SharePoint Site Collection to process
    $site = Get-SPSite $siteURL
    Write-Verbose "Updating Site Collection $($site.URL)"
    foreach ($web in $site.AllWebs)
    {
        Write-Verbose "Updating Web $($web.URL)"
        
        #If there's a list of folders to use as a whitelist then use them
        if ($librariesToCheck)
        {
            Write-Verbose "Updating libraries based on provided White list"
            $lists = $web.Lists | ? {$librariesToCheck -contains $_}
        }
        else
        {
            #If not then process all libraries.
            Write-Verbose "Updating all document libraries only"
            $lists = $web.Lists | ? {$_.BaseType -eq "DocumentLibrary"}
        }
        
        foreach ($list in $lists)
        {
            Write-Verbose "Updating list $($list.Title)"
            try
            {
                #Create a new taxonomy field
                $taxField = $list.fields.CreateNewField("TaxonomyFieldType", $columnName)
                
                #set the term store ID and the termset ID 
                $taxField.SspId = $termStore.Id
                $taxField.TermSetId = $termSet.Id
                
                #Add the column to the list
                $list.Fields.Add($taxField) | Out-Null
                $list.Update()
                
                #Remove the column
                $column = $list.fields[$columnName]
                $column.Delete()
                Write-Verbose "List Complete $($list.Title)"
            }
            catch
            {
                Write-Error "Error encountered on List: $($list.Title)"
            }
        }
    $web.Dispose()
    }
    
    #If a file path was given then write out the error log.
    if ($errorFile)
    {
        $error >> $errorFile
    }
    #Dispose of the site collection
    $site.Dispose()
}

Update-LibrariesInSiteCollection -siteURL $SCURL -termSet $termSet -termStore $termStore -errorFile $ErrorPath -Verbose

My thanks to my colleague Paul Hunt (aka Cimares) who found the fix that we scripted above.

Adding Content Types to the New button on a document library with PowerShell

Background
I was at a customer site and they wanted to remove a load of document types from the “New” button on their document libraries. I tried using the SPContentType.Hidden = $true parameter but realised that wasn’t the one. I then spent some more time banging my head against it and just did it by hand and moved on.

Another person asked how to do something similar on PowerShell.org (here: http://powershell.org/discuss/viewtopic.php?f=12&t=1407). I had some time and was irked by my failure before hand so I gave it another go. I met some success but thought that since it’s something that annoyed me, and since there’s no easily found PowerShell specific posts about this, it’s worth doing properly and blogging.

It turns out that the new button is determined by SPList.rootFolder.UniqueContentTypeOrder property. This is an ordered list of content types to display, any item in the list must be in the lists’ content types but not vice versa. Modify this and you modify the same property you set in the GUI. Happy days.

The first step is to see if a content type is available in the new button or not:

Is-ContentTypeInNewButton

Function Is-ContentTypeInNewButton {

[CmdletBinding()]
Param ([parameter(Mandatory=$true)][string] $ContentTypeName,
       [parameter(Mandatory=$true)][Microsoft.SharePoint.SPList] $SPList)
BEGIN   {  Write-Verbose "Begining Is-ContentTypeInNewButton" }
       PROCESS{
            #get the uniquecontenttypes from the list root folder
            $rootFolder = $SPList.RootFolder
            $contentTypesInPlace = [Microsoft.SharePoint.SPContentType[]] $rootFolder.UniqueContentTypeOrder
            
            #Check if any of them are the same as the test content type
            $results = $contentTypesInPlace | where { $_.Name -eq $ContentTypeName} 
            if ($results -ne $null)
            {
                Write-Verbose "$ContentTypeName Found"
                return $true
            }
            else
            {
                Write-Verbose "$ContentTypeName Not Found"
                return $false
            }
    }
    
END   {  Write-Verbose "Exiting Is-ContentTypeInNewButton" }
}

Of course there’s a possible gotcha. What if the Content type isn’t even added to the list at all?

Ensure-ContentTypeInList

Function Ensure-ContentTypeInList{

[CmdletBinding()]
Param ( [parameter(Mandatory=$true,ValueFromPipeline=$true)][string] $ContentTypeName,
       [parameter(Mandatory=$true)][Microsoft.SharePoint.SPList] $SPList)

BEGIN   {  Write-Verbose "Begining Ensure-ContentTypeInList" }
PROCESS { 

     #Check to see if the content type is already in the list
     $contentType = $SPList.ContentTypes[$ContentTypeName]
     if ($ContentType -ne $null)
     {
        #Content type already present
        Write-Verbose "$ContentTypeName already present in list"
        Return $true
     }
     else
     {
        Write-Verbose "$ContentTypeName not in list. Attempting to add"
        if (!$SPList.ContentTypesEnabled)
        {
            Write-Verbose "Content Types disabled in list $SPList, Enabling"
            $SPList.ContentTypesEnabled = $true
            $SPList.Update()
        }
         #Add site content types to the list from the site collection root
         $ctToAdd = $SPList.ParentWeb.Site.RootWeb.ContentTypes[$ContentTypeName]
         if($ctToAdd -eq $null)
         {
            Write-Error "Error - Content Type could not be found in the Site Collection"
            #I don't believe this will be called.
            return $false
         }
         $SPList.ContentTypes.Add($ctToAdd) | Out-Null
         $SPList.Update()
         Write-Verbose "$ContentTypeName added to list"
         return $true
     }
    }
END {
     Write-Verbose "Exiting Ensure-ContentTypeInList"
    }
}

Well that’s a start. Now we can tell if the content type already exsits, and can add the content type to the list if it doesn’t, let’s put that into something useful:

Ensure-ContentTypeInNewButton

Function Ensure-ContentTypeInNewButton{

[CmdletBinding()]
Param ( [parameter(Mandatory=$true,ValueFromPipeline=$true)][string] $ContentTypeName,
        [parameter(Mandatory=$true)][Microsoft.SharePoint.SPList] $SPList)
    BEGIN   { 
                Write-Verbose "Begining  Ensure-ContentTypeInNewButton"
                #get the uniquecontenttypes from the list root folder
                $contentTypesInPlace = New-Object 'System.Collections.Generic.List[Microsoft.SharePoint.SPContentType]'
                $contentTypesInPlace = $SPList.RootFolder.UniqueContentTypeOrder
                $dirtyFlag = $false
            }
    PROCESS { 
                
        #Check the content type isn't already present in the content type
        $AlreadyPresent = Is-ContentTypeInNewButton -ContentTypeName $ContentTypeName -SPList $SPList
        if ($AlreadyPresent)
        {
            Write-Verbose "$ContentTypeName is already present in the new button"
        }
        else
        {
            #Check that there really is such a content type
            $ContentTypePresent = Ensure-ContentTypeInList $ContentTypeName $SPList
            #Catch error events
            if ($ContentTypePresent)
            {
                #We now know that the content type is not in the new button and is present in the list. Carry on adding the content type
                
                $ctToAdd = $SPList.ContentTypes[$ContentTypeName]
                
                #add our content type to the unique content type list
                $contentTypesInPlace  =  $contentTypesInPlace + $ctToAdd
                $dirtyFlag = $true
                Write-Verbose "$ContentTypeName queued to add to the new button"
            }
            else
            {
                Write-Error -Message "Content type could not be added to the list."
            }
        }
    }
    End{
        #Set the UniqueContentTypeOrder to the collection we made above
        if ($dirtyFlag)
        {
           $SPList = $SPList.ParentWeb.Lists[$SPList.ID]
            $rootFolder = $SPList.RootFolder
            $rootFolder.UniqueContentTypeOrder = [Microsoft.SharePoint.SPContentType[]]  $contentTypesInPlace
        
             #Update the root folder
             $rootFolder.Update()
             Write-Verbose "ContentType(s) added to the new button in list $($SPList.Name)"
        }
        else
        {
                Write-Verbose "No changes"
        }
         Write-Verbose "Exiting  Ensure-ContentTypeInNewButton"
                
    }
}

Awesome. On the other hand the stuff above didn’t lend itself to testing. I had to go into the GUI each time to remove my content types. So let’s have something to help make unwind our changes:

Remove-ContentTypeFromNewButton

Function Remove-ContentTypeFromNewButton{

[CmdletBinding()]
Param ( [parameter(Mandatory=$true,ValueFromPipeline=$true)][string] $ContentTypeName,
        [parameter(Mandatory=$true)][Microsoft.SharePoint.SPList] $SPList)
    
BEGIN   { Write-Verbose "Begining Remove-ContentTypeFromNewButton" }
PROCESS { 
   
            #Check the content type isn't already present in the content type
            $AlreadyPresent = Is-ContentTypeInNewButton -ContentTypeName $ContentTypeName -SPList $SPList
            if ($AlreadyPresent)
            {
                Write-Verbose "$ContentTypeName is present in the new button - removing"
                
                #get the uniquecontenttypes from the list root folder
                $rootFolder = $SPList.RootFolder
                
                #Get the content types where the names are different to our content type
                $contentTypesInPlace = [System.Collections.ArrayList] $rootFolder.UniqueContentTypeOrder
                $contentTypesInPlace = $contentTypesInPlace | where {$_.Name -ne $contentTypeName}
                
                #Set the UniqueContentTypeOrder to the collection we made above
                $rootFolder.UniqueContentTypeOrder = [Microsoft.SharePoint.SPContentType[]]  $contentTypesInPlace
                
                #Update the root folder
                $rootFolder.Update()
                Write-Verbose "$ContentTypeName removed from the new button in list $($SPList.Name)"
            }
            else
            {
                Write-Verbose "$ContentTypeName is not present in the new button. No further action required."
            }
        }
END     { Write-Verbose "Exiting Remove-ContentTypeFromNewButton" }

}

Done.

So we now have the functions to take a list and content type, run a single command which will add a content type, ensuring it’s added to the new button. Further to that we’ve got some basic help (which WordPress has stripped out), error handling and it’ll take piplines and multiple content types. I love PowerShell.

Tests and examples of code

$CTHubSiteCollectionURL = "http://sharepoint/sites/cthub"
$singleContentType = "AlexB_Document"
$contentTypesToAddToNewButton = @("AlexB_Document1b","AlexB_Docudddment2")

$SPWeb = Get-SPWeb $CTHubSiteCollectionURL
$docLib = $spweb.Lists["TestDocLib"]


Write-Host "Is Content Type $ContentTypeName in the new button already? $(Is-ContentTypeInNewButton $singleContentType $doclib )"
Write-Host "Adding the content type to the new button (using the wonderful Ensure method which won't throw errors if already present)"
Ensure-ContentTypeInNewButton -ContentTypeName $singleContentType -SPList $doclib
Write-Host "Is Content Type $ContentTypeName in the new button already? $(Is-ContentTypeInNewButton $singleContentType $doclib )"
#Victory!

"Removing"
#$contentTypesToUpdate | Remove-ContentTypeFromNewButton -SPList $doclib 
Write-Host "Is Content Type in the new button already? $(Is-ContentTypeInNewButton $singleContentType $doclib )"
#Also Victory!

#Let's try a more interesting example
foreach ($contentTypeName in $contentTypesToAddToNewButton)
{
Write-Host "Is Content Type: $ContentTypeName in the new button already? $(Is-ContentTypeInNewButton $contentTypename $doclib)"
}
Write-Host "Adding the content types to the new button (using the wonderful Ensure method which won't throw errors if already present)"
$contentTypesToAddToNewButton | Ensure-ContentTypeInNewButton -SPList $doclib
foreach ($contentTypeName in $contentTypesToAddToNewButton)
{
Write-Host "Is Content Type: $ContentTypeName in the new button already? $(Is-ContentTypeInNewButton $contentTypename $doclib)"
}
#Victory!

And now i can rest. Any critiques of the powershell welcomed.

Just for those that are interested in the bit that makes this all possible, error handling etc. stripped out:

#Get the Web that holds the list
$SPWeb = Get-SPWeb "http://sharepoint/sites/cthub"
#get the library
$list = $SPWeb.Lists["Shared Documents"]

#Get a content type
$contentType = $docLib.ContentTypes | where { $_.Name -eq "AlexB_Document"}

#Get the root folder object
$rootFolder = $list.RootFolder

#Get the current list of content types available
$contentTypesInPlace = [Microsoft.SharePoint.SPContentType[]] $rootFolder.UniqueContentTypeOrder

#add our content type
$contentTypesInPlace  =  $contentTypesInPlace + $ContentType

#set the list to our new list
$rootFolder.UniqueContentTypeOrder = [Microsoft.SharePoint.SPContentType[]]  $contentTypesInPlace

#Update the folder
$rootFolder.Update()

References:
Thanks to Praveen Battula who’s blog post pointed me in the right direction and has some nice C# for doing a similar task.
http://praveenbattula.blogspot.co.uk/2011/01/change-content-type-order-in-new-button.html
Link to TechNet article on the UniqueContentTypeOrder: http://msdn.microsoft.com/en-us/library/microsoft.sharepoint.spfolder.uniquecontenttypeorder(v=office.14).aspx.

Thoughts for the future:
It’d be nice to be able to order the items. Not difficult technically but what would the best way to use such a process be?
It seems you can change the new button for different folders in the hierarchy. That’d be handy

Sorting content types in the new button on a list

As i mentioned in my last post it’d be useful to be able to sort the content types in a document library’s new button. This builds on the examples in the previous post but it can be run on it’s own.

This script will attempt to set a default content type, if one is specified, but if it isn’t or the one listed can’t be found it’ll default to alphabetical.

Function Sort-ContentTypesInNewButton{

[CmdletBinding()]
Param ( [parameter(Mandatory=$false)][string] $DefaultContentTypeName,
[parameter(Mandatory=$true,ValueFromPipeline=$true)][Microsoft.SharePoint.SPList] $SPList)
BEGIN {
Write-Verbose "Begining Sort-ContentTypesInNewButton"
}
PROCESS { 

$rootFolder = $SPList.RootFolder

#Get content types fromt the button
$contentTypesInPlace = New-Object 'System.Collections.Generic.List[Microsoft.SharePoint.SPContentType]'
$contentTypesInPlace = $rootFolder.UniqueContentTypeOrder

#Has a default content type name been specified?
if ($DefaultContentTypeName)
{
$contentType = $contentTypesInPlace | where { $_.Name -eq $DefaultContentTypeName }
if ($contentType -ne $null)
{
#Add the default content type
$sortedListOfContentTypes = New-Object 'System.Collections.Generic.List[Microsoft.SharePoint.SPContentType]'
$sortedListOfContentTypes += $SPList.ContentTypes[$DefaultContentTypeName]

#Remove the default content type from the list
$contentTypesInPlace = $contentTypesInPlace | where {$_.Name -ne $DefaultContentTypeName}
}
else
{
Write-Error "$DefaultContentTypeName was not found in the list, sorting by Name alone"
}
}
else
{
Write-Verbose "No default content type specified"
}

#sort the remaining content types and add the sorted list
foreach ($contentType in $($contentTypesInPlace | Sort-Object -Property Name ))
{
#Add the content types 
$sortedListOfContentTypes = [Microsoft.SharePoint.SPContentType[]] $sortedListOfContentTypes + $contentType
}

$rootFolder.UniqueContentTypeOrder = [Microsoft.SharePoint.SPContentType[]] $sortedListOfContentTypes

#Update the root folder
$rootFolder.Update()
Write-Verbose "ContentType(s) sorted and added to the new button in list $($SPList.Name)"

}
End{
Write-Verbose "Ending Sort-ContentTypesInNewButton"
}

}