An Information Architecture is the core set of designs that enable a structured, manageable and extensible enterprise information system

SharePoint Group Management

Managing SharePoint Groups in PowerShell

SharePoint Groups are a great mechanism for managing user permissions, however they exist within a single site collection. What if you have hundreds of site collections? We can easily script a range of common operations.

I prefer to use a CSV fed approach to manage groups and users. I create a CSV with the name of the group, and the users, which I list in pipe separated format (commas are already being used for the CSV). To read in a CSV use:

Import-Csv "L:PowerShellAD and SP group mapping.csv"

Let’s get the Site, Root Web, as well as an SPUser for the group owner, and get the groups object:

$Site = New-Object Microsoft.SharePoint.SPSite($SiteName)
write-host $site.Url
$rootWeb = $site.RootWeb;
$Owner = $rootWeb.EnsureUser($OwnerName)
$Groups = $rootWeb.SiteGroups;

Here’s how to add a Group:

$Groups.Add($SPGroupName, $Owner, $web.Site.Owner, “SharePoint Group to hold AD group for Members")

Here’s how to give the group Read access, for example:

$GroupToAddRoleTo = $Groups[$SPGroupName]
if ($GroupToAddRoleTo) #if group exists
{
$MyAcctassignment = New-Object Microsoft.SharePoint.SPRoleAssignment($GroupToAddRoleTo)
$MyAcctrole = $RootWeb.RoleDefinitions["Read"]
$MyAcctassignment.RoleDefinitionBindings.Add($MyAcctrole)
$RootWeb.RoleAssignments.Add($MyAcctassignment)
}

Here’s how to add a Member to a Group:

$UserObj = $rootWeb.EnsureUser($userName);
if ($UserObj) #if it exists
{
$GroupToAddTo.addUser($UserObj)  
}

Note that a duplicate addition of a member is a null-op, throwing no errors.

Here’s how to remove a member:

$UserObj = $rootWeb.EnsureUser($userName);
if ($UserObj)
{
$GroupToAddTo.RemoveUser($UserObj)  
}

Here’s how to remove all the members from a given group. This wipes the users from the whole site collection, so use this approach with care and consideration:

$user1 = $RootWeb.EnsureUser($MyUser)
try
{
$RootWeb.SiteUsers.Remove($MyUser)
$RootWeb.update()
}

Here’s the full script, with flags to setting the specific actions described above:

Add-PSSnapin "Microsoft.SharePoint.PowerShell" -ErrorAction SilentlyContinue
# uses feedfile to load and create set of SharePoint Groups.
$mylogfile="L:PowerShellongoinglogfile.txt"
$ADMap= Import-Csv "L:PowerShellAD and SP group mapping.csv"
$OwnerName = "DOMAIN\sp2013farm"
$AddGroups = $false;
$AddMembers = $false;  # optionally populates those groups, Comma separated list
$GrantGroupsRead = $true; #grants read at top rootweb level
$RemoveMembers = $false; # optionally  removes Comma separated list of users from the associated group
$WipeMembers = $false;	# wipes the groups clean		
$WipeUsersOutOfSite = $false;  #The Nuclear option. Useful to eliminate AD groups used directly as groups
#we do not need a hashtable for this work, but let's load it for extensibility
$MyMap=@{}  #load CSV contents into HashTable
for ($i=0; $i -lt $AD.Count; $i++)
{
$MyMap[$ADMap[$i].SharePointGroup] = $ADMap[$i].ADGroup;
}
# Script changes the letter heading for each site collection
$envrun="Dev"			# selects environment to run in
if ($envrun -eq "Dev")
{
$siteUrl = "h ttp://DevServer/sites/"
$mylogfile="L:PowerShellongoinglogfile.txt"
$LoopString = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z"
$LoopStringArr = $LoopString.Split(“,”)
}
elseif ($envrun -eq "Prod")
{
$siteUrl = "ht tp://sharepoint/sites/"
$mylogfile="L:PowerShellongoinglogfile.txt"
$LoopString = "A,B,C,D,E,F,G,H,I,J,K,L,M,N,O,P,Q,R,S,T,U,V,W,X,Y,Z"
$LoopStringArr = $LoopString.Split(“,”)
}
else
{
Write-Host "ENVIRONMENT SETTING NOT VALID: script terminating..."
$siteUrl =  $null;
return;
}
Write-Host "script starting" 
$myheader = "STARTING: $(get-date)"
foreach ($letter in $LoopStringArr)
{
$SiteName=$siteurl+$letter
$Site = New-Object Microsoft.SharePoint.SPSite($SiteName)
write-host $site.Url
$rootWeb = $site.RootWeb;
$Owner = $rootWeb.EnsureUser($OwnerName)
$Groups = $rootWeb.SiteGroups;
for ($ADi = 0; $ADi -lt $ADMap.count; $ADi++)
{
$SPGroupName = $ADMap[$ADi].SharePointGroup;
if ($AddGroups)
{
if (!$Groups[$SPGroupName]) #no exist, so create
{
try
{
$Groups.Add($SPGroupName, $Owner, $web.Site.Owner, “SharePoint Group to hold AD group members")
}
catch
{
Write-Host -ForegroundColor DarkRed "Ouch, could not create $($SPgroupName)"
}
}
else
{
Write-Host -ForegroundColor DarkGreen "Already exists: $($SPgroupName)"
}
} #endif Add Groups
if ($GrantGroupsRead)
{
$GroupToAddRoleTo = $Groups[$SPGroupName]
if ($GroupToAddRoleTo) #if group exists
{
$MyAcctassignment = New-Object Microsoft.SharePoint.SPRoleAssignment($GroupToAddRoleTo)
$MyAcctrole = $RootWeb.RoleDefinitions["Read"]
$MyAcctassignment.RoleDefinitionBindings.Add($MyAcctrole)
$RootWeb.RoleAssignments.Add($MyAcctassignment)
} #if the group exists in the first place
} #ActionFlagTrue
if ($AddMembers)
{
$GroupToAddTo = $Groups[$SPGroupName]
if ($GroupToAddTo) #if group exists
{
$usersToAdd = $ADMap[$ADi].ADGroup;
if ($usersToAdd.length -gt 0) #if no users to add, skip
{
$usersToAddArr = $usersToAdd.split("|")
foreach ($userName in $usersToAddArr)
{
try
{
$UserObj = $rootWeb.EnsureUser($userName);
if ($UserObj)
{
$GroupToAddTo.addUser($UserObj)  #dup adds are a null-op, throwing no errors
}
}
catch
{
Write-Host -ForegroundColor DarkRed "cannot add user ($($userName) to $($GroupToAddTo)"
}
}
} #users to add
} #if the group exists in the first place
} #ActionFlagTrue
if ($RemoveMembers)
{
$GroupToAddTo = $Groups[$SPGroupName]
if ($GroupToAddTo) #if group exists
{
$usersToAdd = $ADMap[$ADi].SharePointGroup;
if ($usersToAdd.length -gt 0) #if no users to add, skip
{
$usersToAddArr = $usersToAdd.split("|")
foreach ($userName in $usersToAddArr)
{
try
{
$UserObj = $rootWeb.EnsureUser($userName);
if ($UserObj)
{
$GroupToAddTo.RemoveUser($UserObj)  #dup adds are a null-op, throwing no errors
}
}
catch
{
Write-Host -ForegroundColor DarkRed "cannot add user ($($userName) to $($GroupToAddTo)"
}
}
} #users to add
} #if the group exists in the first place
} #ActionFlagTrue
if ($WipeMembers)  #Nukes all users in the group
{
$GroupToAddTo = $Groups[$SPGroupName]
if ($GroupToAddTo) #if group exists
{
foreach ($userName in $GroupToAddTo.Users)
{
try
{
$UserObj = $rootWeb.EnsureUser($userName);
if ($UserObj)
{
$GroupToAddTo.RemoveUser($UserObj)  #dup adds are a null-op, throwing no errors
}
}
catch
{
Write-Host -ForegroundColor DarkRed "cannot remove user ($($userName) to $($GroupToAddTo)"
}
}
} #if the group exists in the first place
} #ActionFlagTrue
if ($WipeUsersOutOfSite)  #Nukes all users in the group
{
$usersToNuke = $ADMap[$ADi].ADGroup;
if ($usersToNuke.length -gt 0) #if no users to add, skip
{
$usersToNukeArr = $usersToNuke.split("|")
foreach ($MyUser in $usersToNukeArr)
{
try
{
try
{
$user1 = $RootWeb.EnsureUser($MyUser)
}
catch
{
Write-Host "x1: Failed to ensure user $($MyUser) in $($Site.url)"
}
try
{
$RootWeb.SiteUsers.Remove($MyUser)
$RootWeb.update()
}
catch
{
Write-Host "x2: Failed to remove $($MyUser) from all users in $($Site.url)"
}
}
catch
{
Write-Host "x4: other failure for $($MyUser) in $($Site.url)"
}
} #if user is not null
} #foreach user to nuke
} #ActionFlagTrue
}
$rootWeb.dispose()
$site.dispose()
} #foreach site

URL to a location inside a PDF

Creating Fields and Content Types cleanly in SharePoint

when creating Content Types in SharePoint, it’s nice to script the process, for planning and repeatability. Here’s a nifty routine that allows a single line creation of Site Columns and Content Types.

First, here’s the routine to create Site Columns. The here-string XML is replaced with placeholders filled by parameters. Note the convenient parameters:

function tryAdd-TextField ([Microsoft.SharePoint.SPWeb] $web, [string] $DisplayName, [string] $InternalName, [string] $Group, [Boolean] $Hidden, 
[Boolean] $Required, [Boolean] $ShowInDisplayForm, [Boolean] $ShowInEditForm, [Boolean] $ShowInNewForm )
{
$fields = $web.fields;
try
{
$q=$fields.getFieldByInternalName($InternalName); 
} catch
{
$q=$null;
}
if ($q -ne $null)
{
Write-Host "$($InternalName) already exists!, no action taken to create this site column"
}
else
{
$fieldXMLString = '<Field Type="Text"
Name="@InternalName" 
DisplayName="@DisplayName"
StaticName="@InternalName"
Group="@Group"
Hidden="@Hidden"
Required="@Required"
Sealed="FALSE"
ShowInDisplayForm="@ShowInDisplayForm"
ShowInEditForm="@ShowInEditForm"
ShowInListSettings="TRUE"
ShowInNewForm="@ShowInNewForm">
</Field>' 
$FieldXMLString = $FieldXMLString.Replace("@InternalName",$InternalName.tostring())
$FieldXMLString = $FieldXMLString.Replace("@DisplayName",$DisplayName.tostring())
$FieldXMLString = $FieldXMLString.Replace("@Group",$Group.tostring())
$FieldXMLString = $FieldXMLString.Replace("@Hidden",$Hidden.tostring())
$FieldXMLString = $FieldXMLString.Replace("@Required",$Required.tostring())
$FieldXMLString = $FieldXMLString.Replace("@ShowInDisplayForm",$ShowInDisplayForm.tostring())
$FieldXMLString = $FieldXMLString.Replace("@ShowInEditForm",$ShowInEditForm.tostring())
$FieldXMLString = $FieldXMLString.Replace("@ShowInNewForm",$ShowInNewForm.tostring())
$web.Fields.AddFieldAsXml($fieldXMLString)
}
}
function tryAdd-CT ([Microsoft.SharePoint.SPWeb] $web, [string] $Field, [string] $Group, [string] $Parent, [string] $Name, [string] $Description )
{
try
{
$ctypeParent = $web.availablecontenttypes[$Parent]
}
catch
{
$ctypeParent = $null;
}
if ($ctypeParent -eq $null)
{
write-host "Content Type $($Name) not created because there was a problem finding the Parent Content Type $($Parent)"
}
else
{
$ctype = new-object Microsoft.SharePoint.SPContentType($ctypeParent, $web.contenttypes, $Name)
$ctype.Description = $Description
$ctype.group = $Group
if (![string]::IsNullOrEmpty($field))
{
foreach ($fld in $field.split("|"))
{
$f=$web.fields.getFieldByInternalName($fld)  
$link = new-object Microsoft.SharePoint.SPFieldLink $f
$ctype.FieldLinks.Add($link)
}
}
try
{
$ctype = $web.contenttypes.add($ctype)
}
catch
{
write-host "Content Type $($Name) already exists"
}
}
}

Let’s now create some site columns:

tryAdd-TextField -web $Web -DisplayName "Year"    -InternalName "YearACT"    -Group "Actuarial" -Hidden $False -Required $False -ShowInDisplayForm $True -ShowInEditForm $False -ShowInNewForm $False 
tryAdd-TextField -web $Web -DisplayName "Quarter" -InternalName "QuarterACT" -Group "Actuarial" -Hidden $False -Required $False -ShowInDisplayForm $True -ShowInEditForm $False -ShowInNewForm $False 
tryAdd-TextField -web $Web -DisplayName "Month"   -InternalName "MonthACT"   -Group "Actuarial" -Hidden $False -Required $False -ShowInDisplayForm $True -ShowInEditForm $False -ShowInNewForm $False 
tryAdd-TextField -web $Web -DisplayName "LOB"     -InternalName "LOBACT"     -Group "Actuarial" -Hidden $False -Required $False -ShowInDisplayForm $True -ShowInEditForm $False -ShowInNewForm $False 

Here’s how to create a Content Type with one field:

tryAdd-CT -web $Web -Field $null -Group "Actuarial"  -Parent (select-parent)  -Name "My Core ACT" -Description "Core Actuarial Document"

Note in the comment below, the fields can be passed in pipe-delimited

tryAdd-CT -web $Web -Field "YearACT|QuarterACT|MonthACT|LOBACT" -Group "Actuarial"  -Parent "My Core ACT" -Name "General (ACT)" -Description "General Actuarial Document"

Fixing taxonomy terms after a Metalogix migration

When migrating from SharePoint 2010 to 2013 using the Metalogix Content Matrix 7.0.0.1 migration tool, a taxonomy issue was encountered. The individual term from the SP2010 source was not applied to the SP2013 destination and associated with the correct termset and term.
The destination taxonomy field appeared empty in every sense. It was not displayed when viewing the Item, nor in the list view.
However one remnant of the term did appear in a surprising place. Within the read-only property bag of the item (item.XML) there was a field with what appears to be a GUID devoid of dashes as property name, with a value that seems somewhat usable.
The property value in this case was “a466a2acd62b498291c78829c2bb5fe3”.
item.GetFormattedValue(“a466a2acd62b498291c78829c2bb5fe3”) gets the value, as does item[“a466a2acd62b498291c78829c2bb5fe3”]
This is the GUID of the target field associated with the termset.
To make this more generic, I set the targetField to the field name we are looking for, and derive the GUID generically this way.

[Microsoft.SharePoint.Taxonomy.TaxonomyField]$taxonomyField = $item.Fields.GetField($targetField)

Then stripped off the dashes, to get it into the format that appears in the property bag:

$FieldGUID = $taxonomyField.id.Guid.tostring().replace("-","")

Then I derive the value for the property:

$FQterm = $item[$fieldGUID]

The value appears in the internal format, so I strip off any text appearing after a pipe:

$fqTerm = $FQterm.Substring(0,$FQterm.IndexOf("|"))

To match to the appropriate term, we need to handle the situation where a text value can appear under multiple parents. So we walk the hierarchy to find the single correct match. First we split the term into colon delimited segments:

$termHierarchy = $FQterm.Split(":")

Then check parent term matching to narrow to one appropriate matching term. This assumes checking two levels finds the match. It can be extended to match 7 levels deep if needed:

if ( $TC.count -gt 1)
{
$termIdx=-1;
if ($termHierarchy.count -ge 2)
{
$ParentTerm = $termHierarchy[$termHierarchy.count-2];
}
else
{
$ParentTerm = $null;
}
for ($ti=0; $ti -lt $TC.count; $ti++) #loop to ensure parent is right one
{
if ($TC[$ti].parent.getdefaultlabel(1033) -eq $parentTerm)
{
$termIdx = $ti;
}
}
}

Programmatically Configuring Metadata Navigation

Metadata navigation offers a quite clever way to navigate documents within a library. It cuts across folders, and allows drill-in for taxonomies as configured hierarchies, and also allows for a combination of fields for key filters. One can configure it manually for a library, but how can one do this programmatically? Below are the steps:

# get the SPWeb:
$web = Get-SPWeb "http://WhateverTheSPWebSiteIs"
# get the library:
$JPLib = $web.Lists.TryGetList("WhateverTheListIsCalled")
# Here's the XML object we are coding against:
$listNavSettings = [Microsoft.Office.DocumentManagement.MetadataNavigation.MetadataNavigationSettings]::GetMetadataNavigationSettings($JPLib)
# You can output the XML settings and easily see its configuration each step along the way with this:
$listnavSettings.SettingsXml
# Here's how to clear both Configured Hierarchies, and Key Filters:
$listNavSettings.ClearConfiguredHierarchies()
$listNavSettings.ClearConfiguredKeyFilters()
[Microsoft.Office.DocumentManagement.MetadataNavigation.MetadataNavigationSettings]::SetMetadataNavigationSettings($JPLib, $listNavSettings, $true)
# Let's get ready for a Content Type Hierarchy
$ctHierarchy = [Microsoft.Office.DocumentManagement.MetadataNavigation.MetadataNavigationHierarchy]::CreateContentTypeHierarchy()
$listnavSettings.AddConfiguredHierarchy($ctHierarchy)
# Add a configured Hierarchy:
$listNavSettings.AddConfiguredHierarchy($JPLib.Fields["Field Name"])
# Add a Content Type Key Filter; I chose this on purpose, as using "Content Type" will not work, the field to use here is "Content Type ID":
$listNavSettings.AddConfiguredKeyFilter($JPLib.Fields["Content Type ID"])
# Now the party ends happily with an update; note no $list.update() or $web.update() is needed:
[Microsoft.Office.DocumentManagement.MetadataNavigation.MetadataNavigationSettings]::SetMetadataNavigationSettings($JPLib, $listNavSettings, $true)

Checking for a specific permission for a specific user or group in SharePoint

While the UI allows one to easily check permissions for a given user, how can one do that iteratively?

Here’s the heart of the magic:

# first grab the user principal:
$user = $TargetWeb.Groups[$GroupToAdd];
# Now let's get the Role Assignments for that user on the folder:
$RA = $folder.RoleAssignments.GetAssignmentByPrincipal($user);
#Role bindings are useful
$RoleDefBindings = $RA.get_RoleDefinitionBindings();
#Now let's grab the Role Definition for Contribute permission in this SPWeb:
$roledef = $TargetWeb.RoleDefinitions["Contribute"];
Lastly we can check whether the role bindings for this user on this folder contains the Contribute Role Definition:
if ($RoleDefBindings.Contains($roledef)) {...}

Some useful routines first. Note I like to predefine a “Write” permission that allows creation and editing but not deletion:

function PermRole([string] $RoleChar)
{
switch ($RoleChar)
{
"R" {$res="Read"}
"C" {$res="Contribute"}
"W" {$res="Contribute wo delete"}
"D" {$res="Manage Hierarchy"}  #aka design, for setting permissions
default {$res=$null}
}
return $res;
}
# Routine for adding permission based on passing in a character for the role definition to be granted:
function AddPerm ([string] $RoleChar, [string] $RoleGroup)
{ #JPItem/f and TargetWeb are implied and not passed as parms for efficiency!
if ((!$RoleChar) -or (!$RoleGroup))
{
return; #race to be efficient on NullOp
}
$RoleValue=PermRole($RoleChar);
if (!$RoleValue) 
{
Write-Host -ForegroundColor -darkred "ok, expected Role, but got none, for $($RoleChar)"
return; 
}
try
{
#CONTROVERSIAL!
if ($RoleChar -eq "W")  #wipes out reads etc.
{
RemovePerm $RoleGroup
}
try
{
$user = $TargetWeb.ensureuser($RoleGroup)
}
catch  #if the above fails, user is likely not a user, but in fact a group, let's retry as group
{
$user = $TargetWeb.Groups[$RoleGroup]
}
$roledef = $TargetWeb.RoleDefinitions[$RoleValue]
$roleass = New-Object Microsoft.SharePoint.SPRoleAssignment($user)
$roleass.RoleDefinitionBindings.Add($roledef)
$f1.RoleAssignments.Add($roleass)  #This is SPFolder specific in this routine
}
catch
{
Write-Host -ForegroundColor DarkRed "ERR: Can't Assign $($RoleGroup)"
}
}

Let’s first establish the libraries to look at across all webs and site collections:

$libsArrStr="Library name 1|Library name 2"
$LibsArr=$libsArrStr.split("|")
$GroupToAdd = "Department Contributors"
$Site = "ht tp://SharePoint/sites/SiteOfInterest"
$TargetWeb=$web=get-spweb $Site;
Write-Host "==&gt;working in $($web.url)"
for ($j=0; $j -lt $LibsArr.count; $j++)
{
$libStr=$LibsArr[$j];
$list=$web.Lists.TryGetList($libStr)
if ($list -eq $null)
{
Write-Host -ForegroundColor DarkRed "List not found"
}
else
{
for ($fi=0; $fi -lt $list.Folders.Count; $fi++)
{
$f1 = $list.Folders.get_Item($fi)
$f = $f1.folder;
write-host -f green "The Library $($listName) exists in the site $($web.url), about to set folder Perms"  
try
{
#the rule is if this field has data, make the user a Contributor
$f1.ResetRoleInheritance(); #badda-bing, security is inherited
$isWritable = ($f.item["TargetMetadata"] -ne $null);
if (!$isWritable)
{
# nul op, already inherited
}
else  #let's see whether to break perms, based on whether the group already has Contribute
{
#let's see if the user has Contributor rights already; if so, no need to break inheritence
$user = $TargetWeb.Groups[$GroupToAdd]
$RA = $f1.RoleAssignments.GetAssignmentByPrincipal($user)
$RoleDefBindings = $RA.get_RoleDefinitionBindings()
$roledef = $TargetWeb.RoleDefinitions["Contribute"]
if ($RoleDefBindings.Contains($roledef))  # user is already a Contributor, let's do nothing
{
}
else
{
$f1.BreakRoleInheritance($true);  #minimalist approach
addPerm	"C" 	$GroupToAdd								
}
}
}
catch
{
Write-Host problems setting perms
}
} #Folder processing for loop $fi
} # list found
} #for loop $j

Enhancing SharePoint breadcrumbs when navigating deeply nested folders

I had an interesting challenge to improve breadcrumbs in navigating a deeply folder nested library. The nice breadcrumb dropdown icon is actually gone in SP2013 by default; that can easily be re-enabled via CSS. an onMouseOver event action in JavaScript can eliminate the need to click on it. However to improve the breadcrumb navigation, I created an SPWeb feature to enhance breadcrumbs by overriding the OnPreRender event. The OnPreRender should only occur if the ‘Visible’ property of the web control is set to true. The PreRender event is raised just before the page is about to render its contents. This is the last chance we have to modify the page output before it is received by the browser.

I deployed this web Scoped solution that overrides the breadcrumbs with OnPreRender page processing, and provides the full folder path, clickable for each link. It recursively searches the page for the “TitleBreadcrumb” control, and builds a replacement, adding an a href, folder link name, and breadcrumb image iteratively. It only affects the page when you are navigating within a library.

namespace MyDocLibraryBreadcrumb
{
public class MyDocLibBreadcrumbDelegateControl : WebControl 
{
protected override void OnPreRender(EventArgs e)
{
try
{
base.OnPreRender(e);
// Get the path to the current folder
string path = Context.Request.QueryString["RootFolder"];
// if there's no path then there is nothing to do; it implies are are not in the context of the library 
if (String.IsNullOrEmpty(path))
{
return;
}
// Let's get the current folder
SPWeb web = SPContext.Current.Web;
SPFolder currentFolder = web.GetFolder(path);
// Let's find the breadcrumb control on the current page - it's a ListProperty control where the property is  "TitleBreadcrumb". 
Control c = Utils.FindRecursive(Page.Controls, ctrl =&gt; ctrl is ListProperty &amp;&amp; ((ListProperty)ctrl).Property == "TitleBreadcrumb");
// If not found, nothing to do, and we are not likely in a library. 
if (c == null)
return;
// Let's subsitute the OOTB breadcrumb control with our replacement enhanced one
var parent = c.Parent;
var index = parent.Controls.IndexOf(c);
parent.Controls.RemoveAt(index);
parent.Controls.AddAt(index, new LiteralControl { Text = GetReplacementBreadCrumbOutput(currentFolder) });
}
catch (Exception ex)
{
// log errors quietly 
Utils.WriteMyLog(Utils.GetErrorInfo(ex));
}
}
/// SPFolder is the parameter to create navigation to
/// returns the HTML output
private string GetReplacementBreadCrumbOutput(SPFolder folder)
{
List&lt;BreadcrumbNodeData&gt; nodes = new List&lt;BreadcrumbNodeData&gt;();
// Collect a path from current folder to its root folder
SPFolder nodeFolder = folder;
while (nodeFolder != null)
{
// If we're in folder use the folder name as a title. If not use a library title enstead.
BreadcrumbNodeData node  = new BreadcrumbNodeData();
node.Url = nodeFolder.ServerRelativeUrl;
if (string.IsNullOrEmpty(nodeFolder.ParentFolder.Url))
{
if (nodeFolder.DocumentLibrary != null)
nodes.Add(new BreadcrumbNodeData
{Title = nodeFolder.DocumentLibrary.Title, Url = nodeFolder.ServerRelativeUrl});
}
else
{
nodes.Add(new BreadcrumbNodeData { Title = nodeFolder.Name, Url = nodeFolder.ServerRelativeUrl });
}
nodeFolder = string.IsNullOrEmpty(nodeFolder.ParentFolder.Url) ? null : nodeFolder.ParentFolder;
}
// Reverse the collected path because the root folder must be on the left in bredcrumb
nodes.Reverse();
// Create an HTML output similar to original. An arrow image we've created from the original
string htmlOutput = String.Empty;
foreach (var node in nodes)
{
if (node != nodes.Last())
htmlOutput +=
String.Format(
@"&lt;A href=""{0}""&gt;{1}&lt;/A&gt;&amp;nbsp;&lt;IMG style=""vertical-align:middle"" alt=: src=""/_layouts/images/MyDocLibraryBreadcrumb/breadcrumb_arrow.png""/&gt;&amp;nbsp;", node.Url, node.Title);
else
{
htmlOutput += node.Title;
}
}
return htmlOutput;
}
}
/// temporary class to holds navigation node data
public class BreadcrumbNodeData
{
/// Title for URL (it will be a folder name)
public string Title { get; set; }
/// Url to navigate on click (it will be a server relative URL of the folder)
public string Url { get; set; }
}
public class Utils
{
public static string GetErrorInfo(Exception ex)
{
string result = "ex.Message=" + ex.Message;
result += ex.InnerException == null ? "|ex.StackTrace=" + ex.StackTrace : String.Empty;
if (ex.InnerException != null)
result += "[INNER EXCEPTION: ]" + GetErrorInfo(ex.InnerException);
return result;
}
public static void WriteMyLog(string text)
{
SPDiagnosticsService.Local.WriteTrace(0,
new SPDiagnosticsCategory("MyDocLibBreadcrumbDelegateControl", TraceSeverity.High,
EventSeverity.Error),
TraceSeverity.High, text, null);
}
/// Finds a control based on provided criteria inside controls hierarchy
/// &lt;param name="controls"&gt;A Controls collection to start search&lt;/param&gt;
/// &lt;param name="criteria"&gt;A criteria to return control&lt;/param&gt;
/// &lt;returns&gt;The founded control &lt;/returns&gt;
public static Control FindRecursive(ControlCollection controls, Func&lt;Control, bool&gt; criteria)
{
foreach (Control control in controls)
{
if (criteria(control))
return control;
var innerControl = FindRecursive(control.Controls, criteria);
if (innerControl != null)
return innerControl;
}
return null;
}
}
}

Metadata Warehouse of SharePoint Content

I often write custom reports to be able to analyze huge data from a SharePoint farm. I can ansewr questions such as:
– Number of documents updated per user per month over time.
– What are the metadata fields and values in use, and their frequency by department.

I typically export as a CSV, using pipe delimited format (to avoid misinterpreting the ubiquitous comma), and import into a spreadsheet.

What happens when the dataset is too large for Excel? The data I analyze often pushes the limits of Excel or my machine. In that case, I’ll import into SQL Server. To make it repeatable, I’ll create a import process, that drops and recreates the target table, defines the fields, and how to handle errors for each, then export as an SSIS DTSX package.

To run a DTSX package, I’ll import into Business Intelligence Studio as a package and run from there.

Once in SQL Server, one can handle larger datasets, with all the tools and techniques of SQL for dealing with really big data.

The data can be exposed as an SSRS report.

Report on every library in a SharePoint Farm

Report on every library in a SharePoint Farm

It is very useful to be able to examine attributes from every library in a SharePoint farm. Here’s how to generate a report of useful library summary information, such as what level of versioning is enabled, which libraries are configured for QuickLinks and which have Content Types enabled, and how many documents are within each library. I use pipe-separated fields, in case commas are encountered in library fields, such as titles.

Add-PSSnapin "Microsoft.SharePoint.PowerShell" -ErrorAction SilentlyContinue
$mylogfile="L:PowerShellCOREreportingLibSettings.csv"
$envrun="Prod"			# selects environment to run in
if ($envrun -eq "Dev")
{
$siteUrl = "http ://SharePointDev/"
}
elseif ($envrun -eq "Prod")
{
$siteUrl = "http ://sharepoint/"
}
else
{
Write-Host "ENVIRONMENT SETTING NOT VALID: script terminating..."
$siteUrl =  $null;
return;
}
Write-Host "script starting" 
$myheader = "STARTING: $(get-date)"
$Sep="|"
add-content $mylogfile "Site$($sep)Web$($sep)Library$($sep)FileCount$($sep)CTEnabled$($sep)VerMajor$($sep)VerMinor$($sep)QuickLaunch$($sep)MajVerLimit$($sep)MinorVerLimit$($sep)UniquePerms"
$WA=Get-spWebApplication $siteUrl;
foreach ($site in $wa.sites)
{
write-host $site.Url
foreach ($web in $site.allwebs)
{
if ($true) #useful placeholder for filtering. 
{
Write-Host -foregroundcolor darkgreen "$($site.id) - $($site.Url) - $($site.contentdatabase.id) - $($site.contentdatabase.name)"   
for ($i=0;$i -lt $web.Lists.Count;$i++)
{ 
$JPLib=$web.Lists[$i];
$A_Lib_Count++;
$SkipLib=$true; #true
if ( ($JPlib.BaseType -ne "DocumentLibrary") -or ($JPlib.hidden) )
{
# forget the rest and return to top
Write-Host -foregroundcolor green "fast test skipping Library: $($JPlib)";   
}
elseif ($JPLib.Title -Match "SitesAssets|Photo|Image|CustomizedsReports|Templates|Pages|Picture|cache|style|Slide")
{
# forget the rest and return to top
Write-Host -foregroundcolor red "fast test skipping Library because it mentions $Matches: $($JPlib)";   
}
elseif ($JPLib.BaseTemplate -ne "DocumentLibrary")   #alternatively, only skip if -eq XMLForm
{
# forget the rest and return to top
Write-Host -foregroundcolor red "fast skipping Library because it is not of base DocumentLibrary, it is BaseType:$($JPlib.basetemplate): $($JPlib.title)";   
}
elseif (($JPLib.ThumbnailsEnabled) -or ($JPLib.DefaultView -eq "AllSlides"))
{
# forget any library with thumbnails, these are not normal doclibs, and return to top
Write-Host -foregroundcolor red "fast test skipping Library because it has Thumbnails/Slides $($JPlib)";   
}
else
{  $SkipLib=$false;	}
if (!$SkipLib)
{
#write-Host -foregroundcolor green "Processing Library: $($JPlib)";   
$LineOut = "$($site.url)$($sep)$($web.title)$($sep)$($JPLib.title)$($sep)$($JPLib.items.count)$($sep)$($JPLib.ContentTypesEnabled)$($sep)$($JPLib.get_EnableVersioning())$($sep)$($JPlib.get_EnableMinorVersions())$($sep)$($JPLib.OnQuickLaunch)$($sep)$($JPLib.MajorVersionLimit)$($sep)$($JPLib.MajorWithMinorVersionsLimit)$($sep)$($JPLib.HasUniqueRoleAssignments)"
Write-Host $lineOut
add-content $mylogfile $LineOut
}
}
} #foreach site
} #if $true/Siteurl is not null, if environment setup is valid
} #foreach letter
###########################