Analysing WordPress stats

It’s very useful to know what are the best posts on your blog and what are the most frequent search terms used before reaching your blog, especially when there’s a sudden spike in stats.

I’ve been using the F12 (Developer tool) in IE10 and the Network capture to figure out how to logon to WordPress. I won’t be able to explain it as I’m not a web developer but I can show how I did it in the following function 🙂

But first pls let me also introduce the awsome Get-Matches function I borrowed from Herr Dr. Tobias Weltner on this page.

function Get-Matches            
{            
    param(            
        [Parameter(Mandatory=$true)]            
        $Pattern,            
            
        [Parameter(ValueFromPipeline=$true)]            
        $InputObject            
    )            
    begin {            
        try {            
            $regex = New-Object Regex($pattern)            
        } catch {            
            Throw "Get-Matches: Pattern not correct. '$Pattern' is no valid regular expression."            
        }            
        $groups = @($regex.GetGroupNames() | Where-Object { ($_ -as [Int32]) -eq $null } | ForEach-Object { $_.toString() })            
    }            
    process {            
        foreach ($line in $InputObject)            
        {            
            foreach ($match in ($regex.Matches($line)))            
            {            
                if ($groups.Count -eq 0)            
                {            
                    ([Object[]]$match.Groups)[-1].Value            
                } else {            
                    $rv = 1 | Select-Object -Property $groups            
                    $groups | ForEach-Object { $rv.$_ = $match.Groups[$_].Value }            
                    $rv            
                }            
            }            
        }            
    }            
}

Did I say that Powershell V3 rocks!? Enjoy 😉

Function Get-WordpressStats {
[CmdletBinding()]
param(

    [Parameter()]
    [ValidateRange(-1,365)]            
    [system.int32]$Days=1,

    [parameter(Mandatory)]
    [System.String]$WPPassword=$null,

    [parameter(Mandatory)]
    [System.String]$WPUserName=$null,

    [parameter(ParameterSetName='SearchTerms')]
    [switch]$SearchTerms=$false,
    
    [parameter(ParameterSetName='PostViews')]
    [switch]$PostViews=$false
)
Begin {

    switch ($PsCmdlet.ParameterSetName)
    {
        SearchTerms {
            if ($SearchTerms) { $Action = 'searchterms' ; break}
        }
        PostViews {
            if ($PostViews) { $Action = 'postviews' ; break}
        }
        default { 
            # We will never get here because if both parametersets/switch are specified or missing
            # it will throw an error saying: 'Parameter set cannot be resolved using the specified named parameters.'
        }
    }
    $statsURI = "http://wordpress.com/wp-admin/admin-ajax.php?view=$Action&summarize&numdays=$Days&action=wpcom_load_template&template=my-stats.php"
    $UA = 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.2; WOW64; Trident/6.0)'
    $HT = @{
        UserAgent = $UA
        ContentType = 'application/x-www-form-urlencoded'
        Method = 'Post'
        Body = "log=$WPUserName&pwd=$WPPassword&rememberme=forever&wp-submit=Log+In&testcookie=1"
    }

    # Authenticate
    $res1  = Invoke-WebRequest -Uri https://en.wordpress.com/wp-login.php @HT
    $res2  = Invoke-WebRequest -Uri $res1.BaseResponse.ResponseUri @HT
    $cookie1 = ($res1.BaseResponse.Headers.GetValues('Set-Cookie')[0] -replace ';.*','')
    $cookie2 =  $res2.BaseResponse.Headers.GetValues('Set-Cookie')    -split ";" -split "=\+" -match "^\S+=\S"
    $wc = New-Object System.Net.WebClient
    $wc.Headers.Add("User-Agent", $UA)
    $wc.Headers.Add("Cookie", (( $cookie1,($cookie2 -join '; ')," TESTCOOKIE=home; wordpress_eli=1; hc_post_as=wordpress") -join '; '))
}
Process {
    $result = $wc.DownloadString($statsURI)
    'Showing results for: {0}' -f ($result  | Get-Matches '<h4>(?<date>.*?)</h4>').Date
    switch($Action)
    {
        'postviews' {
            $titles =  $result  | Get-Matches '<tr((\sclass="alternate")|.?)><td\sclass="label"><span\sclass=.*?><a\shref=.*?>(?<title>.*?)</a></span></td>'
            $views  =  $result  | Get-Matches '<td\sclass="more"><a\shref=.*?><img\ssrc=.*?/></a></td><td\sclass="views">(?<views>.*?)</td></tr>'
            $i = 0
            $titles | ForEach-Object {
                New-Object -TypeName PSObject -Property @{
                    Title = $_.Title -replace "&nbsp;"," "
                    Views = $views[$i].views
                }
                $i++
             }
        }
        'searchterms' {
            $result  | Get-Matches  '(<tr((\sclass="alternate")|.?)><td\sclass="label">(?<label>.*?)</td><td\sclass="views">(?<views>.*?)</td></tr>)' | ForEach-Object -Process {
                New-Object -TypeName PSObject -Property @{
                    Terms = $_.Label
                    Views = [int]($_.Views.Trim())
                }
            }
        }
        default {}
    }
}
End {}
}

Install Volume License (MAK) key and activate Windows 8

I’ve always wondered why Microsoft did not turn the slmgr.vbs script (Software Licensing Manager) to Powershell.

There’s now in V3 a cmdlet called Set-WindowsProductKey coming from the DISM built-in module. It’s seems to have a promising -Online parameter according to the following online help
online switch
However it doesn’t work. I believe that the DISM module is primarily used for servicing offline images and that the Online parameterSet hasn’t been implemented yet 😦
online fails

Ed Wilson, the Hey Scripting Guy showed us yesterday how to check for the license status of Windows 8. It’s a nice valuable article that you can read on this page.

I propose to show how to actually install a volume license and activate it

# Requires -version 3.0

Function Set-VLkey
{
[CmdletBinding()]
param(

    [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]            
    [Alias("CN","__SERVER","IPAddress")]            
    [system.string[]]$ComputerName = ".",

    [parameter()]
    [System.Management.Automation.PSCredential]$Credential,

    [parameter(Mandatory)]
    [ValidatePattern('(([0-9A-Z]{5}\-)){4}([0-9A-Z]{5})$')]    
    [system.string]$Key=$null
)
Begin {
    # Make sure we run as admin            
    $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()            
    $IsAdmin = $usercontext.IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")                               
    if (-not($IsAdmin))            
    {            
        Write-Warning "Must run powerShell as Administrator to perform these actions"            
        return            
    }
    # Define some registry constants            
    $HKLM = 2147483650
    $Key1 = 'SOFTWARE\Microsoft\Windows NT\CurrentVersion\SoftwareProtectionPlatform'
    $Key2 = 'SOFTWARE\Wow6432Node\Microsoft\Windows NT\CurrentVersion\SoftwareProtectionPlatform'
}    
Process {
    $ComputerName | ForEach-Object -Process {
        $TargetComputer = $_
        # Prepare a default HT            
        $wmiHT = @{            
            ErrorAction = "Stop";            
        }            
        Switch ($TargetComputer)
        {
            {$_ -eq ($Env:Computername)} {$IsRemoteComputer = $false ; break}
            {$_ -eq "."}                 {$IsRemoteComputer = $false ; break}
            {$_ -eq "::1"}               {$IsRemoteComputer = $false ; break}
            {$_ -eq "127.0.0.1"}         {$IsRemoteComputer = $false ; break}
            {$_ -eq "localhost"}         {$IsRemoteComputer = $false ; break}
            default {
                $IsRemoteComputer = $true
            }            
        }
        if($IsRemoteComputer)
        {
            $wmiHT += @{ 
                Authentication = 6;
                Impersonation  = 3;
                Computername   = $TargetComputer;
            }
            if ($Credential)
            {
                $wmiHT += @{ Credential = $Credential }
            }
        }
        # Get the SPP service version on the remote machine
        try
        {
            $SLsvc = Get-WmiObject -Query 'Select Version from SoftwareLicensingService' @wmiHT
        } catch {
            Write-Warning -Message "Failed to query WMI on $TargetComputer because $($_.Exception.Message)"
        }
        if ($SLsvc)
        {
            $Version = $SLsvc.Version
        } else {
            # We could not connect, skip that computer
            return
        }
        if($IsRemoteComputer)
        {
            # Make sure the remote computer is W8/2012 as it seems that: 
            # 'The Windows 8 version of SLMgr.vbs does not support remote connections to Vista/WS08 and Windows 7/WS08R2 machines'
            if (-not([version]$Version -gt ([version]'6.2')))            
            {            
                Write-Warning -Message "The remote machine $TargetComputer does not support this version of SLMgr.vbs"
                return            
            }
        }        
        # Here we go!
        try
        {
            Write-Verbose -Message "Installing product key $MAK on $TargetComputer"
            Invoke-WmiMethod -InputObject $SLsvc -Name InstallProductKey -ArgumentList $MAK @wmiHT | Out-Null
            Invoke-WmiMethod -InputObject $SLsvc -Name RefreshLicenseStatus @wmiHT | Out-Null
            $SLProduct = Get-WmiObject -Query 'Select * FROM SoftwareLicensingProduct WHERE PartialProductKey <> null' @wmiHT
            Invoke-WmiMethod -Path "ROOT\DEFAULT:StdRegProv" -Name SetStringValue -ArgumentList $HKLM,$Key1,$Version,"KeyManagementServiceVersion" @wmiHT | Out-Null
            Invoke-WmiMethod -Path "ROOT\DEFAULT:StdRegProv" -Name SetStringValue -ArgumentList $HKLM,$Key2,$Version,"KeyManagementServiceVersion" @wmiHT | Out-Null
            'Installed product key {0} successfully on {1}.' -f $MAK,$TargetComputer
            # ' Avoid using a MAK activation count up unless needed'
            'Activating {0} ({1}) ...' -f ($SLProduct.Name),($SLProduct.ID)
            if (($SLProduct.Description -notmatch "MAK") -or ($SLProduct.LicenseStatus -ne 1))
            {
                Write-Verbose -Message "Attempting to activate product on $TargetComputer"
                Invoke-WmiMethod -InputObject $SLProduct -Name Activate @wmiHT | Out-Null
                Invoke-WmiMethod -InputObject $SLSvc -Name RefreshLicenseStatus @wmiHT | Out-Null
            }
            'Product activated successfully on {0}.' -f $TargetComputer
        } catch {
            Write-Warning -Message "Failed to install key and activate computer $TargetComputer because $($_.Exception.Message)"
            return
        }
    }
}
End {}
<#
.SYNOPSIS    
    Set the volume license key
 
.DESCRIPTION  
    Install a volume license key and activate it online if required

.PARAMETER ComputerName
    Array of computers to target
 
.PARAMETER Credential
    Specify the credential to use to target remote computers

.PARAMETER Key
    ProductKey to be installed in format '12345-12345-12345-12345-12345'

.EXAMPLE    
    Set-VLkey -Key '12345-12345-12345-12345-12345'

.EXAMPLE    
    Set-VLkey -Key '12345-12345-12345-12345-12345' -Verbose 

.EXAMPLE    
     "RemotePC1","RemotePC2" | Set-VLkey -Key '12345-12345-12345-12345-12345' -Credential (Get-Credential) -Verbose 

.NOTES    
    Name: Set-VLkey
    Author: Emin Atac
    DateCreated: 22/08/2012
 
.LINK    
    https://p0w3rsh3ll.wordpress.com
 
#>
}

Something strange also with the SetStringValue and the Invoke-WMIMethod. I have had to invert the ValueName and the Value to have it correctly set in the registry whereas it’s mentioned differently on this page.

Deciphering error codes

If you’ve been working with WMI or SCCM or just once encountered an error code that needs to be first converted to be troubleshooted (I mean have hits to MSDN or Technet or support.microsoft.com in Google), Powershell is your friend 🙂

I’ve been working to convert some vbscript to powershell that had to constants written with an hexadecimal notation

private const HKEY_LOCAL_MACHINE = &H80000002

Converted to powershell, we would simply write

$HKLM = 0x80000002

But later on, while I was working with modifying the registry through WMI (using “ROOT\DEFAULT:StdRegProv”), I had a dilema.
Should the HKEY_LOCAL_MACHINE be written like this

$HKLM = 2147483650

or

$HKLM = 0x80000002

Actually both notations are correct and only their type are different.

To figure it out, I did:

0x80000002 | gm
-2147483646 -eq 0x80000002
2147483650 | gm
"{0:X0}" -f ([int64]2147483650)

The following resources also helped me figure it out

The first parameter is the value to convert and the second value is the base (i.e. 2 for binary, 8 for octal, 10 for decimal, and 16 for hexadecimal).

Note also that the WMI registry provider “ROOT\DEFAULT:StdRegProv” and Invoke-WMIMethod cmdlet will accept the long integer and not the hexadecimal/int32.
I’ve also found a list of registry hives with both values on this page :

HKEY_CLASSES_ROOT (2147483648 (0x80000000))
HKEY_CURRENT_USER (2147483649 (0x80000001))
HKEY_LOCAL_MACHINE (2147483650 (0x80000002))
HKEY_USERS (2147483651 (0x80000003))
HKEY_CURRENT_CONFIG (2147483653 (0x80000005))
HKEY_DYN_DATA (2147483654 (0x80000006))

I couldn’t resist and quickly wrote a function that would help deciphering any of these error codes whatever their type:

Function Get-ErrorCode {
[CmdletBinding()]
param(
    [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
    [System.Object[]]$InputObjet
)
Begin {}
Process {
    $InputObjet | ForEach-Object -Process {
        $hex = $int64 = $int32 = $null
        Write-Verbose -Message "Dealing with $_ as $($_.GetType())"
        Switch ($_)
        {
            {$_ -is [string]}  {
                $hex = "{0:X0}" -f ([int32]$_)
                $int64 = [Convert]::ToInt64($hex,16)
                $int32 = $_
                break
            }
           {$_ -is [int32]}  {
                $hex = "{0:X0}" -f ([int32]$_)
                $int64 = [Convert]::ToInt64($hex,16)
                $int32 = $_
                break
            }
            {$_ -is [int64]}  {
                $hex = "{0:X0}" -f ([int64]$_)
                $int64 = $_
                $int32 = [Convert]::ToInt32($hex,16)
                break
            }
            default {}
        }
        New-Object -TypeName psobject -Property @{
            Hexadecimal = "0x$hex"
            Int32 = $int32
            Int64 = $int64
        }
    }
}
End {}
}

To test the above function, you can do for example:


Get-ErrorCode -2147483646 -Verbose
Get-ErrorCode -InputObjet (-2147483646) -Verbose
2147483650,0x80000002,-2147483646 | Get-ErrorCode -Verbose | fl -Property *

Convert vbscript to powershell used in WinPE

Now that we have powershell V3 in WinPE4, the last vbscript I’ve been using can be converted.
I don’t take credits for writing this vbscript, I’ve downloaded it from someone who published it as ‘untoy.vbs’ on his blog that disappeared years ago.
I was using it to inject computernames and other preferences in either the unattend.xml used by setup.exe to install Vista+ computers or the unattend.xml used to boot syspreped images, located in C:\Windows\Panther.

Here is the vbscript

	Dim args
	Dim numargs
	Dim config
	Dim source_unattend
	Dim target_unattend

	Dim OldXMLNode
	Dim NewXMLNode
	Dim parent

	Set args = WScript.Arguments
	numargs = args.Count
		
	'check if user supplied 3 arguments
	If(numargs < 3) Then
            WScript.Echo "Usage: [CScript | WScript] untoy.vbs config source_unattend target_unattend"
            WScript.Quit 1
        End If
		
		config = args.Item(0)
		source_unattend = args.Item(1)
		target_unattend = args.Item(2)
		
        'load up source unattend file
        Set SourceXMLDoc = CreateObject("MSXML2.DOMDocument")
        SourceXMLDoc.Async = False
	SourceXMLDoc.load source_unattend
        If SourceXMLDoc.parseError.errorCode <> 0 Then
	        WScript.Echo "Error parsing source unattend file"
	        WScript.Quit 1
        End If
        
        'load up config file
        Set objFSO = CreateObject("Scripting.FileSystemObject")
        Set objTextFile = objFSO.OpenTextFile(config, 1)
        Dim ConfigArray()
        intSize = -1
        
        Do While objTextFile.AtEndOfStream <> True
            str = objtextFile.Readline
            If inStr(str, ",") Then
                tempArr = split(str, ",")
                intSize = intSize + 2
                ReDim Preserve ConfigArray(intSize)
                ConfigArray(intSize - 1) = tempArr(0)
                ConfigArray(intSize) = tempArr(1)
            Else
                objTextFile.Skipline
            End If
        Loop
 
        'check if config file was empty
        If(intSize = -1) Then
            WScript.Echo "Nothing in config file."
            WScript.Quit 1
        End If

        'apply config file changes to source XML
        SourceXMLDoc.setProperty "SelectionNamespaces", "xmlns:un='urn:schemas-microsoft-com:unattend'"
        SourceXMLDoc.setProperty "SelectionLanguage", "XPath"
        
        Set OldXMLNode = SourceXMLDoc.documentElement.SelectSingleNode(ConfigArray(i))

        i = 0
        Do While i <= intSize
            Set OldXMLNode = SourceXMLDoc.documentElement.SelectSingleNode(ConfigArray(i))
	    if OldXMLNode is Nothing Then
	     wscript.echo "No match for " & ConfigArray(i)
 	    else
 	        Set ParentNode = OldXMLNode.parentNode
	       	Set NewXMLnode = OldXMLNode
           	NewXMLNode.text = ConfigArray(i+1)           
            	ParentNode.replaceChild NewXMLNode, OldXMLNode
	    end if
            i = i + 2
        Loop

        'save modified source XML to target file
        SourceXMLDoc.Save target_unattend

Here’s a sample XML, I’ve highlithed what has to be changed by the above vbscript


<?xml version="1.0" encoding="utf-8"?>
<unattend xmlns="urn:schemas-microsoft-com:unattend">
    <settings pass="windowsPE">
    </settings>
    <settings pass="offlineServicing">
    </settings>
    <settings pass="specialize">
        <component name="Microsoft-Windows-Shell-Setup" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <OEMInformation>
                <HelpCustomized>false</HelpCustomized>
            </OEMInformation>
            <BluetoothTaskbarIconEnabled>false</BluetoothTaskbarIconEnabled>
            <ShowWindowsLive>false</ShowWindowsLive>
            <TimeZone>Romance Standard Time</TimeZone>
            <ComputerName>ChangeThisComputerName</ComputerName>
        </component>
        <component name="Microsoft-Windows-UnattendedJoin" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS" xmlns:wcm="http://schemas.microsoft.com/WMIConfig/2002/State" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
            <Identification>
		<Credentials>
			<Domain>DomainName</Domain>
			<Username>UserName</Username>
			<Password>Password</Password>
		</Credentials>
                <JoinDomain>Domain Name to Join</JoinDomain>
            </Identification>
        </component>
    </settings>
    <settings pass="oobeSystem">
    </settings>
    <cpi:offlineImage cpi:source="catalog:d:/distrib/x64/os/sources/install_windows 7 enterprise.clg" xmlns:cpi="urn:schemas-microsoft-com:cpi" />
</unattend>

Inside my custom WinPE image, I used it like this:
(where %_TARGETARCH% is either amd64 or x86, _PCNAME is the target computername,…)


:: Dump config to a file
echo //un:settings[@pass='specialize']//un:component[@name='Microsoft-Windows-Shell-Setup' and @processorArchitecture='%_TARGETARCH%']/un:ComputerName,%_PCNAME%>C:\$OEM$\$$\config.txt
echo //un:settings[@pass='specialize']//un:component[@name='Microsoft-Windows-UnattendedJoin' and @processorArchitecture='%_TARGETARCH%']/un:Identification/un:Credentials/un:Password,%_INSTPW%>>C:\$OEM$\$$\config.txt
echo //un:settings[@pass='specialize']//un:component[@name='Microsoft-Windows-UnattendedJoin' and @processorArchitecture='%_TARGETARCH%']/un:Identification/un:Credentials/un:Domain,%_WINSDOM%>>C:\$OEM$\$$\config.txt
echo //un:settings[@pass='specialize']//un:component[@name='Microsoft-Windows-UnattendedJoin' and @processorArchitecture='%_TARGETARCH%']/un:Identification/un:Credentials/un:Username,%_ACCOUNTNAME%>>C:\$OEM$\$$\config.txt

:: Create the final xml from our template and config file
cscript /e:vbscript %systemroot%\system32\untoy.txt C:\$OEM$\$$\config.txt %systemroot%\system32\unattend.xml C:\unattend.xml
  • Method1: Using COM objects
$oldxml = New-Object -ComObject MSXML2.DOMDocument            
            
# Here, I'll define an array for testing but it could also be populated by a Get-Content C:\$OEM$\$$\config.txt...            
$config = @()            
$config += "//un:settings[@pass='specialize']//un:component[@name='Microsoft-Windows-Shell-Setup' and @processorArchitecture='amd64']/un:ComputerName,test"            
            
# Loading will return a boolean            
if ($oldxml.load("$env:systemroot\system32\unattend.xml"))            
{            
            
    # Split each line of the array                
    $newconfig = @()            
    $config | ForEach-Object -Process {            
        $newconfig += @{ XPath = ($_ -split ",")[0] ; Value = ($_ -split ",")[1] }            
    }            
    # Apply changes            
    $oldxml.setProperty("SelectionNamespaces", "xmlns:un='urn:schemas-microsoft-com:unattend'")            
    $oldxml.setProperty("SelectionLanguage", "XPath")            
    $newconfig | Foreach-Object -Process {            
        $OldXMLNode = $oldxml.documentElement.SelectSingleNode($_.XPath)            
        $ParentNode = $OldXMLNode.parentNode            
        $NewXMLnode = $OldXMLNode            
        $NewXMLNode.text = $_.Value            
        $ParentNode.replaceChild($NewXMLNode,$OldXMLNode)            
    }            
    # Save changes to the target file            
    $oldxml.save('C:\unattend.xml')            
            
}            
  • Method2: Using .NET objects without Select-XML
# Here, I'll define an array for testing but it could also be populated by a Get-Content C:\$OEM$\$$\config.txt...            
$config = @()            
$config += "//un:settings[@pass='specialize']//un:component[@name='Microsoft-Windows-Shell-Setup' and @processorArchitecture='amd64']/un:ComputerName,test"            
            
# Create .Net object            
$oldxml = New-Object System.Xml.XmlDocument             
            
# Loading doesn't return a boolean so catch exceptions            
try {            
    $oldxml.Load("$env:systemroot\system32\unattend.xml")            
    $loaded = $true            
} catch {            
    $loaded = $false            
    Write-Warning -Message 'Failed to load XML file'            
}            
            
# http://stackoverflow.com/questions/4633127/how-to-select-xml-nodes-with-xml-namespaces-from-an-xmldocument            
# You have to declare the  namespace prefix using an XmlNamespaceManager before you can use it in XPath expressions            
$xmlNameTable = new-object System.Xml.NameTable            
$xmlNameSpace = new-object System.Xml.XmlNamespaceManager($xmlNameTable)            
$xmlNameSpace.AddNamespace("un",'urn:schemas-microsoft-com:unattend')            
            
if ($loaded)            
{            
    # Split each line of the array                
    $newconfig = @()            
    $config | ForEach-Object -Process {            
        $newconfig += @{ XPath = ($_ -split ",")[0] ; Value = ($_ -split ",")[1] }            
    }            
            
    # Apply changes            
    $newconfig | Foreach-Object -Process {            
        $OldXMLNode = $oldxml.DocumentElement.SelectSingleNode($_.XPath,$xmlNameSpace)            
        $ParentNode = $OldXMLNode.ParentNode            
        $NewXMLnode = $OldXMLNode            
        $NewXMLnode.'#text' = $_.Value            
        $ParentNode.replaceChild($NewXMLnode,$OldXMLNode)            
    }            
            
    # Save changes to the target file            
    $oldxml.save('C:\unattend.xml')            
}
  • Method3: Using .NET objects with Select-XML
# Here, I'll define an array for testing but it could also be populated by a Get-Content C:\$OEM$\$$\config.txt...            
$config = @()            
$config += "//un:settings[@pass='specialize']//un:component[@name='Microsoft-Windows-Shell-Setup' and @processorArchitecture='amd64']/un:ComputerName,test"            
            
# Create .Net object            
$oldxml = New-Object System.Xml.XmlDocument             
            
# Loading doesn't return a boolean so catch exceptions            
try {            
    $oldxml.Load("$env:systemroot\system32\unattend.xml")            
    $loaded = $true            
} catch {            
    $loaded = $false            
    Write-Warning -Message 'Failed to load XML file'            
}            
            
if ($loaded)            
{            
    # Split each line of the array                
    $newconfig = @()            
    $config | ForEach-Object -Process {            
        $newconfig += @{ XPath = ($_ -split ",")[0] ; Value = ($_ -split ",")[1] }            
    }            
            
    # Define a namespace as a hashtable            
    $ns = @{ un = 'urn:schemas-microsoft-com:unattend'}            
            
    $newconfig | ForEach-Object -Process {            
            
        $OldXMLNode =  $oldxml | Select-Xml -XPath $_.XPath -Namespace $ns            
        $NewXMLnode = $OldXMLNode            
        $NewXMLnode.Node.'#text' = $_.Value            
        $ParentNode = $OldXMLNode.Node.ParentNode            
        $ParentNode.ReplaceChild($NewXMLnode.Node,$OldXMLNode.Node)            
    }            
            
    # Save changes to the target file            
    $oldxml.save('C:\unattend.xml')            
            
}            
            

Discovering Usage of Keys under 1024 Bits

Microsoft announced on this blog post

If you are currently working with App-V, SCVMM, Hyper-V, SCCM, or any management environment leveraging certificates, it is important to be made aware of a very important update being released next week.

Next week a security fix will be widely distributed which will prevent use of certificates which use weak (less than 1024 bit) RSA keys. Microsoft will issue a critical non-security update (KB 2661254) for Windows XP, Windows Server 2003, Windows Server 2003 R2, Windows Vista, Windows Server 2008, Windows 7, and Windows Server 2008 R2. The update will block the use of cryptographic keys that are less than 1024 bits.

They encouraged every IT admin to inventory the usage of keys under 1024 bits before running into troubles. They show on this blog post how to do it.

Here is the powershell way 🙂

  • Method 1: Just browse your certificates stores
Get-Item -Path cert: |             
   ForEach-Object -Process {            
    Get-ChildItem -Recurse -Force -Path $_.PSPath |             
        ForEach-Object -Process {             
            New-Object -TypeName PSObject -Property @{             
                KeySize = $_.PublicKey.Key.KeySize;            
                Path = $_.PSParentPath;            
                'Expiration Date' = $_.NotAfter;            
                'Issued To' =  $_.Subject;            
                'Issued By' = $_.Issuer;            
                Thumbprint =$_.Thumbprint            
            }            
        }            
    } | Where-Object {            
    if ($_.KeySize)            
    {            
        $_.KeySize | Select-String -Pattern  @(4096,2048,1024) -Notmatch            
    }            
}            
  • Method 2: Use the recommended method from Microsoft

# Method 2: Discovering Usage of Keys under 1024 Bits in Cryptographic Operations

# Step1: Enable verbose diagnostic logging.

    # Add a DWORD (32-bit) value DiagLevel with value of 0x00000005
    Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Services\Crypt32' -Name "DiagLevel" -Type DWORD -Value 5

    # Add a QWORD (64-bit) value DiagMatchAnyMask with value of 0x00ffffff
    Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Services\Crypt32' -Name "DiagMatchAnyMask" -Type QWORD -Value 0x00ffffff

# Step2: Enable CAPI2 operational logging

    $log = New-Object -TypeName System.Diagnostics.Eventing.Reader.EventLogConfiguration -ArgumentList "Microsoft-Windows-CAPI2/Operational"
    $log.isEnabled = $true
    try {
        $log.SaveChanges()
    } catch {
        Write-Warning -Message "Failed to save changes because $($_.Exception.Message)"
    } 

# Step3: Query the logs

    $XMLquery = @"
    <QueryList>
        <Query Id="0" Path="Microsoft-Windows-CAPI2/Operational">
            <Select Path="Microsoft-Windows-CAPI2/Operational">Event[UserData[CertGetCertificateChain[CertificateChain[ChainElement[PublicKeyAlgorithm[@publicKeyLength='384']]]]] and
            UserData[CertGetCertificateChain[CertificateChain[ChainElement[PublicKeyAlgorithm[@publicKeyName='RSA']]]]]]
            or
            Event[UserData[CertGetCertificateChain[CertificateChain[ChainElement[PublicKeyAlgorithm[@publicKeyLength='512']]]]] and
            UserData[CertGetCertificateChain[CertificateChain[ChainElement[PublicKeyAlgorithm[@publicKeyName='RSA']]]]]]</Select>
        </Query>
    </QueryList>
"@

try {
    Get-WinEvent -FilterXml $XMLquery -ErrorAction Stop
} catch {
    Write-Warning -Message "XML query failed because $($_.Exception.Message)"
}
  • NB1: You must have admin credentials to perform all the above operations, of course.
  • NB2: The above code also shows how to enable and disable event log tracing. I’ve used the following article to achieve it: How to: Configure and Read Event Log Properties
  • Working with user profiles

    • The context

    I’ve got a pool of Windows 7 virtual machines dedicated to people who are out of office.
    These VMs aren’t configured with differentiacing because we want them to be maintained by System Center Configuration Manager and have them applying security updates for the operating system as well as applications. In other words, they aren’t based on a VDI template, don’t get reset and patched through an offline imaging process. So, the major problem with such a scenario is that after a certain amount of time and many users login on, the VMs’ system drives are getting full. The solution is easy and consists in launching a Powershell script on a regular basis through the tasks scheduler that does the following:

      • Check the freespace left on the systemdrive
      • based on a threshold specified as parameter (whether there’s less than 20% of freespace left on the drive for example) take remediation actions
      • Get additional information on user profiles size
      • Delete profiles older that x days specified as parameter
      • Get the freespace left on the systemdrive after user profiles removal
      • Send a nice HTML based report to the admin
    • Step 1: Getting the list of user profiles

    There are actually two ways to get the list of user profiles.
    We can either query the registry under ‘HKLM\software\microsoft\windows nt\currentversion\profilelist’ or use the win32_userprofile WMI class
    Ed Wilson, the Hey scripting guy shows these 2 techniques in the following article entitled “Use PowerShell to Find User Profiles on a Computer”
    Well, there’s also a more straight forward technique that consists in querying the last write time of the ntuser.dat (the user hive) file in each folder (representing a username) located under C:\users (the default location of user profiles).
    Paradoxically though it may seem but I prefer the WMI approach as it’s the easiest. WMI instances have already all the interesting properties we are looking for. With WMI, you don’t need to convert paths or usernames to SIDs (security identifier), have a exclusion list of SIDs or folder names of special accounts (‘Default User’,’All Users’,’Default’,”Public”,…), you don’t need to understand ‘State’ value in the registry (by the way, will there be an official documentation provided one day ?). All we may have to do is to convert dates.

    Function Get-UserProfile             
    {            
        Param(            
        [CmdletBinding()]            
            [parameter(ValueFromPipeline = $True,ValueFromPipeLineByPropertyName = $True)]            
            [Alias('CN','__Server','IPAddress','Server')]            
            [string[]]$Computername = $Env:Computername,            
                        
            [parameter()]            
            [Alias('RunAs')]            
            [System.Management.Automation.Credential()]$Credential = [System.Management.Automation.PSCredential]::Empty                   
        )            
        Begin            
        {            
            # Make sure we run as admin            
            $usercontext = [Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()            
            $IsAdmin = $usercontext.IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")                               
            if (-not($IsAdmin))            
            {            
                Write-Warning "Must run powerShell as Administrator to perform these actions"            
                return            
            }             
            # Prepare HT            
            $wmiHT = @{            
                ErrorAction = "Stop"            
                Query = "Select * FROM Win32_UserProfile WHERE Loaded = $false AND Special = $false"            
            }            
            #Supplied Alternate Credentials?                        
            If ($PSBoundParameters['Credential'])            
            {                        
                $wmiHT.credential = $Credential                        
            }            
        }             
        Process             
        {            
            $ComputerName | ForEach-Object -Process {            
                $UserProfiles = $null            
                $Computer = $_            
                If ($Computer -eq $Env:Computername)            
                {            
                    $wmiHT.remove('Credential')            
                } Else {            
                    $wmiHT += @{Computername = $Computer}            
                }            
                try {            
                    $UserProfiles = Get-WmiObject @wmiHT            
                } catch {            
                    Write-Warning -Message "Failed to query Win32_UserProfile on computer $Computer"            
                }            
                if ($UserProfiles)            
                {            
                    $UserProfiles | ForEach-Object -Process {            
                        $LastUseDate = $Status = $Type = $UserName = $LastDownloadTime = $LastUploadTime = $null            
                        Switch($_.Status)            
                        {            
                            # 0 = 'Temporary' according to :            
                            # http://msdn.microsoft.com/en-us/library/windows/desktop/ee886409%28v=vs.85%29.aspx            
                            # Comment from Thomas Lee also says:            
                            # The actual values for the uint field are (0, 1, 2, 4, 8) where a value of zero denotes unset or default            
                            # and 1 = Temporary, 2 = Roaming, 4 = Mandatory and 8 = Corrupted.            
                            0       { $Status = 'Local'}            
                            1       { $Status = 'Roaming'}            
                            2       { $Status = 'Mandatory'}            
                            3       { $Status = 'Corrupted'}            
                            6       { $Status = 'Roaming'}            
                            8       { $Status = 'Temporary and loaded'}            
                            10      { $Status = 'Temporary'}            
                            default { $Status = $_}            
                        }            
                        if ($_.LastUseTime)            
                        {            
                            $LastUseDate = $_.ConvertToDateTime($_.LastUseTime)            
                        }            
                        if ($_.LastDownloadTime)            
                        {            
                           $LastDownloadTime = $_.ConvertToDateTime($_.LastDownloadTime)            
                        }            
                        if ($_.LastUploadTime)            
                        {            
                            $LastUploadTime = $_.ConvertToDateTime($_.LastUploadTime)            
                        }            
                        if ($_.RoamingConfigured)            
                        {            
                            $Status = "Roaming"            
                        } else {            
                
                            $Status = $Status            
                        }            
                        if ($_.RoamingPreference)            
                        {            
                            $Type = "Roaming"            
                        } else {            
                            $Type = "Local"            
                        }                                   
                                    
                        if ($_.SID)            
                        {            
                            $UserName = ConvertTo-NtAccount -Sid $($_.SID)            
                        }            
                        New-Object -TypeName PSObject -Property @{            
                            UserName = $UserName            
                            SID = $_.SID            
                            Type = $Type            
                            Status = $Status            
                            Path = $_.LocalPath            
                            LastUsedDate = $LastUseDate            
                            LastUploadTime = $LastUploadTime            
                            LastDownloadTime = $LastDownloadTime            
                            BinaryState = $_.Status            
                            CentralProfile = $_.RoamingPath            
                            WMIObject = $_            
                        }            
                    }            
                }            
            }            
        }            
                    
        End {}            
                
    }            
    
    • Step 2: Delete/Remove user profiles

    As you can see in the above Get-UserProfile function, I’ve chosen to return the full WMI instance as property of the final object. We actually need it to delete the user profile as I’ll use the Delete hidden method on the WMI instance that is fully supported on windows Vista and onwards.
    Deleting a user profile consists in the following actions:

      • Remove the key representing the SID of the user under HKLM\software\microsoft\windows nt\currentversion\profilelist
      • Remove the GUID of the user profile under the key HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileGuid’
      • Remove the folder of the user under C:\users

    Note that whenever you’ve incoherencies between folders under C:\users and the information under HKLM\software\microsoft\windows nt\currentversion\profilelist, you may have users being logged on a temporary profile and have their registry key backed up…

    Function Delete-UserProfile             
    {            
        [CmdletBinding()]            
        param            
        (            
        [parameter(ValueFromPipeline=$true, Mandatory=$true, Position=0)]            
        [ValidateNotNullOrEmpty()]            
        # [PSTypeName('System.Management.ManagementObject#root\cimv2\Win32_UserProfile')]            
        [System.Management.ManagementObject[]]${WMIObject}            
        )            
        Begin{}            
        Process            
        {            
            $WMIObject | ForEach-Object -Process {            
                $profile = (ConvertTo-NTAccount -SID $_.SID)            
                try            
                {            
                    # Invoke-WmiMethod -Name Delete reports that 'This method is not implemented'            
                    # ([wmi]$_.__Path).Delete()            
                    $_.Delete()            
                } catch {            
                    Write-Warning -Message "Failed to delete profile $profile"            
                }            
            }            
        }             
        End {}            
    } # end of function
    • Step 3: Assemble all the pieces and build the final report
      • Use the above Get-UserProfile and Delete-UserProfile functions
      • Write a new Remove-Profile function that accepts the threshold and days parameters and sends an HTML report

    Here’s the result 🙂

    #Requires -Version 2.0
    
    function Get-FolderSize {
    <# 
    .SYNOPSIS 
    Gets the size of a folder by getting the size of all files contained within.
     
    .DESCRIPTION 
    This function uses recursion to get the size of a folder and its subfolders. The output is
    a collection of objects that each have a folder name, a size in bytes, and a size in 
    formatted bytes (converted to kilo, Mega, Giga, etc).
    
    .PARAMETER Path
    Path to the folder whose info is needed.
    
    .PARAMETER RecurseLevel
    The level under the parent folder to return objects. Negative values mean infinite depth,
    so all subfolders will be returned. Zero means that you only want to have an object returned
    for the parent folder. See examples for more information.
    
    Default is -1 (Infinite)
    
    .PARAMETER Descending
    Because of the nature of the function, the data cannot be sorted in ascending order unless
    no objects are returned until after all of the folder information has been obtained. All of 
    the folder names can be returned in descending order in real time, though.
    
    Default is false, so objects returned have folder information in a semi-ascending order.
    
    .PARAMETER IncludeHidden
    Include hidden objects in folder information.
    
    Default is false.
    
    .PARAMETER IncludeReparsePoints
    Include reparse points in folder information. This means that sizes may be counted more than
    once.
    
    Default is false.
    
    .EXAMPLE
    PS C:\> Get-FolderSize -LiteralPath C:\Temp
    
      SizeInBytes Size of Folder                                               Folder                                                      
      ----------- --------------                                               ------                                                      
            34288 33.00 kiloBytes                                              C:\temp\R196853\Vi32\Data\Cur                               
            34288 33.00 kiloBytes                                              C:\temp\R196853\Vi32\Data                                   
            60393 59.00 kiloBytes                                              C:\temp\R196853\Vi32\Eula                                   
         14906422 14.00 MegaBytes                                              C:\temp\R196853\Vi32                                        
            34288 33.00 kiloBytes                                              C:\temp\R196853\Vi64\Data\Cur                               
            34288 33.00 kiloBytes                                              C:\temp\R196853\Vi64\Data                                   
            60393 59.00 kiloBytes                                              C:\temp\R196853\Vi64\Eula                                   
         15772346 15.00 MegaBytes                                              C:\temp\R196853\Vi64                                        
         30797949 29.00 MegaBytes                                              C:\temp\R196853                                             
            15944 16.00 kiloBytes                                              C:\temp\R252187\Vi32\Eula                                   
         16542513 16.00 MegaBytes                                              C:\temp\R252187\Vi32                                        
         16626114 16.00 MegaBytes                                              C:\temp\R252187                                             
        137289106 131.00 MegaBytes                                             C:\temp                                                     
    
    .EXAMPLE
    PS C:\> Get-FolderSize -LiteralPath C:\Temp -RecurseLevel 1
    
      SizeInBytes Size of Folder                                               Folder                                                      
      ----------- --------------                                               ------                                                      
         30797949 29.00 MegaBytes                                              C:\temp\R196853                                             
         34209551 33.00 MegaBytes                                              C:\temp\R252187                                             
         65007500 62.00 MegaBytes                                              C:\temp                                                     
    
    
    
    .EXAMPLE
    PS C:\> Get-FolderSize -LiteralPath C:\Temp -RecurseLevel 0
    
    
      SizeInBytes Size of Folder                                               Folder                                                      
      ----------- --------------                                               ------                                                      
         65007500 62.00 MegaBytes                                              C:\temp                                                     
    
    
    .EXAMPLE
    PS C:\> Get-FolderSize -LiteralPath C:\Temp -Descending
    
      SizeInBytes Size of Folder                                               Folder                                                      
      ----------- --------------                                               ------                                                      
            73775 72.00 kiloBytes                                              C:\temp\R252187\Vi64\Eula                                   
         17525606 17.00 MegaBytes                                              C:\temp\R252187\Vi64                                        
            73775 72.00 kiloBytes                                              C:\temp\R252187\Vi32\Eula                                   
         16600344 16.00 MegaBytes                                              C:\temp\R252187\Vi32                                        
         34209551 33.00 MegaBytes                                              C:\temp\R252187                                             
            60393 59.00 kiloBytes                                              C:\temp\R196853\Vi64\Eula                                   
            34288 33.00 kiloBytes                                              C:\temp\R196853\Vi64\Data\Cur                               
            34288 33.00 kiloBytes                                              C:\temp\R196853\Vi64\Data                                   
         15772346 15.00 MegaBytes                                              C:\temp\R196853\Vi64                                        
            60393 59.00 kiloBytes                                              C:\temp\R196853\Vi32\Eula                                   
            34288 33.00 kiloBytes                                              C:\temp\R196853\Vi32\Data\Cur                               
            34288 33.00 kiloBytes                                              C:\temp\R196853\Vi32\Data                                   
         14906422 14.00 MegaBytes                                              C:\temp\R196853\Vi32                                        
         30797949 29.00 MegaBytes                                              C:\temp\R196853                                             
         65007500 62.00 MegaBytes                                              C:\temp                                                     
    
    
    .EXAMPLE
    PS C:\> Get-FolderSize -LiteralPath C:\Temp -IncludeHidden -IncludeReparsePoints
    
    If there were hidden files/folders or reparse points in C:\Temp, they would be counted in the size
    totals.
    
    #>
    
        [CmdletBinding()]
        param(
            [Parameter(Mandatory=$true, Position=0)]
            [Alias("Path")]
            [System.IO.DirectoryInfo] $LiteralPath,
            [Parameter(Mandatory=$false, Position=1)]
            [int] $RecurseLevel = -1,
            [Parameter(Mandatory=$false)]
            [switch] $Descending = $false,
            [Parameter(Mandatory=$false)]
            [switch] $IncludeHidden = $false,
            [Parameter(Mandatory=$false)]
            [switch] $IncludeReparsePoints = $false
        )
        
        Write-Debug "In $LiteralPath"
        Write-Verbose "In $LiteralPath"
        
        # Initialize variable that keeps up with folder size
        $Size = 0
        
        # $RecurseLevel and $WriteToPipeline control whether or not an object
        # is written to the pipeline. See below when recursive call is made
        # for more info.
        $WriteToPipeline = $true
    
        if ($RecurseLevel -eq 0) {  
            # Do not write any more objects to pipeline. Recursive calls still
            # need to be made in order to get size of subfolders, though
            $WriteToPipeline = $false
        }
        elseif ($RecurseLevel -gt 0) {
            # Keep $WriteToPipeline equal to $true, but decrement the RecurseLevel
            # for future calls
            $RecurseLevel--
        }
        
        # List all child items in current path. Notice that $IncludeHidden controls whether
        # -Force parameter is used.
        Get-ChildItem -LiteralPath $LiteralPath -Force:$IncludeHidden -ErrorAction SilentlyContinue | 
          Sort-Object -Property FullName -Descending:$Descending | # Sort either ascending or descending
          ForEach-Object {
    
            # Enter if block if we are either including reparse points OR the current file/folder isn't 
            # a reparse point:
            if ($IncludeReparsePoints -or !($_.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
    
                if ($_.PsIsContainer) {
                    # We've found a sub folder!
                    
                    # Prepare parameters to pass:                
                    $Parameters = @{    LiteralPath = $_.FullName;
                                        Descending = $Descending;
                                        IncludeHidden = $IncludeHidden;
                                        IncludeReparsePoints = $IncludeReparsePoints;
                                        RecurseLevel = $RecurseLevel
                                   }
                                   
                    # Recursively call on folder size function; tee-object saves the returned object so we 
                    # can get information from it, and forwards it along the pipeline so it can (potentially)
                    # be written to output
                    Get-FolderSize @Parameters | Tee-Object -Variable SubFolder | Where-Object { 
                            # This acts as a block if we're not supposed to return an object for this folder.
                            # Script execution must continue, though, so that we can get the true folder size
                            # for the original parent folder(s)
    
                            $WriteToPipeline 
                        }
    
                    # As we get deeper into folder structure, $SubFolder object will contain more than one of
                    # the custom objects since $SubFolder contains what has been output by all previous subfolder 
                    # calls.
                    if ($SubFolder.Count) {
            
                        # To get around this, simply throw away all objects except the last one (remember,
                        # all subfolder objects have already been written; this is just the tee'd variable
                        # we're working with here)
                        $SubFolder = $SubFolder[-1]
    
                    }
                    
                    # Get the size of the subfolders from the returned objects
                    $Size += $SubFolder.SizeInBytes
    
                }
                else {
                    # File, so add this to the folder's size
                    $Size += $_.Length
                }
            } # End of Reparse point if block
        } # End of ForEach-Object
    
        # Create PSObject with folder information:
        $ReturnObject = New-Object PSObject -Property @{ 
                Folder = $LiteralPath;
                SizeInBytes = $Size;
    #            "Size of Folder (SI)" = Get-FormattedByte -Bytes $Size -Standard "si";
    #            "Size of Folder (IEC)" = Get-FormattedByte -Bytes $Size -Standard "iec";
                "Size of Folder" = Get-FormattedByte -Bytes $Size -Standard "legacy"
        }
        
        # Return object
        Write-Output $ReturnObject
    }
    
    function Get-FormattedByte {
    <# 
    .SYNOPSIS 
    Changes raw number of Bytes into a more readable string.
     
    .DESCRIPTION 
    This function takes a raw number of bytes and outputs a readable string. Example is 2048 Bytes
    would be changed to 2 kiloBytes.
    
    .PARAMETER Bytes
    Number of bytes that need to be formatted.
    
    .PARAMETER Precision
    Number of decimal places to take conversion to. Default is 2.
    
    .PARAMETER Standard
    Determines the unit and prefixes used when converting the 'Bytes'. Choose between SI, IEC and 
    Legacy standards. NOTE: Legacy standard is not an official standard. Legacy is what Windows 
    operating systems use. 
    
    Default is legacy.
    
    .PARAMETER Suffix
    A string that will be added to the end of the prefix used in formatted text. Default is 'Bytes'
    
    .EXAMPLE 
    PS C:\> Get-FormattedByte 2000000000
    1.86 GigaBytes
    
    .EXAMPLE 
    PS C:\> Get-FormattedByte 2000000000 -Standard si
    2.00 GigaBytes
    
    PS C:\> Get-FormattedByte 2000000000 -Standard iec
    1.86 GibiBytes
    
    PS C:\> Get-FormattedByte 2000000000 -Precision 4
    1.8626 GigaBytes
    
    PS C:\> Get-FormattedByte 2000000000 -Precision 4 -Suffix "B"
    1.8626 GigaB
    
    #>
        
        param(
            [Parameter(Mandatory=$true, Position=0)]
            [double] $Bytes,
            [Parameter(Mandatory=$false, Position=1)]
            [int] $Precision = 2,
            [Parameter(Mandatory=$false)]
            [ValidateSet("si","iec", "legacy")]
            $Standard = "legacy",
            [Parameter(Mandatory=$false)]
            [string] $Suffix = "Bytes"
        )
        
    
        # International System of Units Standard:
        $si =     @{ Unit = 1000;
                     Prefixes = "", "kilo","Mega", "Giga", "Tera", "Peta", "Exa" }
    
        # International Electrotechnial Commision Standard:
        $iec =    @{ Unit = 1024;
                     Prefixes = "", "Kibi", "Mebi", "Gibi", "Tebi", "Pebi", "Exbi" }
        
        # Legacy unit/prefix where binary unit uses SI prefixes:
        $legacy = @{ Unit = 1024;
                     Prefixes = "", "kilo","Mega", "Giga", "Tera", "Peta", "Exa" }
                
        # Get the hashtable that contains the prefixes and unit:    
        $StandardHT = Invoke-Expression "`$$Standard"
    
        foreach ($Prefix in $StandardHT.Prefixes) {
            if (($Prefix -eq $StandardHT.Prefixes[-1]) -or ($Bytes -lt $StandardHT.Unit)) { 
                # Either out of prefixes, in which case we should just say how
                # many "whatever" bytes we have, or we have a small enough number
                # of "whatever" bytes to display the formatted bytes
                return "$($Bytes.ToString(`"F$Precision`")) $($Prefix)$Suffix" 
            }
            else { 
                # Divide by unit and use a new prefix on next loop
                $Bytes /= $StandardHT.Unit 
            }
        }
    }
    
    
    function ConvertTo-NtAccount
    {
    <#
        
    .SYNOPSIS    
        Translate a SID to its displayname
    
    .DESCRIPTION  
        Translate a SID to its displayname
    
    .PARAMETER Sid
        Provide a SID
    
    .NOTES    
        Name: ConvertTo-NtAccount
        Author: thepowershellguy
    
    .LINK    
        http://thepowershellguy.com/blogs/posh/archive/2007/01/23/powershell-converting-accountname-to-sid-and-vice-versa.aspx
         
    .EXAMPLE
        ConvertTo-NtAccount S-1-1-0
        Convert a well-known SID to its displayname
        
    #>
    
    param(
    [parameter(Mandatory=$true,Position=0)][system.string]$Sid = $null
    )
     begin
     {
        $obj = new-object system.security.principal.securityidentifier($sid)
     }
     process
     {
        try
        {
            $obj.translate([system.security.principal.ntaccount])
        }
        catch
        {
            # To remove the silent fail, uncomment next line
            # $_
        }
     }
     End {}
    }
    
    Function Remove-Profile {
        [cmdletbinding()]
        Param (
            [parameter(
                HelpMessage="Threshold of freespace left expressed in percent")]
            [ValidateRange(0,100)]
            [int]$FreeSpaceThreshold = 20,
            
            [parameter()]
            [int]$Days = 5
        )
        Begin {
            # Define the Html parts of the report
            $head = @"
    <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head>
    <title>HTML TABLE</title>
    </head><body>
    <table>
    "@    
        $end = "</table></body></html>"
    
        # Not an array as Send-mailmessage doesn't accept a system.object and tries a conversion to system.string that fails
        $body = @"
    "@    
    }
    Process {
        
        # Define the  HKLM Constant and the Key we are looking for
        $HKLM = 2147483650
        $Key = "software\microsoft\windows nt\currentversion\profilelist"
        $result = $null
        
        # Get the location of user profiles from the registry
        try {
            $result = Invoke-WmiMethod -Path "ROOT\DEFAULT:StdRegProv" -Name GetExpandedStringValue -ArgumentList $HKLM,$Key,"ProfilesDirectory" -ErrorAction Stop
        } catch  {
            Write-Warning -Message "WMI query failed"
        }
        if ($result.sValue) {
            $drive = Split-Path $result.sValue -Qualifier
            try {
                $wmi_worked = $true
                # Get the freespace of the drive where user profiles are located
                $disk = Get-WmiObject -Class Win32_LogicalDisk -filter "DeviceID='$drive'" -ErrorAction Stop
            } catch  {
                Write-Warning -Message "Failed to query WMI"
                $wmi_worked = $false
            }
            if ($wmi_worked) {
                
                # Get a nicely formatted of the freespace initially left on the drive
                $FreeDividedSize=$disk.Freespace/$disk.Size
                [string]$PerFree="{0:P}" -f $FreeDividedSize
                
                # Add the header to the Html report
                $body += $head
                $body += "<p></p><br>"
                
                # Get the freespace 
                $body += New-Object -TypeName PSObject -Property @{
                    'Initial Freespace' = (Get-FormattedByte -Bytes $disk.Freespace -Standard "legacy" -Precision 1)
                    'Percent of total drive' = $PerFree 
                } | ConvertTo-Html -Property 'Initial Freespace','Percent of total drive' -Fragment -As LIST
                
                # Calculate  the total size of C:\users
                $body += New-Object -TypeName PSObject -Property @{
                    'Location of profiles' = $result.sValue
                    'Initial size of profiles' = (Get-FolderSize -LiteralPath $result.sValue -RecurseLevel 0 -IncludeHidden:$true ).'Size of Folder'
                } | ConvertTo-Html -Property 'Location of profiles','Initial size of profiles' -Fragment -As LIST
                $body += "<p></p><br>"
                
                # Define a black list of profiles not to delete
                $ExclusionList = 'Default User','All Users','Default',"Public"
                
                # Calculate the size of each profile
                $body += Get-ChildItem -Path $result.sValue  -Exclude $ExclusionList | Where {$_  -is [system.IO.directoryInfo]} |ForEach-Object -Process {
                    New-Object -TypeName PSObject -Property @{
                        'Profile' = $_.FullName
                        'Size' = (Get-FolderSize -LiteralPath $_.FullName -RecurseLevel -1 -IncludeHidden:$true )[-1].'Size of Folder'
                    } 
                } | ConvertTo-Html -Property 'Profile','Size' -Fragment -As TABLE 
                
                # Evaluate the threshold
                if ($FreeDividedSize -gt $($FreeSpaceThreshold/100)) {
                    "There's more freespace than the {0:P0} threshold specified" -f ($FreeSpaceThreshold/100)
                    # We do nothing...
                } else {
                
                    # We start deleting folders older that the Days specified as parameter
                    $body += "<p></p><br>"
                    $body += Get-UserProfile | Where-Object -FilterScript { $_.LastUsedDate -le (Get-Date).AddDays(-$Days) } |
                        ForEach-Object -Process {
                            # Remove profiles
                            $status = 'ok'
                            Write-Verbose -Message "Attempt to delete profile $($_.UserName) located in $($_.Path)" -Verbose
                            $_ | Select-Object -ExpandProperty WMIObject | Delete-UserProfile
                            # If deleting profiles with WMI failed we may still have a folder left
                            if (Test-Path $_.Path) {
                                $status = 'failed'
                                # Attempt to delete this folder, but not with Remove-Item that throws an Access Denied
                                $cmdcommand =  "$env:systemroot\system32\cmd.exe  /C `"rd /S/Q " + $_.Path + "`""
                                $cmdcommandresult = Invoke-Expression $cmdcommand
                                # Test whether our good old rd command succeeded or failed
                                if (Test-Path $_.Path) {
                                    $status = 'failed'
                                    Write-Warning -Message "Failed to remove folder $($_.Path)"
                                } else {
                                    $status = 'ok'
                                    Write-Warning -Message "2nd attempt to remove folder $($_.Path) was successful"
                                }
                            }
                            New-Object -TypeName PSObject -Property @{
                                'Deleted Profile' = $_.UserName
                                'Result' = $status
                            }
                    } | ConvertTo-Html -Property 'Deleted Profile','Result' -Fragment -As TABLE
    
                    # Get the freespace left on the disk after user profiles removals
                    $disk = Get-WmiObject -Class Win32_LogicalDisk -filter "DeviceID='$drive'"
                    $FreeDividedSize=$disk.Freespace/$disk.Size
                    [string]$PerFree="{0:P}" -f $FreeDividedSize
                    $body += "<p></p><br>"
                    $body += New-Object -TypeName PSObject -Property @{
                        'Final Freespace' = (Get-FormattedByte -Bytes $disk.Freespace -Standard "legacy" -Precision 1)
                        'Percent of total drive' = $PerFree 
                    } | ConvertTo-Html -Property 'Final Freespace','Percent of total drive' -Fragment -As LIST
                    
                    # Get the total size of profiles after removal
                    $body += New-Object -TypeName PSObject -Property @{
                        'Final size of profiles' = (Get-FolderSize -LiteralPath $result.sValue -RecurseLevel 0 -IncludeHidden:$true ).'Size of Folder'
                    } | ConvertTo-Html -Property 'Final size of profiles' -Fragment -As LIST 
                    $body += "<p></p><br>"
                    
                    # Get profiles left after the removal
                    $body += Get-ChildItem -Path $result.sValue  | Where {$_  -is [system.IO.directoryInfo]} |ForEach-Object -Process {
                        New-Object -TypeName PSObject -Property @{
                            'Profile' = $_.FullName
                            'Size' = (Get-FolderSize -LiteralPath $_.FullName -RecurseLevel -1 -IncludeHidden:$true )[-1].'Size of Folder'
                        } 
                    } | ConvertTo-Html -Property 'Profile','Size' -Fragment -As TABLE 
                }
                $body += $end
                # Send the final report
                $smtpserver = "the.smtp.server.address"
                $from = "$env:computername@$(($env:USERDNSDOMAIN).ToLower())"
                $to = "the.admin.email.address"
                $Subject = "Freespace operation"
                try {
                    Send-MailMessage -Body $body -From $from -Subject $Subject -To $to -SmtpServer $smtpserver -BodyAsHtml -ErrorAction Stop
                } catch {
                    Write-Warning -Message "Failed to send the final report because $($_.Exception.Message)"
                }
    
            }        
        }
    }
    End {}
    
    } # end of function
    
    # Main
    Remove-Profile
    

    The only issue I’ve encountered so far with the above script is that the Tee-Object cmdlet that Rohn Edwards uses in his advanced functions fails with the following error message – Tee-Object : The pipeline failed due to call depth overflow. The call depth reached 51 and the maximum is 50. – when used in powershell remote session

    Get-Help -Online error

    I wanted to use one of the new cool feature of powershell V3 but it failed with the following error:
    ‘Launching a program to show online help failed. No program is associated to launch URI’
    I used procmon to quickly troubleshoot this

    Get-help New-JobTrigger -Online

    Get-help online error
    It reports that HKCR\http\shell\open registry key does not exist.
    I’ve verified it with powershell as well

    Get-Item HKLM:\software\classes\http\shell\open

    key does not exist

    To fix this on a W7 computer, I just did:

    New-Item -Path 'HKLM:\software\classes\http\shell\open' -Type container            
    New-Item -Path 'HKLM:\software\classes\http\shell\open\command' -Type container            
    Set-ItemProperty -Path 'HKLM:\software\classes\http\shell\open\command'  -Name '(default)' -Value '"C:\Program Files\Internet Explorer\iexplore.exe" %1'             
    

    And voilà, the -Online switch of the Get-Help cmdlet works as expected 🙂
    Note that this fix will probably not work a W8 as there are other additional values under HKLM:\software\classes\http\shell & HKLM:\software\classes\http\shell\open