Tag Archives: PowerShell

Splitting Office 365 Audit Logs

Office 365 audit exports are quite complicated behind the scenes. If you look at the data in CSV format you’ll quickly see there’s four fields; Time, User, Action and Detail.

The fun is in the Detail field, it’s really a JSON object with all of the interesting data that the audit log holds. Whilst it’s possible to use Excel to expand those objects and convert them into usable CSV content it’s a bit clumsy.

The script below is a first draft at a script to split the content up into separate logs for future analysis.

<#
Notes: This function is not optimised for large files. 

For large files it would be desirable to batch write actions so that no more than (for example)
100 rows are held in memory at once. Immediate write activities are possible but are suspected
to result in excessive disk write activity.
#>
Function Split-O365AuditLogs ()
{
Params(
    $sourcePath,
    $resultsFolderPath
)
    #Get the content to process
    $content = Import-Csv $sourcePath
    $datumArray = @();

    foreach ($line in $content)
    {
   
        #Details are encoded as JSON data.
        $expandedDetails = $line.Detail | ConvertFrom-Json

        #add the non JSON parameters
        Add-Member -InputObject $expandedDetails -MemberType NoteProperty -Name "Time" -Value $line.Time
        Add-Member -InputObject $expandedDetails -MemberType NoteProperty -Name "User" -Value $line.User
        Add-Member -InputObject $expandedDetails -MemberType NoteProperty -Name "Action" -Value $line.Action
        $datumArray += $expandedDetails
    }
    #Build a list of unique actions
    $actions = $datumArray | select Action -Unique -ExpandProperty Action

    foreach ($action in $actions)
    {
    
        $datumArray | ? {$_.Action -eq $action} | ConvertTo-Csv | Out-File -FilePath ("{0}\{1}.csv" -f $resultsFolderPath, $Action) -Append
    }
}

There’s plenty of room for improvement. It’s highly likely that the UserType field is the key to exporting to a more concise set of exports that share common fields. If anyone’s interested i’ll give it another go.

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.

Changing Modified, and Created details in SharePoint

Sometimes you need to lie to SharePoint. In this post i’ll show you how to change the details for who created an item, modified it and when they modified it.

When you’re doing bulk uploads, dealing with lists where you wish to use the Advanced features of only allowing users to edit their own items or just testing some behaviour, eventually you’ll wish you can change the values that SharePoitn doesn’t let you change.

The first thing is, as always, to find the value we want to change:

#Add the SharePoint snapin
Add-PSSnapin Microsoft.SharePoint.Powershell -ea SilentlyContinue

#set the web url and the list name to work upon
$url = "http://sharepoint/sites/cthub"
$listName = "Shared Documents"
$fileName = "FileName.xlsx"

#Get the appropriate list from the web
$web = get-SPWeb $url
$list = $web.lists[$listName]

#Get the file using the filename
$item = $list.Items | ? {$_.Name -eq $fileName}

#Print out current Created by and Created date
Write-Output ("item created by {0} on {1}" -f $item["Author"].tostring(), $item["Created"] )

#Print out current Created by and Created date
Write-Output ("item last modified by {0} on {1}" -f $item["Editor"].tostring(), ($item["Modified"] -f "dd-MM-yyyy"))

As you can see we access the item properties by treating the $item as a hashtable and use the property name as the key.

#Set the created by values
$userLogin = "ALEXB\AlexB"
$dateToStore = Get-Date "10/02/1984"

$user = Get-SPUser -Web $web | ? {$_.userlogin -eq $userLogin}
$userString = "{0};#{1}" -f $user.ID, $user.UserLogin.Tostring()

#Sets the created by field
$item["Author"] = $userString
$item["Created"] = $dateToStore

#Set the modified by values
$item["Editor"] = $userString
$item["Modified"] = $dateToStore


#Store changes without overwriting the existing Modified details.
$item.UpdateOverwriteVersion()

Setting the value is a bit more complicated. To do that you have to build the appropriate user string. In my example the user is already part of the site, if they haven’t previously been added to the user information list you’ll need an extra step here to insert them.

The second bit which differs from your usual PowerShell update is the use of the UpdateOverwriteVersion() method. There’s several update methods in SharePoint but only this one will preserve your changes to modified by and created by.

And now, the full script:

<#
Title: Set modified and created by details
Author: Alex Brassington
Category: Proof of Concept Script
Description
This script is to show how to read, modify and otherwise manipulate the created by and modified by details on documents.
This is to enable correction of incorrect data as part of migrations. It is also useful to enable testing of retention policies.
#>

#Add the SharePoint snapin
Add-PSSnapin Microsoft.SharePoint.Powershell -ea SilentlyContinue

#set the web url and the list name to work upon
$url = "http://sharepoint/sites/cthub"
$listName = "Shared Documents"
$fileName = "FileName.xlsx"

#Get the appropriate list from the web
$web = get-SPWeb $url
$list = $web.lists[$listName]

#Get the file using the filename
$item = $list.Items | ? {$_.Name -eq $fileName}

#Print out current Created by and Created date
Write-Output ("item created by {0} on {1}" -f $item["Author"].tostring(), $item["Created"] )

#Print out current Created by and Created date
Write-Output ("item last modified by {0} on {1}" -f $item["Editor"].tostring(), ($item["Modified"] -f "dd-MM-yyyy"))

#Set the created by values
$userLogin = "ALEXB\AlexB"
$dateToStore = Get-Date "10/02/1984"

$user = Get-SPUser -Web $web | ? {$_.userlogin -eq $userLogin}
$userString = "{0};#{1}" -f $user.ID, $user.UserLogin.Tostring()


#Sets the created by field
$item["Author"] = $userString
$item["Created"] = $dateToStore

#Set the modified by values
$item["Editor"] = $userString
$item["Modified"] = $dateToStore


#Store changes without overwriting the existing Modified details.
$item.UpdateOverwriteVersion()

Deleting Versions

Someone asked for a script that could delete previous versions for SharePoint 3.0. I don’t have a 3.0 dev environment but I do have a 2010 build and it interested me.

Function Delete-SPVersions ()
{
[CmdletBinding(SupportsShouldProcess=$true)]
param(
    [Parameter(Mandatory=$True)][string]$webUrl, 
    [Parameter(Mandatory=$True)][string]$listName, 
    [Parameter(Mandatory=$False)][string]$numberOfMajorVersions
    ) 

    #Get the web
    $web = Get-SPWeb $webUrl


    #Get the list
    $list = $web.Lists[$listName]

    $list.Items | % {
        #Get the item in a slightly more usable variable
        $item = $_

        Write-Output ("Deleting versions for item {0}" -f $item.Name)
        
        #Get all major versions
        #Note, the version ID goes up by 512 for each major version.
        $majorVersions = $item.Versions | ? { $_.VersionID % 512 -eq 0}

        #get the largest version number
        $latestMajorID = $majorVersions | select VersionID -ExpandProperty VersionID | sort -Descending | select -First 1

        #Slightly lazy way to get the latest major version and format it as a single point decimal
        Write-Output ("   Latest major version to retain is {0:0.0}" -f ($latestMajorID /512))
        
        #Filter the major versions to only those which are lower than the highest number - 512 * $numberOfMajorVersions
        $majorVersionsToDelete = $majorVersions | ? {$_.VersionID -le ($latestMajorID - 512 * $numberOfMajorVersions)}
        if ($majorVersionsToDelete)
        {
            $majorVersionsToDelete | % {
                Write-Verbose ("  Deleting major version {0}" -f $_.VersionLabel)
                if ($pscmdlet.ShouldProcess($_.VersionLabel,"Deleting major version"))
                {
                    $_.Delete()
                }
            }
        }
        else
        {
            Write-Verbose "No major versions to delete"
        }
        
        #Re-fetch the item to ensure that the versions are still valid
        $item = $list.GetItemByUniqueId($item.UniqueId)
        
        #Get all the minor versions
        $minorVersions = $item.Versions | ? { $_.VersionID % 512 -ne 0}

        #Delete Minor versions greater than the last major version kept
        $minorVersionsToDelete = $minorVersions | ? {$_.VersionID -lt $latestMajorID}
        if ($minorVersionsToDelete)
        {
            $minorVersionsToDelete | % {
                Write-Verbose ("Deleting minor version {0}" -f $_.VersionLabel)
                if ($pscmdlet.ShouldProcess($_.VersionLabel,"Deleting minor version"))
                {
                    $_.Delete()
                }
            }
        }
        else
        {
            Write-Verbose "No minor versions to delete"
        }
    }
    $web.Dispose()
}

Failed Search Scripting

Sometimes knowing what doesn’t work is as useful as what does work. In that vein here’s how I spent my journey home…

A post on technet asked about how to deal with long running search crawls that were impacting users when they overran into business hours. In large SharePoint environments that shouldn’t really happen but it’s a fairly common concern in smaller shops.

Ideally you’ll tune your search so that it always completes in time but that doesn’t always work. For those edge cases there’s two options:

  1. Pause a specific (or all) crawl(s) during working hours.
  2. Reduce the impact of the crawls during working hours

Pausing a crawl is easy, it’s also done very well by other people such as:
Ed Wilson

I wanted to drop the performance of the crawl down so that it can still keep going but not impact the end users.

The first step was to find out how to create a crawl rule to reduce the impact of the search

$shRule = Get-SPEnterpriseSearchSiteHitRule –Identity "SharePoint"

#Cripple search
$shRule.HitRate = 1000
$shRule.Behavior = 'DelayBetweenRequests'
$shRule.Update()

#Revive search
$shRule.HitRate = 8
$shRule.Behavior = 'SimultaneousRequests'
$shRule.Update()

It turns out that in the API a crawl rule is known as a hit rule. Hit rules have two important values, the ‘rate’ and the behaviour.

The script above was enough to let me create a rule and set it to either run at a normal page or with a 16 minute delay between requests. And it worked!

Well, it created a rule and that rule worked. Sadly i’d forgotten that the crawler rules are only checked when you start a crawl. If you start a crawl then punch the delay between items up to 1000 it won’t make a blind bit of difference.

It turns out that pausing the crawl doesn’t make the search engine re-check the crawl rate.

So, a failure. The only thing i can think of is using reflection to find out what is happening in the background and then doing something deeply unsupported to modify the values in flight. Maybe another time.

Check Blob Cache settings across a farm

A routine but really annoying task came up today. The status of BLOB caching for all Web Applications, normally I’d hop into the config files and check but these are spread over 6 farms.
To make things worse each farm had between 4 and 5 web apps and between 1 and 3 WFEs. In total that should have meant checking in the region of 50 config files.

Sod that.

Getting data from the config file is easy, after all it’s just an xml document. Once we’ve got the document it’s a nice easy task to suck the actual values out:

	$xml = [xml](Get-Content $webConfig)
           $blobNode =  $xml.configuration.SharePoint.BlobCache
            
           $props = @{
                'Location' = $blobNode.location;
                'MaxSize' = $blobNode.maxsize;
                'Enabled' = $blobNode.enabled
            }
           New-Object -TypeName PSObject -Property $props
That on it's own isn't that helpful. After all we don't just want to get the values for ONE config file, we want to get them for something like 50. Also I don't really want to have to list each one out, I just want them all... Through some rather excessive PowerShelling I know that we can get the config file's physical path from the IIS settings on a web application:
$WebApp = Get-SPWebApplication "http://sharepoint.domain.com" 
$WebApp.IISSettings
#This is horrible code but I haven't found a better way to get the value in a usable format.
$physicalPath = ($settings.Values | select path -Expand Path).Tostring()

That will give us the physical path to the folder containing the web.config file. It's only a small leap to make it loop through all the locations…

#Script to check the blob caching configuration in all webconfig files in your farm on all servers.

Add-PSSnapin Microsoft.SharePoint.PowerShell -ea SilentlyContinue

$outputFile = "C:\Folder\TestFile.txt"

#This is a rubbish way of getting the server names where the foundation web app is running but i can't
#find a better one at the moment.
$serversWithFWA =Get-SPServiceInstance `
    | ? { $_.TypeName-eq "Microsoft SharePoint Foundation Web Application" -AND $_.Status -eq "Online"} `
    | Select Server -ExpandProperty Server | select Address

	
	
Get-SPWebApplication | %{
 
    $configResults = @()
	$webAppName = $_.name
    Write-Host "Webapp $webAppName"
    
    #Get the physical path from the iis settings
    $settings =$_.IISSettings
    $physicalPath = ($settings.Values | select path -Expand Path).Tostring()
     
    #foreach server running the Foundation Web Application.
    foreach ($server in $serversWithFWA)
    {
        #Build the UNC path to the file Note that this relies on knowing more or less where your config files are
        #This could be improved using regex.
        $serverUNCPath = $physicalPath.Replace("C:\",("\\" + $server.Address + "\C$\"))
        $webConfig = $serverUNCPath +"\web.config"
 
        #If the file exists then try to read the values
        If(Test-Path ($webConfig))
        {
            $xml = [xml](Get-Content $webConfig)
            $blobNode =$xml.configuration.SharePoint.BlobCache

            $props = @{
				'Server' = $server.Address;
                'Location' = $blobNode.location;
                'MaxSize' = $blobNode.maxsize;
                'Enabled' = $blobNode.enabled
            }
            $configResults += New-Object -TypeName PSObject -Property $props
        }
    }
	#Print the results out for the GUI
	$webAppName
    $configResults | ft
	
	#Output the data into a useful format - start by printing out the file name
	$webAppName >>  $outputFile
	#CSV because the data is immeasurably easier to load into a table etc. later. HTML would be a good alternative
	$configResults | ConvertTo-CSV >> $outputFile
}

Gooey SharePoint Scripting

Today i’m going to show you how to script Sharepoint through the GUI. Whilst in this example we’ll be running the code on server the same concepts and approach can be used to Script it from any machine that can hit the relevant website…

Our example might seem to be a little forced but it’s based on a real world experience. We had a client who had a fairly complicated Content Type scenario, over 150 Content Types spread over 8 levels of inheritance with untold columns. Then we discovered an issue and needed to publish every single one of those content types. This is the classic example of where PowerShell should be used but awkwardly they’d been burnt with PowerShell publishing before.
As such we had a flat edict, no PowerShell publishing of content types. It must go through the GUI.

A post i’d seen recently by Dr James McCaffrey popped into my head. It was about using PowerShell to automate testing of web applications using PowerShell.
Why not use the same process to automate the publishing of the content types?

The first thing to do is to get ourselves an IE window:

$ie = New-Object -com "Internet Explorer"
#This starts by default in a hidden mode so let's show it
$ie.Visible = $true

This isn’t much use on its’ own so let’s send it to a page. In our case we want to go to the page to publish one of our content types. We know that the publish page itself is an application page that is referenced from a site collection root web with the following URL syntax:

siteCollectionRoot/_layouts/managectpublishing.aspx?ctype=ContentTypeID

Glossing over how to get the ContentTypeID for now we have this:

$pageUrl= "http://sharepoint/sites/cthub/_layouts/managectpublishing.aspx?ctype=0x0100A4CF347707AC054EA9C3735EBDAC1A7C"
$ie.Naviagte($pageUrl)

Now PowerShell moves fast, so we’ll need to wait for Javascript to catch up.

While ($ie.ReadyState -ne 4)
{
	Sleep -Milliseconds 100
}

Now we’re there, let’s get the publish button. Thankfully this button has a consistent ID that we can get using the trusty F12 button in IE.

Image of Identifying an element's ID uwing F12

Identifying an element’s ID uwing F12

The catchily titled “ctl00_PlaceHolderMain_ctl00_RptControls_okButton” button? Depressingly i think i’m starting to see the naming convention behind these ids…

$textBoxID = "ctl00_PlaceHolderMain_ctl00_RptControls_okButton";
#You have to pass the Document into it's own object otherwise it will fail
$document = $ie.Document
$button= $document.getElementByID($buttonID)

And now all we need to do is to click that button:

$button.Click()

Now you might think that we’ve done all we need to do here and slap it into a foreach loop and be done with it. Of course you can’t do that as you need to give IE time to send that request using our good old friend Javascript.
So we wait for the page to re-direct us:

 
While ($ie.locationurl -eq $url)
{
start-sleep -Milliseconds 100
}

Now we can slap it into a foreach loop and with a little bit of work we can come up with something like the code below:

Add-PSSnapin Microsoft.SharePoint.PowerShell -ea SilentlyContinue

#URL for the content type hub to use
$CTHubURL= "https://sharepoint/sites/cthub"

#Get the Content Type hub
$site = Get-SPSite $CTHubURL

#Content Types to publish
$ContentAndColumns = @(
("Document Type 1"),
("Document Type 2"),
("Document Type 3")
)


#Open a new IE window
$ie = New-Object -com "InternetExplorer.Application"

#Make the window visible
$ie.visible = $true

#Loop through the content types and publish them
foreach ($contentTypeName in $ContentTypes)
{
    
    Write-Verbose "Processing $ContentTypeName"
    
    #Content types live at the root web
    $web = $site.rootWeb
    #Get the content type using it's name
    $ct =   $web.ContentTypes[$ContentTypeName]
    #Get the GUID for the CT
    $GUID = $ct.ID.ToString()
    #Get the URL for the page based on the content type hub url, the application page that does publishing and the GUID
    $url = $CTHubURL+ "/_layouts/managectpublishing.aspx?ctype=" + $GUID  
    #Go to the page
    $ie.navigate($url)
    #Wait for the page to finish loading
    while ($ie.ReadyState -ne 4)
     {
        start-sleep -Milliseconds 100
     }
     #The ID of the button to press
    $buttonID = "ctl00_PlaceHolderMain_ctl00_RptControls_okButton"
    $document = $ie.Document
    $btn = $document.getElementByID($buttonID)
    #Push the button
    $btn.click()
    #Wait for the page to be re-directed
     while ($ie.locationurl -eq $url)
     {
        start-sleep -Milliseconds 100
     }
     Write-Verbose "Content Type $contentTypeName published"
}

I don’t know about you but there is something deeply neat about sitting at your desk watching IE do the dull task that you were convinced was going to bring your RSI back with a vengance, and in half the time you could do it.

This example might not be useful for that many people but the concept is intriguing. There’s no reason most of this can’t be done without any code on the server at all, the only time we use it is to get the GUIDs and those can be pre-fetched if needs be. Nor does it need any significant rights, as long as the account you use has permision to get into that site collection and publish content types then that’s all they need.

The logical destination of this is Office 365, the scripts and rules for running them on there are limited and limiting, they have to be. But the beauty of Scripting is that we don’t have to be limited by the detail of code, we can use higher level components and tools to worry about that for us. In this case, the GUI that microsoft were kind enough to provide us for when it’s too awkward to find the PowerShell console.

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.