Mittwoch, 15. August 2018

Papierkörbe einer WebApplication Auflisten & Löschen

​Zur "Bereinigung" einer WebApplication kann man dann- und wann mal die Paprierkörbe ansehen und-/oder löschen.

Ich habe da mal ein keines Skript dazu Vorbereitet:
<# 
.SYNOPSIS  
    List or Remove elements from RecycleBin(s) of a WebApplication 
.DESCRIPTION  
    This script lists- and optionally removes all items from all RecycleBins 
    of a WebApplication, including the End-Users (1st Stage) and 
    Administrators (2nd Stage) RecycleBins.
.NOTES  
    File Name  : Delete-Site-Recycle-Bin.ps1  
    Author     : Nils Andresen - nils.andresen@adesso.de    
.Example
    .\Empty-SPRecycleBin.ps1 -WebApp http://sp.dev/ -FirstStageCleanup RemovePermanent -SecondStageCleanup RemovePermanent
    Removes all deleted items (1st and 2nd stage) from all Sites/Webs of the WebApplication
#>
[CmdletBinding()]
param(
    [Parameter(Mandatory=$True, HelpMessage = "Url to the WebApp")]
    [string]$WebApp,
 
    [Parameter(HelpMessage = "What to do with the 1st-stage Recycle Bins")]
    [ValidateSet("ListOnly", "MoveTo2nd", "RemovePermanent")]
    [string]$FirstStageCleanup = "ListOnly",
 
    [Parameter(HelpMessage = "What to do with the 2nd-stage Recycle Bins")]
    [ValidateSet("ListOnly", "RemovePermanent")]
    [string]$SecondStageCleanup = "ListOnly"
)
 
if((Get-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) -eq $null) {
    Add-PSSnapin Microsoft.SharePoint.PowerShell
}
 
$Global:TotalRemovedSize = 0;
 
function Format-ForPc {
[CmdletBinding()]
param(
    [Parameter(Position=0, 
        Mandatory=$true, 
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true)]
    [int]$size
)
    if($size -lt 1MB) {
        return "{0:0.0#}KB" -f ($size / 1KB);
    } 
    if($size -lt 1GB) {
        return "{0:0.0#}MB" -f ($size / 1MB);
    } 
    if($size -lt 1TB) {
        return "{0:0.0#}GB" -f ($size / 1GB);
    } 
    return "{0:0.0#}TB" -f ($size / 1TB);
}
 
function Process-Web {
[CmdletBinding()]
param(
    [Parameter(Position=0, 
        Mandatory=$true, 
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true)]
    [Microsoft.SharePoint.SPWeb[]]$web
)
Process {
    Write-Verbose "Accessing Web: $($web.Url)";
 
    if((-not $web.RecycleBinEnabled) -or ($web.RecycleBin.Count -lt 1)) {
        return;
    }
    $size = 0;
    $web.RecycleBin | %{ $size += $_.Size }
 
    Write-Output "Web $($web.Title) has $($web.RecycleBin.Count) items ($($size | Format-ForPc)) in Users-RecycleBin";          
         
    switch ($FirstStageCleanup) {
        "ListOnly" {
            $web.RecycleBin | %{ Write-Output "- $($_.ItemType):$($_.Title) ($($_.Size | Format-ForPc), Deleted by $($_.DeletedByName))" }
        }
        "RemovePermanent" {
            $web.RecycleBin.DeleteAll();
            $Global:TotalRemovedSize += $size;
            Write-Output "- Deleted permanently";
        }
        "MoveTo2nd" {
            $web.RecycleBin.MoveAllToSecondStage();
            Write-Output "- Moved to second stage";
        }
    }
}
}
 
function Process-Site {
[CmdletBinding()]
param(
    [Parameter(Position=0,
        Mandatory=$true, 
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true)]
    [Microsoft.SharePoint.SPSite[]]$site
)
Process {
    Write-Verbose "Accessing Site: $($site.Url)";
 
    $secondStage = $site.RecycleBin | ? { $_.ItemState -eq [Microsoft.SharePoint.SPRecycleBinItemState]::SecondStageRecycleBin }
 
    $site.AllWebs | Process-Web
 
    if($secondStage.length -lt 1) {
        return;
    }
    $size = 0;
    $secondStage | %{ $size += $_.Size }
 
    Write-Output "Site $($site.Title) ($($site.Url)) has $($secondStage.Length) items ($($size | Format-ForPc)) in Admin-RecycleBin";          
 
    switch ($SecondStageCleanup) {
        "ListOnly" {
            $secondStage | %{ Write-Output "- $($_.ItemType):$($_.Title) ($($_.Size | Format-ForPc), Deleted by $($_.DeletedByName))" }
        }
        "RemovePermanent" {
            $secondStage | %{ $_.Delete(); }
            $Global:TotalRemovedSize += $size;
            Write-Output "- Deleted permanently";
        }
    }
}
}
 
 
$sa = Start-SPAssignment
$w = Get-SPWebApplication $WebApp -AssignmentCollection $sa;
$w.Sites | Process-Site;
Stop-SPAssignment $sa
if($SecondStageCleanup -eq "RemovePermanent" -and $FirstStageCleanup -eq "MoveTo2nd") {
    Write-Warning "The selected combination of removing from second stage and moving from first to second possibly leaves items undeleted."
}
if($Global:TotalRemovedSize -gt 0) {
    Write-Output "$($Global:TotalRemovedSize | Format-ForPc) were removed permanently.";
}

SharePoint: Auflisten aller Obejkte mit eigener Berechtigungszuweisung

​Die Frage meines Kunden letztens: "Wie kann ich alle Elemente (Listen, Items, etc.) auflisten, bei denen eigene berechtigungszuweisungen erfolgt sind?"

Meine schnelle Antwort: "Nicht möglich, OOTB!".

Mittlerweile weiß ich: In 2007 ging das - aber nur mit dem SharePoint Administrator Toolkit.

Ich habe - mit Inspiration von Mike Smith - ein Skript erstellt​, dass eben dies macht.

<#    
    .SYNOPSIS 
    List all items with broken inheritance
 
    .DESCRIPTION
    Lists all Webs, Lists, Folders and Items with broken inheritance.
    This script was heavily inspired by http://techtrainingnotes.blogspot.de/2014/07/sharepoint-powershellfind-all-broken.html
 
    .PARAMETER SiteCollectionUrl
    Url to SiteCollection
 
    .PARAMETER Site
    Site. Can be Piped in.
 
    .PARAMETER StopAtWeb
    Stop the search at Web-Level. Do not search Lists/Folders/Items.
 
    .PARAMETER StopAtList
    Stop the search at List-Level. Do not search Folders/Items.
 
    .PARAMETER StopAtFolder
    Stop the search at Folder-Level. Do not search Items.
 
    .INPUTS
    Site
 
    .OUTPUTS
    Grid of Securable | Item | Url | Parent
    where
     Securable is one of "Web", "List", "Folder" or "Item"
     Item is the Name or Title
     Url is the url to the item
     Parent is the url to the parent-item
 
 
    .EXAMPLE
    .\Get-SpBrokenInheritances.ps1 -SiteCollectionUrl http://my.lovely.site/
#>
[CmdletBinding()]
param (
    [Parameter(ParameterSetName='SiteByUrl')]
    [string]$SiteCollectionUrl,
     
    [Parameter(ParameterSetName='SiteByObject', ValueFromPipeline=$true)]
    [Microsoft.SharePoint.SPSite]$Site,
 
    [Parameter()]
    [switch]$StopAtWeb = $false,
 
    [Parameter()]
    [switch]$StopAtList = $false,
 
    [Parameter()]
    [switch]$StopAtFolder = $false
)
 
Set-StrictMode -Version Latest
$script:ErrorActionPreference = "Stop";
 
function Get-ParentUrl {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [Microsoft.SharePoint.SPListItem]$Item
    )
    $List = $Item.ParentList;
     
    if ($List.BaseType -eq [Microsoft.SharePoint.SPBaseType]::DocumentLibrary) {
        return "$($_.ParentList.ParentWeb.ServerRelativeUrl)/$($_.File.ParentFolder.Url)";
    } else {
        # SPListItem.Url looks like ////_.000 - I have no idea how to get the folder Url "correctly"
        $FolderUrl = $Item.Url.Substring(0, $Item.Url.LastIndexOf("/"));
        return "$($List.ParentWeb.ServerRelativeUrl)/$FolderUrl";
    }
}
 
function Get-Url {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [Microsoft.SharePoint.SPListItem]$Item
    )
    $List = $Item.ParentList;
    if ($List.BaseType -eq [Microsoft.SharePoint.SPBaseType]::DocumentLibrary) {
        return "$($List.ParentWeb.ServerRelativeUrl)/$($Item.Url)";
    } else {
        # e.g. /blubb/Lists/Test12/DispForm.aspx?ID=1
        return "$($Item.ParentList.DefaultDisplayFormUrl)?ID=$($Item.Id)";
    }
}
 
function Process-Lists {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [Microsoft.SharePoint.SPList[]]$Lists
    )
    $Folders = $Lists | Select -ExpandProperty Folders;
    $Folders | ? { $_.HasUniqueRoleAssignments } | 
        Select @{Label="Securable"; Expression={"Folder"}}, 
            @{Label="Item"; Expression={$_.Title}}, 
            @{Label="Url"; Expression={"$($_.ParentList.ParentWeb.ServerRelativeUrl)/$($_.Url)"}},
            @{Label="Parent"; Expression={"$($_.ParentList.ParentWeb.ServerRelativeUrl)/$($_.ParentList.RootFolder.Url)"}} | Write-Output
 
    if($StopAtFolder) {
        return;
    }
    $Items = $Lists | Select -ExpandProperty Items;
    $Items | ? { $_.HasUniqueRoleAssignments } | 
        Select @{Label="Securable"; Expression={"Item"}}, 
            @{Label="Item"; Expression={$_.Name}}, 
            @{Label="Url"; Expression={Get-Url -Item $_ }},
            @{Label="Parent"; Expression={Get-ParentUrl -Item $_ }} | Write-Output
}
 
function Process-Webs {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        $Webs
    )
    $Lists = $Webs | Select -ExpandProperty Lists | ? { $_.EntityTypeName -ne "PublishedFeedList" -and -not $_.Hidden }
    $Lists | ?{ $_.HasUniqueRoleAssignments } | 
        Select @{Label="Securable"; Expression={"List"}}, 
            @{Label="Item"; Expression={$_.Title}}, 
            @{Label="Url"; Expression={"$($_.ParentWeb.ServerRelativeUrl)/$($_.RootFolder.Url)"}},
            @{Label="Parent"; Expression={$_.ParentWebUrl}} | Write-Output
   
    if($StopAtList) {
        return;
    }
    Process-Lists -Lists $Lists
}
 
function Process-Site {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [Microsoft.SharePoint.SPSite]$Site
    )
    Write-Verbose "Process-Site $($Site.RootWeb.Title)";
    $WebGc = Start-SPAssignment;
    try {
        $Webs = $Site | Get-SPWeb -AssignmentCollection $WebGc -Limit All;
        $Webs | ?{ $_.HasUniquePerm -and $_.ParentWeb -ne $Null } | 
            Select @{Label="Securable"; Expression={"Web"}}, 
                @{Label="Item"; Expression={$_.Title}}, 
                @{Label="Url"; Expression={$_.ServerRelativeUrl}}, 
                @{Label="Parent"; Expression={$_.ParentWeb.ServerRelativeUrl}} | Write-Output
   
        if($StopAtWeb) {
            return;
        }
        Process-Webs -Webs $Webs
    } finally {
        Stop-SPAssignment $WebGc;
    }
}
 
Add-PSSnapin "Microsoft.SharePoint.PowerShell" -ErrorAction Inquire
 
$gc = Start-SPAssignment;
try{
    if(!$Site) {
        Write-Verbose "Site was neither given, nor in Pipe. Fetching Site from Url:$SiteCollectionUrl";
        if(!$SiteCollectionUrl) {
            Write-Error "Neither -Site, nor -SiteCollectionUrl was given.";
            Exit -1;
        }
        $Site = Get-SpSite -Identity $SiteCollectionUrl -AssignmentCollection $gc -ErrorAction SilentlyContinue;
        if ($Site -eq $null)
        {
            Write-Error "No SiteCollection with Identity '$SiteCollectionUrl' found. Exiting...";
            Exit -1;
        }
    }
    Process-Site -Site $Site;
} finally {
    Stop-SPAssignment $gc;
}

Freitag, 10. August 2018

Termstoremanagement mit gegebenen Term öffnen

Die Frage heute beim Kunden: Kann ich das "Term Store Management Tool" an einer bestimmten Stelle im TermStore öffnen - quasi den zu bearbeitenden Term gleich mitgeben? Die Antwort ist "ja" und findet sich in der Datei TermStoreManager.js.

Es kann mit dem query-Parameter termPath ein Pfad zu einem bestimmten Term übergeben werden. Dafür müssen alle IDs aller Eltern-Elemente (TermGroup, -Sets) hintereinander gestellt werden - mit "|"-Getrennt.

Das Ergebnis ist nicht schön, läuft aber. Aus einer beliebigen SP-Seite heraus also einfach den href setzen​:

function getManagementUrl(idPathsArray) {
    return _spPageContextInfo.webAbsoluteUrl + '/' + _spPageContextInfo.layoutsUrl + '/termstoremanager.aspx?termPath=' + idPathsArray.join('|')
}

Credentials in der PowerShell überprüfen lassen

Hintergrund:

Mitten in einem PowerShell-Skript beim Kunden steht "Get-Credentials" - wenn bei der Erfassung der credentials ein Fehler unterläuft, bricht das S​kript ab und man kann von vorne beginnen...

Die Anforderung:

Nach erfassen der Credentials sollen diese auf Gültigkeit überprüft werden.
Es gibt die eine oder andere Frage und auch eine Lösung in der TechNet-Gallery​...

Mir war das leider  nicht "generell" genug. Hier ist meine Lösung:

function Test-Credential { 
<#
    .SYNOPSIS
       checks some credentials.
    
      .PARAMETER Credential
        The Credentials to check
 
      .PARAMETER Retry
        retry on fail.
     
      .EXAMPLE
        Get-Credential dev\developer | Test-Credential -Verbose -Retry
         
#> 
    [OutputType([System.Management.Automation.PSCredential])] 
    [CmdletBinding()]
    Param ( 
        [Parameter( 
            Mandatory = $true, 
            ValueFromPipeLine = $true, 
            ValueFromPipelineByPropertyName = $true
        )] 
        [Alias( 'PSCredential' )] 
        [ValidateNotNull()] 
        [System.Management.Automation.PSCredential] 
        [System.Management.Automation.Credential()] 
        $Credential = [System.Management.Automation.PSCredential]::Empty,
        [switch]$Retry=$false
     ) 
    $processing = $true;
    while($processing) {
        
        $UserName = $Credential.username
        $networkCred = $Credential.GetNetworkCredential();
        $password = $networkCred.Password
        $domain = $networkCred.Domain
        Write-Verbose "Credentials supplied for user: $UserName"
        if(!$domain) {
            if($UserName -match "@") {
                # user was foo@bar.baz...
                Write-Verbose "User in UPN-Form. No domain-parsing will be done.."
            } else {
                # current domain
                $domain = "LDAP://" + ([ADSI]"").DistinguishedName
                Write-Verbose "No domain given. DistinguishedName of current domain: $domain"
            }
        } else {
            Write-Verbose "domain given as $domain in old NT-syntax."
            $test = Get-Command Get-ADDomain -ErrorAction SilentlyContinue
            if($test -eq $null) {
                $guess =  "$($UserName.Substring($domain.length+1))@$domain";
                Write-Verbose "CMDLet Get-ADDomain is not available. Guessing UPN-form for user as $guess";
                $UserName = $guess;
                $domain = "";
            } else {
                Write-Verbose "CMDLet Get-ADDomain is available. Fetching DistinguishedName..";
                $domain = Get-ADDomain $domain -ErrorAction Stop;
                $domain = "LDAP://" + $domain.DistinguishedName;
                Write-Verbose "DistinguishedName of domain is: $domain";
            }        
        }
        
 
        # now re-fetch the current domain using the supplied credentials.
        Write-Verbose "Validating credentials"
        $entry = New-Object System.DirectoryServices.DirectoryEntry($domain,$UserName,$Password)
 
        if ($entry.name -eq $null)
        {
            Write-Verbose "Authentication failed."
            $abort = $false;
            if($Retry) {
                $Credential = Get-Credential -UserName $user -Message "Failed. Please rety..." -ErrorAction SilentlyContinue
                if($Credential -eq $null) {
                    $abort = $true;
                }
            }
            if(!$Retry -or $abort) {
                throw "Validation failed..."
                return $null;
            }
        }
        else
        {
            Write-Verbose "Authentication succeeded."
            $processing = $false;
            return $Credential;
        } 
    }
}

Montag, 6. August 2018

Größe eines Web berechnen

Hat schon einmal jemand versucht die größe eines Web zu berechnen? Für Sites kann man ja einfach das folgende Statement verwenden:

Get-SPSite | select url, @{label="Size in MB";Expression={$_.usage.storage/1MB}}

Bei Webs scheint das ganze etwas komplizierter.
Ich habe mal das folgende Skript verwendet um eine Näherung zu erreichen - ich hoffe ich habe nichts großes "vergessen":

function Get-SPWebSize {
   <#
      .SYNOPSIS
        Measures the size of an SPWeb - be warned that this is only an estimate, 
        as SharePoint has no OOTB function to measure the size of an web.
   
      .PARAMETER Web
        the web to measure
 
      .PARAMETER HumanReadable
        whether the output should be human-readable (KB, MB, GB, TB)
   
      .EXAMPLE
        Get-SPWeb path/to/web | Get-SPWebSize -HumanReadable GB
   #>
   [CmdletBinding()]
   param (
      [Parameter(Mandatory=$true, ValueFromPipeline=$True, HelpMessage = "the SPWeb to measure")]
      [Microsoft.SharePoint.SPWeb]$Web,
      [Parameter(Mandatory=$false, HelpMessage = "set to a size to measure in (e.g. KB or GB)")]
      [ValidateSet("None", "KB", "MB", "GB", "TB")]
      [string]$HumanReadable = "None",
      [Parameter(Mandatory=$false, HelpMessage = "Suppress the startup-warning")]
      [Switch]$SuppressWarn = $false
 
   )
   if(-not $SuppressWarn) {
      Write-Warning "Be aware, that the calculated size is only an estimate!";
   }
   if((Get-PSSnapin Microsoft.SharePoint.PowerShell -ErrorAction SilentlyContinue) -eq $null) {
      Add-PSSnapin Microsoft.SharePoint.PowerShell
   }
   function Get-FolderSize {
      [CmdletBinding()]
      param (
         [Parameter(Mandatory=$true, ValueFromPipeline=$True)]
         [Microsoft.SharePoint.SPFolder]$Folder
      )
      [long]$size = 0;
      $Folder.Files | %{
         $size += $_.Length;
      }
      $Folder.SubFolders | %{
         $size += $_ | Get-FolderSize
      }
      Write-Verbose "Folder $($Folder.ParentWeb.Url +"/"+ $Folder.Url) has size of $size";
      $size
   }
   function Get-SubWebSize {
      [CmdletBinding()]
      param (
         [Parameter(Mandatory=$true, ValueFromPipeline=$True)]
         [Microsoft.SharePoint.SPWeb]$Web
      )
      [long]$size = 0;
      $Web.Folders | %{
         $size += $_ | Get-FolderSize
      }
      $Web.Webs | %{
         $size += $_ | Get-SubWebSize
      }
      Write-Verbose "Web $($Web.Url) has size of $size";
      $size
   }
    
   $size = $Web | Get-SubWebSize
   if ($HumanReadable -ne "None") {
      $size = Invoke-Expression "$size / 1$HumanReadable";
   } 
   $size
}

PowerShell CmdLet reflecten

Meistens muss ich alles ganz genau wissen: Z.B. wie ein spezielles PowerShell-CmdLet implementiert ist.
Mittels des folgenden kleinen Skriptes​ kann man ganz schnell die implementierende DLL zu einem CmdLet finden und notfalls auch schon mal einen decompiler starten.​
Die Inspiration kommt von OISIN GREHAN​.

<#  
.SYNOPSIS  
    Run reflection on a given commandlet
.DESCRIPTION  
    Run reflection on any CmdLet
.NOTES  
    This code was heavily inspired from OISIN GREHAN, see http://www.nivot.org/post/2008/10/30/ATrickToJumpDirectlyToACmdletsImplementationInReflector
.Example
    Get-Command Get-ChildItem | Reflect-Cmdlet -Reflect ShowDllOnly
.Example
    Reflect-Cmdlet -CmdLet (Get-Command Get-ChildItem) -Reflect ShowDllOnly
#>
[CmdletBinding()]
param(
    [Parameter(Position=0, 
        Mandatory=$true, 
        ValueFromPipeline=$true,
        ValueFromPipelineByPropertyName=$true,
        HelpMessage = "The CmdLet to reflect")]
    [Management.Automation.CommandInfo]$CmdLet,
 
    [Parameter(HelpMessage = "Which reflector to use.. ")]
    [ValidateSet("ShowDllOnly", "Reflector", "JustDecompile", "ILSpy")]
    [string]$Reflect = "ShowDllOnly"
)
      
# resolve to command if this is an alias  
while ($CmdLet.CommandType -eq "Alias") {  
    $def = $CmdLet.definition;
    Write-Verbose "$CmdLet is an alias. Using Definition: $def";
    $CmdLet = Get-Command $def
}  
Write-Verbose "Reflecting $CmdLet.";
       
$name = $CmdLet.ImplementingType      
$DLL = $CmdLet.DLL  
if($DLL -eq $null) {
    Write-Warning "$CmdLet is not implemented in any DLL. Possibly a script?";
    if($CmdLet.Path -ne $null) {
         Write-Warning "Have a look at: $($CmdLet.Path)"
    }
    #$CmdLet | gm
    Exit;
}
 
Write-Verbose "Type:$name, DLL:$DLL";
switch ($Reflect) {
    "ShowDllOnly" {
        Write-Output "$CmdLet is implemented in $name in the dll:$DLL";
    }
    "Reflector" {
        if (-not (Get-Command reflector.exe -ErrorAction SilentlyContinue)) {  
            throw "Reflector.exe is not in your path."
        }  
        Write-Verbose "Starting Reflector.";
        reflector /select:$name $DLL
    }
    "JustDecompile" {
        $regKey = Get-Item HKCU:\Software\Telerik\JustDecompile -ErrorAction SilentlyContinue
        if($regKey -eq $null) {
            throw "It seems JustDecompile is not installed."
        }
        $exe = $regKey.GetValue("ExecutablePath") ;
        Write-Verbose "invoking $exe";
        &$exe """$DLL""" ; #TODO: select the right type...
    }
    "ILSpy" {
      if (-not (Get-Command ilspy.exe -ErrorAction SilentlyContinue)) {  
            throw "ilspy.exe is not in your path."
        }  
      Write-Verbose "Starting ILSpy.";
      ilspy $DLL /navigateTo:T:$name
    }
}