ADLAB PowerShell source file: lib-modifyActions.ps1

(C) Ondrej Sevecek, 2019 - www.sevecek.com, ondrej@sevecek.com



# ============================================================
# Note: Make sure that the lib-common has been loaded       ||
#                                                           ||

if (($global:adlabVersion -lt 1) -or (-not $global:libCommonScriptInitialized)) {
  
  $msgLibCommonNotInitialized = 'ADLAB: lib-common not loaded or initialized. Exiting'
  Write-Host $msgLibCommonNotInitialized -ForegroundColor Red
  throw $msgLibCommonNotInitialized
  exit 1
}
     
$adlabVersionThisLib = 907
$adlabReleaseDateThisLib = [DateTime]::Parse('2019-09-20')
DBG ('Library loaded: {0} | v{1} | {2:yyyy-MM-dd}' -f (Split-Path -leaf $MyInvocation.MyCommand.Definition), $adlabVersionThisLib, $adlabReleaseDateThisLib)

#                                                           ||
# Note: Make sure that the lib-common has been loaded       ||
# ============================================================

 
  
#==================
# Some constants

$global:PutExClear = 1 
$global:PutExUpdate = 2
$global:PutExAdd = 3 # Note: even if you use [string[]] variable in PutEx(), you must always enclose it into an array like PutEx(3, 'someMultivalue', @($alreadyArray))
$global:PutExDelete = 4

$global:hypervClientCPUMatrix = @{ 

  #              2000            2000            XP             XP64           2003           Vista          2008           7              2008 R2         8               2012            8.1             2012 R2         10              10
  '6.0srv'  = @{ '5.0clt' =  1 ; '5.0srv' =  1 ; '5.1clt' = 2 ; '5.2clt' = 2 ; '5.2srv' = 2 ; '6.0clt' = 2 ; '6.0srv' = 4 ; '6.1clt' = 4 ; '6.1srv' =  4 ; '6.2clt' = -1 ; '6.2srv' = -1 ; '6.3clt' = -1 ; '6.3srv' = -1 ; '6.4clt' = -1 ; '6.4srv' = -1 ; '10.0clt' = -1 ; '10.0srv' = -1 } ; # 2008
  '6.1srv'  = @{ '5.0clt' =  1 ; '5.0srv' =  1 ; '5.1clt' = 2 ; '5.2clt' = 2 ; '5.2srv' = 2 ; '6.0clt' = 2 ; '6.0srv' = 4 ; '6.1clt' = 4 ; '6.1srv' =  4 ; '6.2clt' =  4 ; '6.2srv' =  4 ; '6.3clt' = -1 ; '6.3srv' = -1 ; '6.4clt' = -1 ; '6.4srv' = -1 ; '10.0clt' = -1 ; '10.0srv' = -1 } ; # 2008 R2
  '6.2srv'  = @{ '5.0clt' = -1 ; '5.0srv' = -1 ; '5.1clt' = 2 ; '5.2clt' = 2 ; '5.2srv' = 2 ; '6.0clt' = 2 ; '6.0srv' = 8 ; '6.1clt' = 4 ; '6.1srv' = 64 ; '6.2clt' = 32 ; '6.2srv' = 64 ; '6.3clt' = 32 ; '6.3srv' = 64 ; '6.4clt' = 32 ; '6.4srv' = 64 ; '10.0clt' = 32 ; '10.0srv' = 64 } ; # 2012
  '6.3srv'  = @{ '5.0clt' = -1 ; '5.0srv' = -1 ; '5.1clt' = 2 ; '5.2clt' = 2 ; '5.2srv' = 2 ; '6.0clt' = 2 ; '6.0srv' = 8 ; '6.1clt' = 4 ; '6.1srv' = 64 ; '6.2clt' = 32 ; '6.2srv' = 64 ; '6.3clt' = 32 ; '6.3srv' = 64 ; '6.4clt' = 32 ; '6.4srv' = 64 ; '10.0clt' = 32 ; '10.0srv' = 64 } ; # 2012 R2
  '10.0srv' = @{ '5.0clt' = -1 ; '5.0srv' = -1 ; '5.1clt' = 2 ; '5.2clt' = 2 ; '5.2srv' = 2 ; '6.0clt' = 2 ; '6.0srv' = 8 ; '6.1clt' = 4 ; '6.1srv' = 64 ; '6.2clt' = 32 ; '6.2srv' = 64 ; '6.3clt' = 32 ; '6.3srv' = 64 ; '6.4clt' = 32 ; '6.4srv' = 64 ; '10.0clt' = 32 ; '10.0srv' = 64 } ; # 2016
  
  }

if ($global:virtualizationWmiAPIversion -eq 1) {

  $global:hypervResourceSpecs = @{

    computerSystem = 'Msvm_ComputerSystem'   

    storResTypeVHDVFDISO = 21
    storClassVHDVFDISO   = 'Msvm_ResourceAllocationSettingData'
    storSubTypeVHD       = 'Microsoft Virtual Hard Disk'
    storSubTypeVFD       = 'Microsoft Virtual Floppy Disk'
    storSubTypeISO       = 'Microsoft Virtual CD/DVD Disk'
    storPathVHDVFDISO    = 'Connection'
    storBusAddress       = 'Address'

    storResTypeDVD = 16
    storSubTypeDVD = 'Microsoft Synthetic DVD Drive'
    storResTypeDisk = 22
    storSubTypeDisk = 'Microsoft Synthetic Disk Drive'
    storResTypeFDD = 14
    storSubTypeFDD = 'Microsoft Synthetic Diskette Drive'

    storResTypeIDEBus = 5
    storSubTypeIDEBus = 'Microsoft Emulated IDE Controller'
    storResTypeSCSIBus = 6
    storSubTypeSCSIBus = 'Microsoft Synthetic SCSI Controller'
    storResTypeFDDBus = 1
    storSubTypeFDDBus = 'Diskette Controller'

    ntwSwitch = 'Msvm_VirtualSwitch'
    ntwLanEndpoint = 'Msvm_SwitchLANEndpoint'
    ntwSwitchPort = 'Msvm_SwitchPort'
    ntwResTypeEthPort = 10
    ntwSubTypeEthPort = 'Microsoft Synthetic Ethernet Port'
    ntwResTypeLegacyEthPort = 10
    ntwSubTypeLegacyEthPort = 'Microsoft Emulated Ethernet Port'
  }

} else {

  $global:hypervResourceSpecs = @{

    computerSystem = 'Msvm_ComputerSystem'

    storResTypeVHDVFDISO  = 31
    storClassVHDVFDISO    = 'Msvm_StorageAllocationSettingData'
    storSubTypeVHD        = 'Microsoft:Hyper-V:Virtual Hard Disk'
    storSubTypeVFD        = 'Microsoft:Hyper-V:Virtual Floppy Disk'
    storSubTypeISO        = 'Microsoft:Hyper-V:Virtual CD/DVD Disk'
    storPathVHDVFDISO     = 'HostResource'
    storBusAddress        = 'AddressOnParent'

    storResTypeDVD = 16;
    storSubTypeDVD = 'Microsoft:Hyper-V:Synthetic DVD Drive';
    storResTypeDisk = 17;
    storSubTypeDisk = 'Microsoft:Hyper-V:Synthetic Disk Drive';
    storResTypeFDD = 14;
    storSubTypeFDD = 'Microsoft:Hyper-V:Synthetic Diskette Drive';

    storResTypeIDEBus = 5;
    storSubTypeIDEBus = 'Microsoft:Hyper-V:Emulated IDE Controller';
    storResTypeSCSIBus = 6;
    storSubTypeSCSIBus = 'Microsoft:Hyper-V:Synthetic SCSI Controller';
    storResTypeFDDBus = 14;
    storSubTypeFDDBus = 'Microsoft:Hyper-V:Synthetic Diskette Drive';

    ntwSwitch = 'Msvm_VirtualEthernetSwitch'
    ntwLanEndpoint = 'Msvm_LANEndpoint'
    ntwSwitchPort = 'Msvm_EthernetSwitchPort'
    ntwSwitchPortExternal = 'Msvm_ExternalEthernetPort'

    ntwResTypeEthPort = 10
    ntwSubTypeEthPort = 'Microsoft:Hyper-V:Synthetic Ethernet Port'
    ntwWmiTypeEthPort = 'Msvm_SyntheticEthernetPortSettingData'
    ntwResTypeLegacyEthPort = 10
    ntwSubTypeLegacyEthPort = 'Microsoft:Hyper-V:Emulated Ethernet Port'
    ntwWmiTypeLegacyEthPort = 'Msvm_EmulatedEthernetPortSettingData'

    ntwResTypeEthConn = 33
    ntwSubTypeEthConn = 'Microsoft:Hyper-V:Ethernet Connection'
  }
}

$global:hypervPermanent = 'PERMANENT-?*'

$global:sevecekMacPrefix = '02:88:88'

$global:vhdType = @{ 2 = 'fixed' ; 3 = 'dynamic' ; 4 = 'diff' }

$global:vhdExtensions = @('.vhd', '.avhd', '.vhdx', '.avhdx')

$global:wmiFltVirtualMachines = 'SELECT * FROM MSVM_ComputerSystem WHERE Caption <> "Hosting Computer System" AND Description <> "Microsoft Hosting Computer System"'

$global:vmOperationalStatus = @{
 
    2 = 'OK'
    3 = 'Degraded'
    5 = 'Predictive failure'
    10 = 'Stopped'
    11 = 'In service'
    15 = 'Dormant'
  }

$global:vmOperationalSubstatus = @{

    32768 = 'Creating snapshot'
    32769 = 'Applying snapshot'
    32770 = 'Deleting snapshot'
    32771 = 'Waiting to start'
    32772 = 'Merging disks'
    32773 = 'Exporting'
    32774 = 'Migrating'
  }

$global:ipMultivalueSeparator = ','


#==================
# And then the functions


function global:Set-MustChangePwdNextLogon ([string] $userDN)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $userDN }

  [System.Collections.ArrayList] $deList = @()
  $userDE = Get-DE $userDN ([ref] $deList)
  
  if (Is-NonNull $userDE) {

    DBGIF $MyInvocation.MyCommand.Name { ((([int] (GDES $userDE userAccountControl)) -band 65536) -eq 65536) }

    DBGSTART

    $userDE.Put('pwdlastset', 0)
    $adsiRs = $userDE.SetInfo()
    # although this normally returns $null, the $null is also returned as a result of the function
    # hence we should cope with it like this.

    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
  }
  
  Dispose-List ([ref] $deList)
}

function global:Update-PwdLastSet ([string] $userDN)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $userDN }
  
  [System.Collections.ArrayList] $deList = @()
  $userDE = Get-DE $userDN ([ref] $deList)
  
  if (Is-NonNull $userDE) {

    DBGSTART
    $userDE.Put('pwdlastset', 0)
    $adsiRs = $userDE.SetInfo()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))

    DBGSTART
    $userDE.Put('pwdlastset', -1)
    $adsiRs = $userDE.SetInfo()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
  }
  
  Dispose-List ([ref] $deList)
}


function global:Set-RegistryValue ([string] $key, [string] $name, [object] $value, [string] $type, [ref] $withBackup)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $key }
  DBGIF $MyInvocation.MyCommand.Name { (Is-NonNull $withBackup) -and (Is-Null $withBackup.Value) }
  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $type) -and ($type -ne 'string') -and ($type -ne 'expandstring') -and ($type -ne 'binary') -and ($type -ne 'dword') -and ($type -ne 'multistring') -and ($type -ne 'qword') }

  DBGIF ('Restore of multivalues not implemented/tested yet for MULTI_SZ or BINARY types') { $withBackup -and (($type -eq 'MultiString') -or ($type -eq 'Binary')) }

  $pathNormalization = @{ 
    'Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\' = 'hklm'
    'Microsoft.PowerShell.Core\Registry::HKEY_CURRENT_USER\' = 'hkcu'
    'Microsoft.PowerShell.Core\Registry::HKEY_USERS\' = 'hku'
    'hklm\' = 'hklm'
    'hkcu\' = 'hkcu'
    'hku\' = 'hku'
    'HKEY_LOCAL_MACHINE\' = 'hklm'
    'HKEY_CURRENT_USER\' = 'hkcu'
    'HKEY_USERS\' = 'hku'
    }
    
  foreach ($onePathNormalization in $pathNormalization.Keys) {
  
    if ($key -like ('{0}*' -f $onePathNormalization)) {

      $key = '{0}:\{1}' -f $pathNormalization[$onePathNormalization], $key.SubString($onePathNormalization.Length)
      break
    }
  }  
  
  DBG ('Key path normalized as: {0}' -f $key)
  DBGIF $MyInvocation.MyCommand.Name { ($key -notlike 'HKLM:\?*') -and ($key -notlike 'HKCU:\?*') -and ($key -notlike 'HKU:\?*') }
  if (($key -like 'HKLM:\?*') -or ($key -like 'HKCU:\?*') -or ($key -like 'HKU:\?*')) {

      if (Is-ValidString $type) {
  
        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $name }

        # value is [object] which can be int or string or anything
        #DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $value }
      }


      [PSCustomObject] $keyValueBackup = $null

      if ((Is-NonNull $withBackup) -and (Is-NonNull $withBackup.Value)) {

        DBG ('We are going to backup the previous state first')
        DBGIF $MyInvocation.MyCommand.Name { -not ($withBackup.Value -is [System.Collections.ArrayList]) }

        DBGSTART
        # Note: minimize our own code to be able to collect any errors at the DBGEND


        $keyValueBackup = New-Object PSCustomObject

        $keyOnlyOperation = $name -eq ''
        $keyExisted = Test-Path $key

        $originalValue = $null
        $valueExisted = $false
        $originalValueKind = $null
        
        [string] $preexistingKey = $key

        if (-not $keyExisted) {

          do {

            $preexistingKey = (Split-Path -Parent $preexistingKey)
      
          } while (($preexistingKey -ne '') -and (-not (Test-Path $preexistingKey)))
        }

        if (($keyExisted) -and (-not $keyOnlyOperation)) {

          [Microsoft.Win32.RegistryKey] $keyItem = Get-Item -Path $key
          $originalValue = $keyItem.GetValue($name)
          $valueExisted = Is-NonNull $originalValue

          if ($valueExisted) {

            $originalValueKind = $keyItem.GetValueKind($name)
          }
        }


        $errorDuringBackup = ($error.Count -gt 0)        
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND


        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $preexistingKey }
        DBGIF ('Invalid value type modification: current = {0} | asked = {1}' -f $originalValueKind, $type) { $valueExisted -and ($type -ne $originalValueKind) }


        Add-Member -In $keyValueBackup -MemberType NoteProperty -Name errorDuringBackup -Value $errorDuringBackup

        Add-Member -In $keyValueBackup -MemberType NoteProperty -Name reqKey -Value $key.TrimEnd('\')
        Add-Member -In $keyValueBackup -MemberType NoteProperty -Name reqName -Value $name
        Add-Member -In $keyValueBackup -MemberType NoteProperty -Name reqType -Value $type
        Add-Member -In $keyValueBackup -MemberType NoteProperty -Name reqValue -Value $value

        Add-Member -In $keyValueBackup -MemberType NoteProperty -Name keyOnlyOperation -Value $keyOnlyOperation
        Add-Member -In $keyValueBackup -MemberType NoteProperty -Name keyExisted -Value $keyExisted
        Add-Member -In $keyValueBackup -MemberType NoteProperty -Name preexistingKey -Value $preexistingKey.TrimEnd('\')
        Add-Member -In $keyValueBackup -MemberType NoteProperty -Name valueExisted -Value $valueExisted
        Add-Member -In $keyValueBackup -MemberType NoteProperty -Name originalValue -Value $originalValue
        Add-Member -In $keyValueBackup -MemberType NoteProperty -Name originalValueKind -Value $originalValueKind


        # Note: just capture any errors
        DBGSTART ; DBGEND
      }


      $errorDuringKeyPathCreation = $false
      $errorDuringValueModification = $false


      DBG ("Ensure the key exists")
      if (Test-Path $key) {
  
        DBG ("Key already exists: {0}" -f $key)
      }
  
      else {
  
        DBG ("Creating the whole registry path: {0}" -f $key)
        DBGSTART

        # Note: here we are correctly creating Force the key only if it does not exist
        #       Otherwise the -Force parameter would recreate the key and delete all
        #       previously existing values if the key was existing
        [void] (New-Item -Path $key -ItemType Registry -Force)

        if (Is-NonNull $keyValueBackup) {

          $errorDuringKeyPathCreation = ($error.Count -gt 0)        
        }

        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }


      if (Is-ValidString $type) {
  
        DBG ("Set the value")
        DBGSTART

        Set-ItemProperty -Path $key -Name $name -Value $value -Type $type -Force
    
        if (Is-NonNull $keyValueBackup) {
        
          $errorDuringValueModification = ($error.Count -gt 0)
        }

        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }


      if (Is-NonNull $keyValueBackup) {

        Add-Member -In $keyValueBackup -MemberType NoteProperty -Name errorDuringKeyPathCreation -Value $errorDuringKeyPathCreation
        Add-Member -In $keyValueBackup -MemberType NoteProperty -Name errorDuringValueModification -Value $errorDuringValueModification
        [void] $withBackup.Value.Add($keyValueBackup)
      }
  }
}


function global:Restore-RegistryChangeBackup ([Collections.ArrayList] $backup)
{
<#

lib-common.ps1 ; lib-modifyActions.ps1

# Preparation phase
$key0 = 'HKLM:\SOFTWARE\Microsoft\Windows\SEVECEK-CreatedSubPath'
$key1 = 'HKLM:\SOFTWARE\Microsoft\Windows\SEVECEK-CreatedSubPath\subtest1\finalTest'
$key2 = 'HKLM:\SOFTWARE\Microsoft\Windows\SEVECEK-CreatedSubPath\subtest1\justCreatedAkey'
$key22 = 'HKLM:\SOFTWARE\Microsoft\Windows\SEVECEK-CreatedSubPath\subtest1\justCreatedAkey2'
$key3 = 'HKLM:\SOFTWARE\Microsoft\Windows\SEVECEK-CreatedSubPath\subtest2\justCreatedAkey'
$key4 = 'HKLM:\SOFTWARE\Microsoft\Windows\SEVECEK-PreexistingEmpty'
$key5 = 'HKLM:\SOFTWARE\Microsoft\Windows\SEVECEK-PreexistingWithValues'

if (Test-Path $key0) { Remove-Item $key0 -Recurse:$true }
if (Test-Path $key4) { Remove-Item $key4 -Recurse:$true }
if (Test-Path $key5) { Remove-Item $key5 -Recurse:$true }
New-Item $key4 | out-null
New-Item $key5 | out-null
Set-ItemProperty $key5 -name PreexistingDword -Value 7 -type dword
Set-ItemProperty $key5 -name PreexistingDwordToRewrite -Value 7 -type dword
Set-ItemProperty $key5 -name PreexistingString -Value 'hello' -type string
Set-ItemProperty $key5 -name PreexistingStringToRewrite -Value 'original' -type string
Set-ItemProperty $key5 -name PreexistingStringToTheSame -Value 'theSame' -type string



# Verification phase NORMAL SITUATION
[System.Collections.ArrayList] $backup = @()

Set-RegistryValue -key $key1 -name 'ValDword' -value 5 -type dword -withBackup ([ref] $backup)
Set-RegistryValue -key $key1 -name 'ValString' -value 'Hello' -type string -withBackup ([ref] $backup)
Set-RegistryValue -key $key2 -withBackup ([ref] $backup)
Set-RegistryValue -key $key22 -withBackup ([ref] $backup)
Set-RegistryValue -key $key3 -withBackup ([ref] $backup)
Set-RegistryValue -key $key4 -withBackup ([ref] $backup)
Set-RegistryValue -key $key5 -name 'ValDword' -value 5 -type dword -withBackup ([ref] $backup)
Set-RegistryValue -key $key5 -name 'PreexistingStringToRewrite' -value rewritten -type string -withBackup ([ref] $backup)
Set-RegistryValue -key $key5 -name 'PreexistingStringToTheSame' -value theSame -type string -withBackup ([ref] $backup)
Set-RegistryValue -key $key5 -name 'PreexistingDwordToRewrite' -value 99999 -type dword -withBackup ([ref] $backup)

Restore-RegistryChangeBackup $backup




# Verification phase CORRUPTED SCENARIO

Set-RegistryValue -key $key1 -name 'ValString' -value 'Corrupt' -type string
Set-RegistryValue -key $key1 -name 'ValDword' -value 'Corrupt' -type string
Set-RegistryValue -key $key1 -name 'ValAdded' -value 'Corrupt' -type string
Set-RegistryValue -key $key2 -name 'ValAdded' -value 'Corrupt' -type string
Set-RegistryValue -key $key22\Corrupted
Remove-Item $key3
Remove-ItemProperty $key1 -name 'ValString'

Restore-RegistryChangeBackup $backup

#>

  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $backup) -lt 1 }
  

  function Is-KeyValidForSafeDelete ([string] $key)
  {
    [bool] $valid = $false

    DBGSTART

    if (Test-Path $key) {

      $validKey = Get-Item $key
      [string[]] $validKeyValueNames = $validKey.GetValueNames()
      [Microsoft.Win32.RegistryKey[]] $validKeySubkeys = Get-ChildItem $key

      $valid = (([int] $validKeyValueNames.Count) -eq 0) -and (([int] $validKeySubkeys.Count) -eq 0) -and ($key -match '\A(?:hklm\:\\)|(?:hku\:\\)|(?:hkcu\:\\).*')

      $keyTrimmed = $key.TrimEnd('\')
      $valid = $valid -and ($keyTrimmed -ne 'HKLM:\SOFTWARE\Microsoft') -and
                           ($keyTrimmed -ne 'HKLM:\SOFTWARE\Microsoft\Windows') -and 
                           ($keyTrimmed -ne 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion') -and 
                           ($keyTrimmed -ne 'HKLM:\SOFTWARE\Microsoft\Windows NT') -and 
                           ($keyTrimmed -ne 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion') -and 
                           ($keyTrimmed -ne 'HKLM:\SYSTEM\CurrentControlSet') -and 
                           ($keyTrimmed -ne 'HKLM:\SYSTEM\CurrentControlSet\Control') -and 
                           ($keyTrimmed -ne 'HKLM:\SYSTEM\CurrentControlSet\Services')
    }

    $valid = $valid -and ($error.Count -eq 0)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF ('Key invalid for safe delete: {0}' -f $key) { -not $valid }

    return $valid
  }


  if ((Get-CountSafe $backup) -gt 0) {

    DBG ('Going to restore the registry operation backups in reverse order: {0}' -f $backup.Count)

    $anyPreventiveErrorFound = (Get-CountSafe (Obtain-ListMembers $backup @{ errorDuringBackup = $true })) -gt 0
    DBGIF $MyInvocation.MyCommand.Name { $anyPreventiveErrorFound }

    if (-not $anyPreventiveErrorFound) {

      for ($i = ($backup.Count - 1); $i -ge 0; $i --) {
      
        $oneBackup = $backup[$i]
        #DBGX ("One backup item:`n`n{0}" -f ($oneBackup | Out-String))


        $valid = $false
        $validCurrentValue = $null

        if ($oneBackup.keyOnlyOperation) {

          $valid = Is-KeyValidForSafeDelete $oneBackup.reqKey

        } else {

          DBGSTART

          $validKey = Get-Item $oneBackup.reqKey
          $validCurrentValue = $validKey.GetValue($oneBackup.reqName)
          $validCurrentValueType = $validKey.GetValueKind($oneBackup.reqName)

          $valid = (Is-NonNull $validCurrentValue) -and ($validCurrentValue -eq $oneBackup.reqValue) -and ($validCurrentValueType -eq $oneBackup.reqType)

          $valid = $valid -and ($error.Count -eq 0)
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
        }


        $valid = $valid -and (-not $oneBackup.errorDuringKeyPathCreation) -and (-not $oneBackup.errorDuringValueModification)
        
        DBGIF $MyInvocation.MyCommand.Name { -not $valid }
        if (-not $valid) {

          break
        }



        if (-not $oneBackup.keyOnlyOperation) {

          if (-not $oneBackup.valueExisted) {

            DBG ('Will remove the value which was previously created: {0} | {1}' -f $oneBackup.reqKey, $oneBackup.reqName)
            DBGSTART
            Remove-ItemProperty -Path $oneBackup.reqKey -Name $oneBackup.reqName -Force:$false
            $valid = $error.Count -eq 0
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
            if (-not $valid) { break }

          } else {
  
            DBG ('Will reset the value which was previously modified: {0} | {1} | {2} | {3}' -f $oneBackup.reqKey, $oneBackup.reqName, $oneBackup.originalValue, $oneBackup.originalValueKind)
            DBGSTART
            Set-RegistryValue -key $oneBackup.reqKey -name $oneBackup.reqName -type $oneBackup.originalValueKind -value $oneBackup.originalValue
            $valid = $error.Count -eq 0
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
            if (-not $valid) { break }
          }
        }


        if (-not $oneBackup.keyExisted) {

            DBG ('Will remove the key which was previously created: {0}' -f $oneBackup.reqKey)

            if (-not (Is-KeyValidForSafeDelete $oneBackup.reqKey)) {

              break
            }

            DBGSTART
            Remove-Item $oneBackup.reqKey -Recurse:$false -Force:$false -Confirm:$false
            $valid = $error.Count -eq 0
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
            if (-not $valid) { break }


            DBGIF $MyInvocation.MyCommand.Name { $oneBackup.preexistingKey -like '*\' }
            DBGIF $MyInvocation.MyCommand.Name { $oneBackup.reqKey -like '*\' }

            if ($oneBackup.preexistingKey -ne $oneBackup.reqKey) {

              $valid = $oneBackup.reqKey -like ('{0}\?*' -f $oneBackup.preexistingKey)
              DBGIF ('Ivalid request key vs. preexisting key: req = {0} | preex = {1}' -f $oneBackup.reqKey, $oneBackup.preexistingKey) { -not $valid }
              if (-not $valid) { break }

              [string] $oneParentKey = $oneBackup.reqKey
              do {
                
                $oneParentKey = (Split-Path -Parent $oneParentKey).TrimEnd('\')

                if ($oneParentKey -like ('{0}\?*' -f $oneBackup.preexistingKey)) {

                  DBG ('Delete a previously provisioned parent-path key: {0}' -f $oneParentKey)

                  if (-not (Is-KeyValidForSafeDelete $oneParentKey)) {

                    break
      
                  } else {

                    DBGSTART
                    Remove-Item $oneParentKey -Recurse:$false -Force:$false -Confirm:$false
                    $valid = $error.Count -eq 0
                    DBGER $MyInvocation.MyCommand.Name $error
                    DBGEND
                    if (-not $valid) { break }
                  }
                }
                
              } while (($oneParentKey -ne '') -and ($oneParentKey -ne $oneBackup.preexistingKey))

              DBGIF $MyInvocation.MyCommand.Name { $oneParentKey -eq '' }
          }
        }
      }
    }
  }
}


function global:Restart-WithAutologon ([string] $domain, [string] $user, [string] $password, [string] $runValue, [string] $runCmd, [bool] $doNotRestart, [int] $logonCount, [bool] $verifyAndPrelogonUser, [bool] $shutdownInstead)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  #DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $runValue }

  #[bool] $applySyncForegroundPolicy = ($global:thisOSRole -eq 'Workgroup Workstation') -or ($global:thisOSRole -eq 'Member Workstation') -or (Is-LocalComputerDomainController -rodcOnly $true)
  #[bool] $applySyncForegroundPolicy = ($global:thisOSRole -eq 'Workgroup Workstation') -or ($global:thisOSRole -eq 'Member Workstation') -or ($global:thisOSRole -eq 'BDC')
  # Note: current reasearch shows that not only RODCs, but also BDCs and then also subdomain PDCs need this setting
  #       so basically it seams reasonable to enable this setting for any domain members
  [bool] $applySyncForegroundPolicy = Is-LocalComputerMemberOfDomain

  if (Is-EmptyString $user) {
  
    DBG ("Removing autologon.")
  
    DBGSTART
    # there is a bug in the Remove-ItemProperty which stores in the $error variable although you specify the erIgnore explicitly
    Remove-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' AutoAdminLogon -Force #-EV erIgnore -EA SilentlyContinue
    Remove-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' AutoLogonCount -Force #-EV erIgnore -EA SilentlyContinue
    Remove-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' DefaultPassword -Force #-EV erIgnore -EA SilentlyContinue
    Remove-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' DefaultUserName -Force #-EV erIgnore -EA SilentlyContinue
    Remove-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' DefaultDomainName -Force #-EV erIgnore -EA SilentlyContinue

    if (Is-ValidString $runValue) {
    
      Remove-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' $runValue -Force #-EV erIgnore -EA SilentlyContinue
    }

    if ($applySyncForegroundPolicy) {

      DBG ('This is workstation machine, we should remove the Always wait for network registry setting')
      Remove-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' SyncForegroundPolicy -Force
    }

    DBGEND


    if ($global:thisOSVersion -like '5.*') {

      if (Is-ValidString $global:thisComputerDomainNetBIOS) {

        DBG ('Reset the default domain on 5.x to primary domain: {0}' -f $global:thisComputerDomainNetBIOS)
        Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' DefaultDomainName $global:thisComputerDomainNetBIOS String
        Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' AltDefaultDomainName $global:thisComputerDomainNetBIOS String

      } else {

        DBG ('Reset the default domain on 5.x to local computer name: {0}' -f $global:thisComputerNetBIOS)
        Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' DefaultDomainName $global:thisComputerNetBIOS String
        Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' AltDefaultDomainName $global:thisComputerNetBIOS String
      }

      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' DefaultUserName ([Environment]::UserName) String
      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' AltDefaultUserName ([Environment]::UserName) String
    }

    #DBGER $MyInvocation.MyCommand.Name $er
  }
  
  else {
  
    DBG ("Registering autologon.")
  
    if ($thisOSVersion -like '5.*') {
    
      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' AutoAdminLogon 1 String
    }
    
    else {
    
      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' AutoAdminLogon 1 DWord
    }
    
    if ($logonCount -lt 1) { 
    
      # Note: if installing updates, the machine sometimes restarts automatically and thus value of 1 is not sufficient
      #       since 2010 and until 2018 the number used was set to 2 but recently with Hyper-V 2016 emergence and cloud workloading
      #       some virtual machines began to freeze and hang at chaotic stages and if restarted forcibly the autologon might be lost
      #       so we upgraded the value to 4 just to be sure because we remove the autologon manually anyway
      DBG ('Logon count not specified, default to 4')
      $logonCount = 4
    }

    Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' AutoLogonCount $logonCount DWord
    Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' DefaultPassword $password String

    if (($thisOSVersion -like '5.*') -and (-not (Is-LocalDomain $domain $true))) {

      # this says that if the login is a domain login then we must use a single UPN@ instead of the two separate registry values

      DBGIF $MyInvocation.MyCommand.Name { -not ($domain.Contains('.')) }

      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' DefaultUserName ('{0}@{1}' -f $user, $domain) String

      DBGSTART
      Remove-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' DefaultDomainName -Force #-EV erIgnore -EA SilentlyContinue
      DBGEND

    } else {

      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' DefaultUserName $user String
      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' DefaultDomainName $domain String
    }

    if (Is-ValidString $runValue) {

      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run' $runValue $runCmd ExpandString
    }


    # Note: yes, we are still a "member workstation" when we have just joined a domain
    if ($applySyncForegroundPolicy) {

      # Note: there were latent instances of Windows 8 and Windows 8.1 not autologging without
      #       in the cases observed it was always when a new user was to be autologged
      DBG ('This is workstation machine, we should use the Always wait for network registry setting for autologon scenarios')
      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\WinLogon' SyncForegroundPolicy 1 dword
    }


    if ($verifyAndPrelogonUser) {

      DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $user }

      DBG ('Verify the autologon password is valid and let user profile create in the process')
      [int] $pwdVerificationExitCode = Prelogon-User $user $domain $password

      DBGIF 'Autologon identity verification failed, will not restart' { $pwdVerificationExitCode -ne 0 }
      $doNotRestart = $doNotRestart -or ($pwdVerificationExitCode -ne 0)
    }

    #----------------------
    # Note: Restart is not advisable when removing the autologon.
    #       Machines should remain running after all has been prepared, to remain in service
    #       for others to cooperate, and not to go into immediate restart which is also
    #       unnecessary - the last autologon has been is empty anyway

    if (-not $doNotRestart) {
    
      DBG "Sleep for 8sec before restart"
      Start-Sleep 8

      if (-not $shutdownInstead) {

        DBG ('Restarting')
        #DBGSTART
        #Restart-Computer -Force
        #DBGER $MyInvocation.MyCommand.Name $error
        #DBGEND
        Run-Process shutdown ('/r /f /t 0 /c "Sevecek BUILDER RESTART with autologon"')
      
      } else {

        DBG ('Shutting down instead of restarting')
        Run-Process shutdown ('/s /f /t 0 /c "Sevecek BUILDER SHUTDOWN with autologon"')
      }
    }
    
    else {
    
      if (-not $shutdownInstead) {

        DBG ('RESTART disabled.')

      } else {

        DBG ('SHUTDOWN disabled.')
      }
    }
  }
}



function global:Change-NIConWindowsXP ([string] $nicName, [string] $action)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $nicName }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $action }

  [bool] $did = $false

  DBG ('Get Shell object')
  DBGSTART
  $shell = $null
  $shell = New-Object -ComObject 'Shell.Application'
  # This always produces an error: The Win32 internal error "Not enough storage is available to process this command" 0x8 occurred when writing console output buffer at current cursor position
  # thus we just ignore as there are two other asserts and error capture points
  #DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $shell }

  DBG ('Get control panel')
  DBGSTART
  $controlPnl = $null
  $controlPnl = $shell.Namespace(3)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $controlPnl }

  if (Is-NonNull $controlPnl) {

    DBG ('Parse control panel items')
    $networkConns = $null
    foreach ($oneControlPnlItem in $controlPnl.Items()) {

      DBG ('One control panel item: {0}' -f $oneControlPnlItem.Name)

      if ($oneControlPnlItem.Name -eq 'Network Connections') {

        DBG ('Network Connections control panel item found: {0} | {1:s}' -f $oneControlPnlItem.Path, $oneControlPnlItem.ModifyDate)
        $networkConns = $oneControlPnlItem.GetFolder
        break
      }
    }

    DBGIF $MyInvocation.MyCommand.Name { Is-Null $networkConns }

    if (Is-NonNull $networkConns) {

      DBG ('Parse network connections')
      foreach ($oneNetworkConn in $networkConns.Items()) {

        DBG ('One network connection: {0}' -f $oneNetworkConn.Name)
 
        if ($oneNetworkConn.Name -eq $nicName) {

          DBG ('Found NIC: {0} | {1}' -f $oneNetworkConn.Name, $oneNetworkConn.Path)

          $nic = $oneNetworkConn
          break
        }
      }

      DBGIF $MyInvocation.MyCommand.Name { Is-Null $nic }

      if (Is-NonNull $nic) {

        DBG ('Parse NIC verbs')
        foreach ($nicVerb in $nic.Verbs()) {

          DBG ('One NIC verb: {0}' -f $nicVerb.Name)

          if ($nicVerb.Name.Replace('&', '') -eq $action) {
    
            DBG ('Call NIC verb: {0}' -f $nicVerb.Name)
            DBGSTART
            [void] $nicVerb.DoIt()
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND

            $did = $true

            # Note: do not break here to see all verbs in case of any troubleshooting
          }
        }
      }
    }
  }

  DBGIF $MyInvocation.MyCommand.Name { -not $did }
  DBG ('Change NIC on Windows XP finished.')
}


function global:Disable-NIC ([WMI] $oneNIC)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $oneNIC }
  
  DBG ("Disabling NIC: {0}, {1}, {2}, {3}, {4}, {5}" -f $oneNic.NetConnectionID, $oneNic.Name, $oneNic.DeviceID, $oneNic.AdapterType, $oneNic.MACAddress, $oneNic.Speed)
  
  # Note: on Windows 2008/Vista, although the .Disable() method returns 0 for unplugged (non-connected) NICs
  #       the network adapter does not go disabled in fact.
  #       For 2008R2/7 this works fine
  #
  # Note: The NETSH also does not work at all on Windows XP as the interfaces are "dedicated": http://support.microsoft.com/kb/262265
  #

  $jmeno = "ondra"

  if ($global:thisOSVersionNumber -eq 5.1) {

    #DBGIF 'No way how to disable NIC on Windows XP' { $true }
    Change-NIConWindowsXP $oneNic.NetConnectionID 'Disable'
  
  } elseif (($thisOSVersionNumber -eq 5.2) -or ($thisOSVersionNumber -eq 6.0)) {

    DBG ("Disabling NIC by using NETSH.")
    
    Run-Process 'NETSH' "INTERFACE SET INTERFACE Name=`"$($oneNic.NetConnectionID)`" Admin=DISABLED"
    #$cmdRs = & { NETSH INTERFACE SET INTERFACE Name="$($oneNic.NetConnectionID)" Admin=DISABLED } | Out-String
    #DBG ("CMD output: {0}" -f $cmdRs)
  }
      
  else {
        
    DBG ("Disabling NIC by using WMI.")
      
    DBGSTART
    $wmiRs = $null
    $wmiRs = $oneNic.Disable()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGWMI $wmiRs
  }  
}


function global:Rename-NIC ([WMI] $oneNIC, [string] $newName)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $oneNIC }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $newName }
  
  DBG ("Renaming NIC: {0}/{1}, {2}, {3}, {4}, {5}, {6}" -f $oneNic.NetConnectionID, $newName, $oneNic.Name, $oneNic.DeviceID, $oneNic.AdapterType, $oneNic.MACAddress, $oneNic.Speed)
  
  if ($thisOSVersion -like '5.*') {

    DBG ("Renaming NIC by using NETSH.")
      
    Run-Process 'NETSH' "INTERFACE SET INTERFACE Name=`"$($oneNic.NetConnectionID)`" NewName=`"$newName`""
    #$cmdRs = & { NETSH INTERFACE SET INTERFACE Name="$($oneNic.NetConnectionID)" NewName="$newName" } | Out-String
    #DBG ("CMD output: {0}" -f $cmdRs)
  }
      
  else {
        
    DBG ("Renaming NIC by using WMI.")
      
    DBGSTART
    $oneNic.NetConnectionID = $newName
    $wmiRs = $null
    $wmiRs = $oneNic.Put()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGWMI $wmiRs
  }  
}


function global:Adjust-WMIQuota ([string] $computer, [int] $memory)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  if (Is-EmptyString $computer) { $computer = '.' }

  DBG ('Get quota settings')
  DBGSTART
  $quotaCfg = Get-WMIQuerySingleObject $computer 'SELECT * FROM __ProviderHostQuotaConfiguration' $false 'root'
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  if (Is-NonNull $quotaCfg) {
    
    DBG ('Current quota settings: MemoryAllHosts = {0} | MemoryPerHost = {1} | HandlesPerHost = {2} | ProcessLimitAllHosts = {3} | ThreadsPerHost = {4}' -f $quotaCfg.MemoryAllHosts, $quotaCfg.MemoryPerHost, $quotaCfg.HandlesPerHost, $quotaCfg.ProcessLimitAllHosts, $quotaCfg.ThreadsPerHost)
    
    if ($memory -gt 0) {
    
      $newMemory = $memory
    }
    
    else {
    
      $newMemory = $quotaCfg.MemoryPerHost * 2
    }
    
    $newAllMemory = $quotaCfg.MemoryAllHosts
    if ($newAllMemory -lt $newMemory) { $newAllMemory = $newMemory }
    
    DBGSTART
    $quotaCfg.MemoryAllHosts = $newAllMemory
    $quotaCfg.MemoryPerHost = $newMemory
    $wmiRs = $null
    $wmiRs = $quotaCfg.Put()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGWMI $wmiRs
  }

  DBG ('Get quota settings to confirm the change')
  DBGSTART
  $quotaCfg = Get-WMIQuerySingleObject $computer 'SELECT * FROM __ProviderHostQuotaConfiguration' $false 'root'
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  DBG ('New quota settings: MemoryAllHosts = {0} | MemoryPerHost = {1} | HandlesPerHost = {2} | ProcessLimitAllHosts = {3} | ThreadsPerHost = {4}' -f $quotaCfg.MemoryAllHosts, $quotaCfg.MemoryPerHost, $quotaCfg.HandlesPerHost, $quotaCfg.ProcessLimitAllHosts, $quotaCfg.ThreadsPerHost)


  DBG ('Reset WmiPrvSE processes')
  DBGSTART
  $wmiWorkers = Get-WMIQueryArray $computer 'SELECT * FROM Win32_Process WHERE Name = "WmiPrvSE.exe"'
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  DBG ('Found WmiPrvSE processes. Terminating (no error): {0}' -f (Get-CountSafe $wmiWorkers))
  
  if (Is-NonNull $wmiWorkers) {
  
    $wmiWorkers | % { 
    
      DBGSTART
      $_.Terminate() | Out-Null
      DBGEND
      
    }
  }
}

function global:Enable-RRAS ([bool] $vpn)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBG ('Get the RRAS service status')
  DBGSTART
  $rrasSvc = $null
  $rrasSvc = Get-WmiQuerySingleObject '.' 'SELECT * FROM Win32_Service WHERE Name = "RemoteAccess"'
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $rrasSvc }
  DBG ('RRAS service status: {0} | {1}' -f $rrasSvc.StartMode, $rrasSvc.State)

  [int] $rrasType = 0
  $rrasType = Get-RegValue '.' 'SYSTEM\CurrentControlSet\Services\RemoteAccess\Parameters' RouterType 'DWORD'

  $rrasType = $rrasType -bor 2

  if ($vpn) {

    $rrasType = $rrasType -bor 4
  }

  # Note: ROUTER_TYPE_LAN = 0x02
  #       ROUTER_TYPE_WAN = 0x04
  #       ROUTER_TYPE_RAS = 0x01
  #       IPV6_ROUTER_TYPE_RAS = 0x08
  #       IPV6_ROUTER_TYPE_LAN = 0x10
  #       IPV6_ROUTER_TYPE_WAN = 0x20
  Set-RegistryValue 'HKLM:\SYSTEM\CurrentControlSet\Services\RemoteAccess\Parameters' RouterType $rrasType DWord
  # Note: all events
  Set-RegistryValue 'HKLM:\SYSTEM\CurrentControlSet\Services\RemoteAccess\Parameters' LoggingFlags 3 DWord
  # Note: 1 = is configured, 0 = not configured
  Set-RegistryValue 'HKLM:\SYSTEM\CurrentControlSet\Services\RemoteAccess' ConfigurationFlags 1 DWord

  if ($rrasSvc.StartMode -eq 'Disabled') {

    DBG ('Enable the RRAS service for autostart')
    DBGSTART
    Set-Service -Name RemoteAccess -StartupType Automatic
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Start the service')
    DBGSTART
    Start-Service -Name RemoteAccess
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

  } else {

    DBG ('Restart the service only')
    DBGSTART
    Restart-Service -Name RemoteAccess -Force
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }
}

function global:Configure-NIC ([WMI] $oneNIC, [string] $ips, [string] $masks, [string] $gw, [string] $dnsList, [bool] $enableRouting, [string] $natType, [string] $dnsUpdate, [bool] $disableNetBIOS)
# $dnsUpdate - Primary, Both, None
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $oneNIC }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $ips }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $masks }

  DBG ("Configuring NIC: {0}, {1}, {2}, {3}, {4}, {5} | if = {6} | idx = {7}" -f $oneNic.NetConnectionID, $oneNic.Name, $oneNic.DeviceID, $oneNic.AdapterType, $oneNic.MACAddress, $oneNic.Speed, $oneNic.InterfaceIndex, $oneNic.Index)

  if ($global:thisOSVersionNumber -le 5.1) {

    # Note: there is no InterfaceIndex column on Windows XP, thus we make
    #       do with Index instead, although it is only WMI internal representation    
    DBGSTART
    $nicCfg = Get-WMIQuerySingleObject '.' "SELECT * FROM Win32_NetworkAdapterConfiguration WHERE Index = '$($oneNic.Index)'"
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    # Note: just a matter of some asserting
    $nicCfgRelated = Get-WmiRelatedSingle $oneNic 'Win32_NetworkAdapterConfiguration'
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $nicCfgRelated }
    DBGIF $MyInvocation.MyCommand.Name { $nicCfgRelated.Index -ne $nicCfg.Index }

  } else {

    DBGSTART
    $nicCfg = Get-WMIQuerySingleObject '.' "SELECT * FROM Win32_NetworkAdapterConfiguration WHERE InterfaceIndex = '$($oneNic.InterfaceIndex)'"
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    $nicCfgRelated = Get-WmiRelatedSingle $oneNic 'Win32_NetworkAdapterConfiguration'
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $nicCfgRelated }
    DBGIF $MyInvocation.MyCommand.Name { $nicCfgRelated.Index -ne $nicCfg.Index }
    DBGIF $MyInvocation.MyCommand.Name { $nicCfgRelated.InterfaceIndex -ne $nicCfg.InterfaceIndex }
  }

  # On Hyper-V 2008 R2: Microsoft Virtual Machine Bus Network Adapter
  # On Hyper-V 2012:    Microsoft Hyper-V Network Adapter
  DBGIF ('Weird NIC on VM: {0} | {1} | {2} | {3}' -f $oneNIC.InterfaceIndex, $oneNIC.NetConnectionID, $nicCfg.ServiceName, $nicCfg.MACAddress) { $global:runningInHyperV -and (($nicCfg.ServiceName -ne 'netvsc') -or (($oneNIC.ProductName -ne 'Microsoft Hyper-V Network Adapter') -and ($oneNIC.ProductName -ne 'Microsoft Virtual Machine Bus Network Adapter') -and ($oneNIC.ProductName -ne 'Hyper-V Virtual Ethernet Adapter'))) }
  DBGIF $MyInvocation.MyCommand.Name { $oneNIC.MacAddress -ne $nicCfg.MacAddress }

  if ($ips -eq $global:emptyValueMarker) {
    
    DBG ("Setting NIC to obtain IP/Mask/GW config from DHCP")
      
    DBGSTART
    $wmiRs = $null
    $wmiRs = $nicCfg.EnableDHCP()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGWMI $wmiRs
  }

  else {
    
    [string[]] $ipAddresses = $ips.Split($global:ipMultivalueSeparator)
    [string[]] $ipMasks = $masks.Split($global:ipMultivalueSeparator)
    DBG ('IPs and masks requested: {0} | {1}' -f ($ipAddresses -join ';'), ($ipMasks -join ';'))
    DBGIF $MyInvocation.MyCommand.Name { $ipAddresses.Count -ne $ipMasks.Count }
    
    foreach ($oneIPAddress in $ipAddresses) {
    
      DBGIF ('Weird IP address requested: {0}' -f $oneIPAddress) { -not (Is-IPv4OrIPv6Address $oneIPAddress) }
    }


    DBG ("Configuring static IP/Mask")
    DBG ("Current config: {0}, {1}" -f (Format-MultiValue($nicCfg.IPAddress)), (Format-MultiValue($nicCfg.IPSubnet)))
    
    do {

      DBGSTART
      $wmiRs = $null
      $wmiRs = $nicCfg.EnableStatic($ipAddresses, $ipMasks)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGWMI $wmiRs
    
      if ($wmiRs.ReturnValue -eq 2147786788) {
      
        DBG ("Known error KB2530185 occured. Going to rerun the config attempt after 5sec. sleep")
        Start-Sleep 5
      }

    # Note: This is because of the 2008R2/7 bug: http://support.microsoft.com/kb/2530185
    } while ($wmiRs.ReturnValue -eq 2147786788)
      
    if ((Is-ValidString $gw) -and ($gw -ne $global:emptyValueMarker)) {
      
      DBG ('Configuring static GW: {0}' -f $gw)
    
      DBGSTART
      $wmiRs = $null
      $wmiRs = $nicCfg.SetGateways($gw)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGWMI $wmiRs
    }
      
    else {
      
      DBG ("Configuring no GW")
    
      DBGSTART
      $wmiRs = $null
      $wmiRs = $nicCfg.SetGateways($null)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGWMI $wmiRs
    }
  }

  if ($disableNetBIOS) {

    DBG ('Disabling NetBIOS on the NIC')
    DBGSTART
    $wmiRs = $null
    $wmiRs = $nicCfg.SetTcpipNetbios(2)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGWMI $wmiRs
  }

  if ((Is-EmptyString $dnsList) -or ($dnsList -eq $global:emptyValueMarker)) {
    
    DBG ("Setting NIC to obtain DNS config from DHCP or no DNS servers")
      
    DBGSTART
    $wmiRs = $null
    $wmiRs = $nicCfg.SetDNSServerSearchOrder($null)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGWMI $wmiRs
  }

  else {

    [string[]] $dnsSrvs = Split-MultiValue $dnsList
    DBG ("Setting static DNS server search order: #{0} = {1} | {2}" -f (Count-MultiValue $dnsList), $dnsList, (Get-CountSafe $dnsSrvs))

    Wait-Periodically -maxTrialCount 5 -sleepSec 7 -sleepMsg 'Waiting for undefined 84 IP error disappearance' -scriptBlockWhichReturnsTrueToStop {

      # Note: it has been observed on Windows 2016 that this setting sometimes fails with erro 84 = IP not enabled on adapter
      #       thus I have decided to try waiting, because the problem does not have any particular and visible reason
      DBGSTART
      $wmiRs = $null
      $wmiRs = $nicCfg.SetDNSServerSearchOrder($dnsSrvs)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGWMI $wmiRs

      return ($wmiRs.ReturnValue -eq 0)
    }
  } 
  
  #=================
  #if ($thisOSVersion -like '5.*') {

    if ($enableRouting) {
    
      DBG ("Enabling routing for the NIC by using NETSH in Win5.x and Win6.x")
        
      Run-Process 'NETSH' "ROUTING IP ADD INTERFACE Name=`"$($oneNic.NetConnectionID)`" State=ENABLE"
      Run-Process 'NETSH' "ROUTING IP SET INTERFACE Name=`"$($oneNic.NetConnectionID)`" State=ENABLE"

      Enable-RRAS
      
    } elseif ($thisOSVersion -like '5.*') {

      DBG ("Disabling routing for the NIC by using NETSH in Win5.x")
      
      # Note that the interfaces are most probably not registered with the router.
      # So in such a case, we ignore the 1 error code. I actually don't know if it is 
      # the only returned error code, but it is actualy the one returned if the
      # interface is not registered at all.
      Run-Process 'NETSH' "ROUTING IP DELETE INTERFACE Name=`"$($oneNic.NetConnectionID)`"" -ignoreExitCodes @(1)
    
    } elseif ($thisOSVersionNumber -ge 6.0) {

      DBG ('Disabling routing for the NIC by using NETSH in Win6.x')
      Run-Process 'NETSH' "INTERFACE IPv4 SET INTERFACE Interface=`"$($oneNic.NetConnectionID)`" Forwarding=DISABLED"
    }
  #}
  
  #else {

    <#switch ([bool] $enableRouting) {
    
      $true { $stateKeyWord = 'ENABLED' }
      $false { $stateKeyWord = 'DISABLED' }
    }

    DBG ("Setting routing for the NIC by using NETSH in Win6.x+: {0}" -f $stateKeyWord)
    Run-Process 'NETSH' "INTERFACE IPv4 SET INTERFACE Interface=`"$($oneNic.NetConnectionID)`" Forwarding=$stateKeyWord"#>
  #}

  #=============
  switch ($natType) {
  
    'I' { $natType = 'Private' }
    'E' { $natType = 'Full' }
    default { }
  }

  if (Is-ValidString $natType) {
    
    DBG ('Setting NAT for the NIC to be: {0}' -f $natType)
    Run-Process 'NETSH' 'ROUTING IP NAT INSTALL'
    Run-Process 'NETSH' ('ROUTING IP NAT ADD INTERFACE Name="{0}" Mode={1}' -f $oneNic.NetConnectionID, $natType)
    Run-Process 'NETSH' ('ROUTING IP NAT SET INTERFACE Name="{0}" Mode={1}' -f $oneNic.NetConnectionID, $natType)
  }


  if ($dnsUpdate -eq 'Both') {

    Set-NICDNSRegistration $nicCfg $true $true
      
  } elseif ($dnsUpdate -eq 'None') { 

    Set-NICDNSRegistration $nicCfg $false $false

  } else {
      
    Set-NICDNSRegistration $nicCfg $true $false
  }       
}


function global:Set-NICDNSRegistration ([WMI] $nic, [bool] $primaryOnly, [bool] $connSpecific)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $nic }
  DBGIF $MyInvocation.MyCommand.Name { $connSpecific -and (-not $primaryOnly) }

  DBGSTART
  $wmiRs = $null
  $wmiRs = $nic.SetDynamicDNSRegistration($primaryOnly, $connSpecific)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGWMI $wmiRs
}


function global:Configure-NICbyName ([string] $nicName, [string] $ips, [string] $masks, [string] $gw, [string] $dnsList, [bool] $enableRouting, [string] $natType)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $nicName }
  
  if (Is-ValidString $nicName) {

    $nicNameAndFlags = $nicName
    $nicFlags = Get-ValueFlags $nicName
    $nicName = Strip-ValueFlags $nicName
    
    DBG ("Processing NIC: {0} | {1} | {2} | {3} | {4}" -f $nicName, $nicFlags, $ips, $masks, $gw)
    $oneNIC = Get-WMIQueryArray '.' "SELECT * FROM Win32_NetworkAdapter WHERE NetConnectionID ='$nicName'"
    
    if ($oneNIC.Count -eq 1) {
    
      if (Has-ValueFlags $nicNameAndFlags 'B') {

        $dnsUpdate = 'Both'
      
      } elseif (Has-ValueFlags $nicNameAndFlags 'N') { 

        $dnsUpdate = 'None'

      } else {
      
        $dnsUpdate = 'Primary'
      }       


      Configure-NIC $oneNIC[0] $ips $masks $gw $dnsList $enableRouting $natType $dnsUpdate

      if (Has-ValueFlags $nicNameAndFlags 'D') { 
        
        DBG ("NIC config set to Disable.")
        Disable-NIC $oneNIC[0]
      }
    }
    
    else
    {
      DBG ("Found nonsense number of NICs by name: {0}. Skipping config." -f $oneNIC.Count)
    }
  }
}


function global:New-DNComponent ([string] $parentDn, [string] $dn, [string] $dnType, [hashtable] $attrAndValues, [string] $ACEs)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dn }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dnType }

  [PSObject] $component = New-Object PSObject
  $component | Add-Member -Name dn -MemberType NoteProperty -Value $dn
  $component | Add-Member -Name dnType -MemberType NoteProperty -Value $dnType
  $component | Add-Member -Name attrAndValues -MemberType NoteProperty -Value $attrAndValues
  $component | Add-Member -Name ACEs -MemberType NoteProperty -Value $ACEs
  $component | Add-Member -Name fullDn -MemberType NoteProperty -Value ('{0},{1}' -f $dn, $parentDn)
  
  return $component
}


function global:Add-DNComponent ([System.Collections.ArrayList] $dnList, [string] $dn, [string] $dnType, [hashtable] $attrAndValues, [string] $ACEs, [string] $rootDN)
{
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $dnList }

  if ($dnList.Count -gt 0) {

    [void] $dnList.Add((New-DNComponent ($dnList[$dnList.Count-1].fullDn) $dn $dnType $attrAndValues $ACEs))

  } else {

    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $rootDN }
    [void] $dnList.Add((New-DNComponent $rootDN $dn $dnType $attrAndValues $ACEs))
  }
}


function global:Create-DNPath ([System.Collections.ArrayList] $dnComponents, [object] $rootDE, [ref] $deListRef, [string] $ACEs)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $rootDE }
  DBGIF $MyInvocation.MyCommand.Name { $rootDE -isnot [System.DirectoryServices.DirectoryEntry] }  
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $dnComponents }
  DBGIF $MyInvocation.MyCommand.Name { $dnComponents.Count -le 0 }
  DBGIF $MyInvocation.MyCommand.Name { $deListRef.Value -isnot [System.Collections.ArrayList] }
  
  [System.Collections.ArrayList] $deList = @()
  $outDE = $null

  if ((Is-NonNull $rootDE) -and (Is-NonNull $dnComponents) -and ($dnComponents.Count -ge 1)) {
  
    DBG ("Going to create {0} DN components under {1}" -f $dnComponents.Count, $rootDE.Path)
    
    $baseDE = $rootDE
    foreach ($oneComponent in $dnComponents) {
    
      DBG ("Processing DN: {0}, type = {1}, parent = {2}" -f $oneComponent.dn, $oneComponent.dnType, $baseDE.Path)
      
      $thisDE = Get-SubDE $oneComponent.dn $baseDE ([ref] $deList) $true
      
      if (Is-Null $thisDE) {
      
        DBG ("Component {0} does not exist. Creating." -f $oneComponent.dn)
        
        DBGSTART
        $thisDE = $baseDE.Create($oneComponent.dnType, $oneComponent.dn)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

        if ((Is-NonNull $thisDE) -and (Is-NonNull $oneComponent.attrAndValues) -and ((Get-CountSafe $oneComponent.attrAndValues) -gt 0)) {

          DBG ('Fill default attributes into the new object')
          foreach ($oneAttr in $oneComponent.attrAndValues.Keys) {

            DBG ('Fill one attribute: {0} | {1}' -f $oneAttr, $oneComponent.attrAndValues[$oneAttr])
            DBGSTART
            $thisDE.Put($oneAttr, $oneComponent.attrAndValues[$oneAttr])
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
          }
        }

        DBGSTART
        $adsiRs = $thisDE.SetInfo()
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
        
        if (Is-NonNull $thisDE) {

          $deList.Add($thisDE) | Out-Null
        }
      }
      
      else {

        DBG ("Component {0} already exists." -f $oneComponent.dn)
      }

      $baseDE = Get-OthDE (GDES $thisDE distinguishedName) $baseDE ([ref] $deList)

      if (Is-Null $baseDE) {
    
        DBG ('Cannot create the DN component, breaking prematurelly')
        break
      }
    }
    

    if (Is-NonNull $baseDE) {

      DBG ("Final existing ADSI path: {0}" -f $baseDE.Path)
    
      # Note: we must copy out userName/password because it is not copied automatically within .Create()
      $outDE = Get-OthDE (GDES $baseDE distinguishedName) $baseDE $deListRef
    }


    
    $dnComponents.Reverse()

    DBG 'Set security on objects in reverse order'
    foreach ($oneComponent in $dnComponents) {
    
      if (Is-ValidString $oneComponent.ACEs) {

        $aceList = Split-MultiValue $oneComponent.ACEs
        DBG ('Setting security on object: {0} | {1}' -f $oneComponent.fullDn, ($aceList | Out-String))
          
        foreach ($oneACE in $aceList) {
          
          $oneACEDef = Strip-ValueFlags $oneACE
          $oneACEFlags = Get-ValueFlags $oneACE

          DBG ('ACE parameters: {0} | {1} | {2}' -f $oneACE, $oneACEDef, $oneACEFlags)

          if (Is-ValidString $oneACEDef) {

            if ($oneACEDef -eq '-') {
            
              Run-Process 'DSACLS' ('"{0}" /P:Y' -f $oneComponent.fullDn) $false $baseDE.SevecekDEUserName $null $baseDE.SevecekDEPassword

            } elseif ($oneACEDef -eq '+') {
            
              Run-Process 'DSACLS' ('"{0}" /P:N' -f $oneComponent.fullDn) $false $baseDE.SevecekDEUserName $null $baseDE.SevecekDEPassword
            
            } else {
                     
              DBGSTART
              $oneAceAccount = $oneACEDef.SubString(0, $oneACEDef.IndexOf(':'))
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND

              Assert-AccountExists $oneAceAccount 'ACE account determined as'
              Run-Process 'DSACLS' ('"{0}" {1} /G "{2}"' -f $oneComponent.fullDn, $oneACEFlags, $oneACEDef) $false $baseDE.SevecekDEUserName $null $baseDE.SevecekDEPassword
            }
          }
        }
      }
    }
  }

  Dispose-List ([ref] $deList)
  
  return $outDE
}


function global:Open-GPO ([string] $gpoName, [string] $domain)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domain }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $gpoName }

  $resGPO = $null
  
  if ((Is-ValidString $gpoName) -and (Is-ValidString $domain)) {

    DBG ('Initializing GPMC engine: domain = {0}' -f $domain)
    DBGSTART
    $gpm = New-Object -ComObject 'GPMgmt.GPM'
    $gpmConst = $gpm.GetConstants()
    $gpmDomain = $gpm.GetDomain($domain, '', 0)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Open search criteria for the requested GPO name: {0}' -f $gpoName)
    DBGSTART  
    $gpmSearchCrit = $gpm.CreateSearchCriteria()
    $gpmSearchCrit.Add($gpmConst.SearchPropertyGPODisplayName, $gpmConst.SearchOpEquals, $gpoName)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Searching for the GPO')
    DBGSTART
    $foundGPOs = $gpmDomain.SearchGPOs($gpmSearchCrit)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  
    DBG ('Found GPOs in the domain: {0}' -f (Get-CountSafe $foundGPOs))
    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $foundGPOs) -gt 1 }

    if ((Get-CountSafe $foundGPOs) -eq 0) {

      $resGPO = $null
      DBG ('No such GPO found.')

    } elseif ((Get-CountSafe $foundGPOs) -eq 1) {

      $resGPO = $foundGPOs.Item(1)
      DBG ('Just one such GPO exists: {0} | {1} | created = {2} | modified = {3}' -f $resGPO.DisplayName, $resGPO.Id, $resGPO.CreationTime, $resGPO.ModificationTime)

    } elseif ((Get-CountSafe $foundGPOs) -gt 1) {

      $resGPO = $foundGPOs.Item(2)
      DBG ('Found more GPOs with the same display name, returning the first one: {0} | {1} | created = {2} | modified = {3}' -f $resGPO.DisplayName, $resGPO.Id, $resGPO.CreationTime, $resGPO.ModificationTime)
    }
  }

  return $resGPO
}

function global:Enable-LinkedGPO ([string] $gpo, [string] $domain = $global:thisComputerDomain, [string] $domainDN = $global:thisComputerDomainDN, [string] $ou)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $gpo }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domain }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domainDN }
  
  if (Is-ValidString $gpo) {

    DBG ('Initializing GPMC engine')
    DBGSTART
    $gpm = $null
    $gpm = New-Object -ComObject 'GPMgmt.GPM'
    $gpmConst = $null
    $gpmConst = $gpm.GetConstants()
    $gpmDomain = $null
    $gpmDomain = $gpm.GetDomain($domain, '', 0)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    $linkDN = $domainDN

    if (Is-ValidString $ou) {

      $linkDN = '{0},{1}' -f $ou, $domainDN
    }

    $foundGPO = Open-GPO -gpoName $gpo -domain $domain
    
    if (Is-NonNull $foundGPO) {

      DBG ('Will try to find the GPO at: {0} | {1} | {2}' -f $gpo, $foudGPO.ID, $linkDN)
      DBGSTART
      $gpmSOM = $null
      $gpmSOM = $gpmDomain.GetSOM($linkDN)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGIF $MyInvocation.MyCommand.Name { Is-Null $gpmSOM }

      if (Is-NonNull $gpmSOM) {

        DBG ('Try find the existing link for the current GPO')
        DBGSTART
        $gpmLinksExisting = $null
        $gpmLinksExisting = $gpmSOM.GetGPOLinks()
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBGIF $MyInvocation.MyCommand.Name { Is-Null $gpmLinksExisting }

        if (Is-NonNull $gpmLinksExisting) {

          $gpmLink = $null
          for ($i = 1; $i -le $gpmLinksExisting.Count; $i ++) {
          
            $oneGpmLinkExisting = $gpmLinksExisting.Item($i)
            DBG ('One GPO link found at the current SOM: {0} | enabled = {1} | forced = {2} | gpo = {3} | ourGPO = {4}' -f $linkDN, $oneGpmLinkExisting.Enabled, $oneGpmLinkExisting.Enforced, $oneGpmLinkExisting.GPOID, $foundGPO.ID)

            if ($foundGPO.ID -eq $oneGpmLinkExisting.GPOID) {

              DBG ('GPO link found')
              $gpmLink = $oneGpmLinkExisting
              break
            }
          }

          DBGIF $MyInvocation.MyCommand.Name { Is-Null $gpmLink }
          if (Is-NonNull $gpmLink) {

            DBG ('Enable the link')
            DBGIF $MyInvocation.MyCommand.Name { $gpmLink.Enabled }
            DBGSTART
            $gpmLink.Enabled = $true
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
          }
        }
      }
    }
  }
}

function global:Restore-GPO ([string] $backupFolder, [string] $gpo, [string] $domain = $global:thisComputerDomain, [string] $linkDN, [bool] $enforce, [bool] $disabled, [string] $wmiFilter, [string] $nameSfx, [string] $dacl, [bool] $existingOnly)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $backupFolder }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domain }
  DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $gpo) -and ($existingOnly) }
  #DBGIF $MyInvocation.MyCommand.Name { ((Is-ValidString $gpo) -and (Is-EmptyString $linkDN)) }
  DBGIF $MyInvocation.MyCommand.Name { (-not (Test-Path $backupFolder)) }

  DBG ('Initializing GPMC engine')
  DBGSTART
  $gpm = New-Object -ComObject 'GPMgmt.GPM'
  
  $gpmConst = $gpm.GetConstants()
  $gpmDomain = $gpm.GetDomain($domain, '', 0)
  $gpmBackup = $gpm.GetBackupDir($backupFolder)
  
  $gpmSearchCrit = $gpm.CreateSearchCriteria()
  $gpmSearchCrit.Add($gpmConst.SearchPropertyBackupMostRecent, $gpmConst.SearchOpEquals, $true)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  DBG ("Searching for backups")
  DBGSTART
  $backups = $gpmBackup.SearchBackups($gpmSearchCrit)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  
  DBG ('Found GPOs in the backup: {0}' -f (Get-CountSafe $backups))
  
  DBG ('Should import a specific GPO? specific = {0} | name = {1}' -f (Is-ValidString $gpo), $gpo)
  [bool] $explicitGPOprocessed = $false
 
  
  foreach ($oneBackup in $backups) {

    $newGPO = $null
    $existingGpoFound = $false


    if (Is-ValidString $nameSfx) {

      $gpoDisplayName = '{0} ({1})' -f $oneBackup.GPODisplayName, $nameSfx
      DBG ('Will search for a name-suffixed GPO: sfx = {0} | bckName = {1} | search = {2}' -f $nameSfx, $oneBackup.GPODisplayName, $gpoDisplayName)
    
    } else {

      $gpoDisplayName = $oneBackup.GPODisplayName
    }


    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneBackup.GPODisplayName }

    if ((Is-EmptyString $gpo) -or ($oneBackup.GPODisplayName -eq $gpo)) {

      DBG ('Try open the GPO if it already exists: bckId = {0} | bckName = {1} | bckGPOID = {2} | openName = {3}' -f $oneBackup.Id, $oneBackup.GPODisplayName, $oneBackup.GPOId, $gpoDisplayName)
      $newGPO = Open-GPO $gpoDisplayName $domain
      $existingGpoFound = Is-NonNull $newGPO

      if ((Is-Null $newGPO) -and (-not $existingOnly)) {

        DBG ("Creating and importing GPO: {0} | {1} | {2} | {3}" -f $oneBackup.Id, $oneBackup.GPODisplayName, $oneBackup.GPOId, $gpoDisplayName)
   
        DBGSTART
        $newGPO = $gpmDomain.CreateGPO()
        $newGPO.DisplayName = $gpoDisplayName

        $importRs = $newGPO.Import(0, $oneBackup)
        DBG ('Import status: {0} | {1}' -f $importRs.OverallStatus(), $newGPO.Id)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      
        if ($newGPO.Id -match $global:rxGUIDAnyFormat) {

          $newGPOSysvol = '\\{0}\SYSVOL\{0}\Policies\{1}' -f $domain, $newGPO.Id
        }

        DBG ('Newly imported GPO sysvol path: {0}' -f $newGPOSysvol)
        DBGIF ('Invalid newly imported GPO sysvol path: {0}' -f $newGPOSysvol) { (Is-EmptyString $newGPOSysvol) -or (-not (Test-Path -Literal $newGPOSysvol)) }

        if (Test-Path -Literal $newGPOSysvol) {

          [int] $filesInUser = Get-ChildItem (Join-Path $newGPOSysvol User) -Force -Recurse | ? { -not $_.PsIsContainer } | Measure | Select -Expand Count
          [int] $filesInMachine = Get-ChildItem (Join-Path $newGPOSysvol Machine) -Force -Recurse | ? { -not $_.PsIsContainer } | Measure | Select -Expand Count

          DBG ('Files found in imported GPO: user = #{0} | machine = #{1}' -f $filesInUser, $filesInMachine)
          DBGIF ('No files found in imported GPO: {0}' -f $gpo) { ($filesInUser -eq 0) -and ($filesInMachine -eq 0) }

          if (($filesInUser -gt 0) -or ($filesInMachine -gt 0)) {

            if ($filesInUser -eq 0) {

              DBG ('Disabling user part of the GPO: {0}' -f $gpo)
              DBGSTART
              $newGPO.SetUserEnabled($false)
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND
            
            } elseif ($filesInMachine -eq 0) {

              DBG ('Disabling machine part of the GPO: {0}' -f $gpo)
              DBGSTART
              $newGPO.SetComputerEnabled($false)
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND
          
            } else {

              DBG ('Leaving both user and machine configurations enabled for the GPO: {0}' -f $gpo)
            }
          }
        }

      } else {

        DBG ('GPO already exists. No action required anyway: {0} | {1}' -f $existingGpoFound, $newGPO.Id)
      }

      $explicitGPOprocessed = (Is-ValidString $gpo) -and ($oneBackup.GPODisplayName -eq $gpo) -and (Is-NonNull $newGPO)
    }


    $shouldProcess = (Is-NonNull $newGPO) -and (($existingOnly -and $existingGpoFound) -or (-not $existingOnly))

    if ((Is-ValidString $linkDN) -and $shouldProcess) {

      DBG ('GPO already exists or was just created. Going to link the object to its SOM: {0}' -f $linkDN)
      DBGSTART
      $gpmSOM = $null
      $gpmSOM = $gpmDomain.GetSOM($linkDN)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGIF $MyInvocation.MyCommand.Name { Is-Null $gpmSOM }

      DBG ('Try find an existing link for the current GPO')
      DBGSTART
      $gpmLinksExisting = $null
      $gpmLinksExisting = $gpmSOM.GetGPOLinks()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGIF $MyInvocation.MyCommand.Name { Is-Null $gpmLinksExisting }

      $gpmLink = $null
      for ($i = 1; $i -le $gpmLinksExisting.Count; $i ++) {

        $oneGpmLinkExisting = $gpmLinksExisting.Item($i)
        DBG ('One GPO link found at the current SOM: {0} | enabled = {1} | forced = {2} | gpo = {3} | ourGPO = {4}' -f $linkDN, $oneGpmLinkExisting.Enabled, $oneGpmLinkExisting.Enforced, $oneGpmLinkExisting.GPOID, $newGPO.ID)

        if ($newGPO.ID -eq $oneGpmLinkExisting.GPOID) {

          DBG ('GPO link already exists. Assert some properties')
          DBGIF $MyInvocation.MyCommand.Name { $oneGpmLinkExisting.Enabled -ne (-not $disabled) }
          DBGIF $MyInvocation.MyCommand.Name { $oneGpmLinkExisting.Enforced -ne $enforce }
          DBGIF $MyInvocation.MyCommand.Name { Is-NonNull $gpmLink }
          $gpmLink = $oneGpmLinkExisting
        }
      }

      if (Is-Null $gpmLink) {

        DBG ('The link does not exist yet, go attach the GPO now')
        DBGSTART
        $gpmLink = $null
        $gpmLink = $gpmSOM.CreateGPOLink(-1, $newGPO)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBGIF $MyInvocation.MyCommand.Name { Is-Null $gpmLink }
      }

      if (Is-NonNull $gpmLink) {

        DBG ('Set some basic parameters on the link')
        DBGSTART
        $gpmLink.Enforced = $enforce
        $gpmLink.Enabled = -not $disabled
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }
    }


    if ((Is-ValidString $wmiFilter) -and $shouldProcess) {

      DBG ('Going to apply WMI filter: {0}' -f $wmiFilter)

      $gpmSearchCrit = $gpm.CreateSearchCriteria()
      
      DBG ("Searching for WMI filters")
      DBGSTART
      $foundWmiFilters = $gpmDomain.SearchWmiFilters($gpmSearchCrit)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBG ('Found WMI filters: {0}' -f (Get-CountSafe $foundWmiFilters))
      
      if ((Get-CountSafe $foundWmiFilters) -gt 0) {
      
        $foundWmiFilter = $foundWmiFilters | ? { $_.Name -eq $wmiFilter }
        DBGIF $MyInvocation.MyCommand.Name { Is-Null $foundWmiFilter }
            
        DBG ("Set the found WMI filter")
        DBGSTART
        $gpoRs = $null
        $gpoRs = $newGPO.SetWMIFilter($foundWmiFilter)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBG ("GPMC Result: {0}" -f $gpoRs)
      }
    }


    if ((Is-ValidString $dacl) -and $shouldProcess) {

      DBG ('Going to apply NTFS/LDAP security filter: {0}' -f $dacl)

      # Note: there is an option to do it with the GPM object itself
      #       using the $gpm.CreatePermission() and $newGPO.SetSecurityInfo()
      #       but I resolved it with manual LDAP/NTFS permissions configuration
      #       and do not want to spend additional time debugging the GPM method

      <#

        dsacls "CN={96223E6B-F80D-4E38-A109-A1FCCABFDF02},CN=Policies,CN=System,DC=gopas,DC=virtual" /R "NT AUTHORITY\Authenticated Users"

        # Note: Read property (RP), Read permissions (RC), List (LC)
        #       the GUI does not grant Generic read (GR) which contains "List object" in addition
        #       NTFS permissions: gPCFileSysPath
        #                         Read and Execute on "This folder, subfolders and files"

        dsacls "CN={96223E6B-F80D-4E38-A109-A1FCCABFDF02},CN=Policies,CN=System,DC=gopas,DC=virtual" /I:T /G "kamil:RCLCRP"
        dsacls "CN={96223E6B-F80D-4E38-A109-A1FCCABFDF02},CN=Policies,CN=System,DC=gopas,DC=virtual" /I:T /G "kamil:CA;Apply Group Policy"

        Apply-NtfsDacl '\\gopas.virtual\sysvol\gopas.virtual\Policies\{96223E6B-F80D-4E38-A109-A1FCCABFDF02}' 'X$kamil@gopas.virtual' -addOnly $true
        Apply-NtfsDacl '\\gopas.virtual\sysvol\gopas.virtual\Policies\{96223E6B-F80D-4E38-A109-A1FCCABFDF02}' '-$NT AUTHORITY\Authenticated Users' -addOnly $true

      #>

      # Note: eventually it does not seem to be such a big task to do it directly with the GPM object
      #       so I do it this way finally instead :-)

      DBG ('Obtain current GPO permissions')
      DBGSTART
      $gpoPermissions = $null
      $gpoPermissions = $newGPO.GetSecurityInfo()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGIF $MyInvocation.MyCommand.Name { Is-Null $gpoPermissions }
      DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $gpoPermissions) -lt 1 }

      [hashtable] $gpoTrusteeTypes = @{

          # Note: this enum comes from the SID_NAME_USE structure

          1 = 'User'
          2 = 'Group'
          5 = 'WellKnownGroup'
          9 = 'Computer'

          # 3 = 'Domain'
          # 4 = 'Alias'
          # 6 = 'DeletedAccount'
          # 7 = 'Invalid'
          # 8 = 'Unknown'
          # 10 = 'Label'
        }

      if (Is-NonNull $gpoPermissions) {

        #DBG ('Are GPO permissions consistent: {0}' -f $newGPO.IsACLConsistent())
        DBG ('List the current GPO permissions: {0}' -f (Get-CountSafe $gpoPermissions))
        foreach ($oneGpoPermission in $gpoPermissions) {

          # Note: we do not translate the number constants ($gpmConst.permXXX), as it is not too easy 
          #       and problably has little use here
          DBG ('One current permission entry: trustee = {0}\{1} ({2} = {3}) | perm = {4} | denied = {5} | inherited = {6} | inheritable = {7} | trusteeType = {8}' -f $oneGpoPermission.Trustee.TrusteeDomain, $oneGpoPermission.Trustee.TrusteeName, $oneGpoPermission.Trustee.TrusteeSid, $oneGpoPermission.Trustee.TrusteeDSPath, $oneGpoPermission.Permission, $oneGpoPermission.Denied, $oneGpoPermission.Inherited, $oneGpoPermission.Inheritable, $gpoTrusteeTypes[$oneGpoPermission.Trustee.TrusteeType])
          DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneGpoPermission.Trustee.TrusteeDomain }
          DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneGpoPermission.Trustee.TrusteeName }
          DBGIF $MyInvocation.MyCommand.Name { ($oneGpoPermission.Trustee.TrusteeType -ne 5) -and (Is-EmptyString $oneGpoPermission.Trustee.TrusteeDSPath) }
          DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $oneGpoPermission.Trustee.TrusteeDSPath) -and ($oneGpoPermission.Trustee.TrusteeDSPath -notmatch (Get-NormalDNMatch $true)) }
          DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneGpoPermission.Trustee.TrusteeSid }
          DBGIF $MyInvocation.MyCommand.Name { $oneGpoPermission.Trustee.TrusteeSid -notmatch $global:rxSID }
          DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $gpoTrusteeTypes[$oneGpoPermission.Trustee.TrusteeType] }
        }

        DBG ('Set the requested principals on the GPO: {0}' -f (Count-MultiValue $dacl))
        DBGIF $MyInvocation.MyCommand.Name { (Count-MultiValue $dacl) -lt 1 }

        # Note: we must parse this twice as the Deny ACE is configured directly with
        #       LDAP/NTFS permissions and the SetSecurityInfo() method would reset that
        #       if called as the last thing
        DBG ('Do the IGPM permission configuration part for ALLOW ACEs')

        foreach ($oneACE in (Split-MultiValue $dacl)) {

          $oneAcePrincipal = Strip-ValueFlags $oneACE
          DBG ('Apply one ACE to the GPO: {0} | {1}' -f $oneACE, $oneAcePrincipal)

          $oneAcePermission = $null
          
          if (Has-ValueFlags $oneACE R) {

            DBG ('Remove the trustee: {0}' -f $oneAcePrincipal)
            DBGSTART
            $gpoPermissions.RemoveTrustee($oneAcePrincipal)
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND

          } elseif (-not (Has-ValueFlags $oneACE D)) {

            DBG ('Add the principal to the GPO: {0}' -f $oneAcePrincipal)
            DBGSTART
            # Note: the permission should be Inheritable probably as are all the other permissions
            #       https://msdn.microsoft.com/en-us/library/aa814303(v=vs.85).aspx
            #       IGPM::CreatePermission method
            $oneAcePermission = $gpm.CreatePermission($oneAcePrincipal, $gpmConst.permGPOApply, $true)
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
            DBGIF $MyInvocation.MyCommand.Name { Is-Null $oneAcePermission }

            if (Is-NonNull $oneAcePermission) {

              DBG ('Add the newly defined permissions to the list of GPO permissions')
              DBGSTART
              $gpoPermissions.Add($oneAcePermission)
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND
            }
          }
        }

        DBG ('Set the modified permissions back on the GPO')
        DBGSTART
        $newGPO.SetSecurityInfo($gpoPermissions)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

        
        #
        #
        #

        DBG ('Do the LDAP/NTFS permission configuraiton part for any DENY permissions')

        foreach ($oneACE in (Split-MultiValue $dacl)) {

          $oneAcePrincipal = Strip-ValueFlags $oneACE

          if (Has-ValueFlags $oneACE D) {

            DBG ('Deny the GPO apply permission to the principal: {0} | {1}' -f $oneAcePrincipal, $newGPO.Path)
            DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $newGPO.Path }
            DBGIF $MyInvocation.MyCommand.Name { $newGPO.Path -notmatch (Get-NormalDNMatch $true) }

            # Note: this cannot be done with IGPM interfaces, so we have to make do with normal LDAP and NTFS permissions

            Run-Process dsacls ('"{0}" /I:T /D "{1}:RCLCRP"' -f $newGPO.Path, $oneAcePrincipal)
            Run-Process dsacls ('"{0}" /I:T /D "{1}:CA;Apply Group Policy"' -f $newGPO.Path, $oneAcePrincipal)

            [Collections.ArrayList] $deList = @()
            $newGPODE = Get-DE $newGPO.Path ([ref] $deList)
            $gpoNtfsPath = GDES $newGPODE gPCFileSysPath
            Dispose-List ([ref] $deList)

            DBG ('GPO SYSVOL share path determined: {0}' -f $gpoNtfsPath)
            DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $gpoNtfsPath }
            DBGIF $MyInvocation.MyCommand.Name { $gpoNtfsPath -notlike "\\$domain\sysvol\$domain\policies\$($newGPO.ID)" }

            Apply-NtfsDacl $gpoNtfsPath ('DX${0}' -f $oneAcePrincipal) -addOnly $true
         
            DBGIF $MyInvocation.MyCommand.Name { -not $newGPO.IsACLConsistent() }
          }
        }
      }
    }
  }

  DBGIF ('Requested GPO was not processed: {0}' -f $gpo) { (Is-ValidString $gpo) -and (-not $explicitGPOprocessed) }
}


function global:Add-GroupMember ([string] $groupsWithFlags, [string] $domainDN, [string] $memberDNorSAM)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $groupsWithFlags }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domainDN }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $memberDNorSAM }
  
  [System.Collections.ArrayList] $deList = @()

  $groupList = Split-MultiValue $groupsWithFlags
  DBG ("Going to set group membership: {0}x = {1}" -f (Get-CountSafe $groupList), $groupsWithFlags)

  if (Get-CountSafe $groupList) {
    
    DBG ('Connect to domain: {0}' -f $domainDN)
    $domainDE = Get-DE $domainDN ([ref] $deList)
  
    DBG ('Obtain the member''s DE: {0}' -f $memberDNorSAM)
    $memberDE = Get-DEbyDNorSAMorUPN $memberDNorSAM '*' $domainDN ([ref] $deList)
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $memberDE }

    if (Is-NonNull $memberDE) {
    #  if ($memberDNorSAM -like 'CN=*,DC=*') {
    #  
    #    $memberDE = Get-DE $memberDNorSAM ([ref] $deList)
    #  }
    #  
    #  else {
    #
    #    DBG ("Searching for member: {0}" -f $memberDNorSAM)
    #    $memberDE = Find-DE $domainDE 'sAMAccountName' $memberDNorSAM '(objectClass=group)' ([ref] $deList)
    #  }

      foreach ($oneGroup in $groupList) {
  
        $primary = $false
    
        if (Has-ValueFlags $oneGroup P) {
    
          $primary = $true
        }

        DBG ("Searching for group: {0} (primary = {1})" -f (Strip-ValueFlags $oneGroup), $primary)
        $groupDE = Find-DE $domainDE 'sAMAccountName' (Strip-ValueFlags $oneGroup) '(objectClass=group)' ([ref] $deList)
    
        DBGIF ('Group not found: {0}' -f $oneGroup) { Is-Null $groupDE }
        if (Is-NonNull $groupDE) {

          if (-not (Is-GroupMember (GDES $groupDE distinguishedName) (GDES $memberDE distinguishedName))) {

            DBG ("Adding member to the group")
            DBGSTART
            $groupDE.Add($memberDE.Path)
            $adsiRs = $groupDE.SetInfo()
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
            DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))

            if ($primary) {

              $groupID = (GDES $groupDE 'primaryGroupToken')
              DBG ("Setting the group as primary: token = {0}" -f $groupID)
              DBGSTART
              $memberDE.Put('primaryGroupID', $groupID)
              $adsiRs = $memberDE.SetInfo()
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND
              DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
            }
          }
        }
      }
    }
  }  

  Dispose-List ([ref] $deList)
}

function global:Create-UserAccount ([string] $cn, [string] $sam, [string] $upn, [string] $pwd, [string] $ou, [string] $domainDN, [int] $uac, [string] $groups, [ref] $outObjDN, [int] $aes)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $cn }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $sam }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $upn }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $ou }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domainDN }
  
  [System.Collections.ArrayList] $deList = @()

  if ((Is-ValidString $cn) -and (Is-ValidString $ou)) {
  
    $domainDE = Get-DE $domainDN ([ref] $deList)
    $ouDE = Get-SubDE $ou $domainDE ([ref] $deList)
    
    if ($cn -notlike 'cn=*') { $cn = 'CN=' + $cn }

    DBG ("Checking if the user exists")
    $newUser = $null
    $newUser = Find-DE $domainDE 'sAMAccountName' $sam '(objectClass=user)' ([ref] $deList)

    if (Is-Null $newUser) {

      DBG ("Creating user account: {0}" -f $cn)
      DBGSTART
      $newUser = $ouDE.Create('user', $cn)
      $deList.Add($newUser) | Out-Null      
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBGSTART
      $newUser.Put('sAMAccountName', $sam)
      $newUser.Put('userPrincipalName', $upn)
      $adsiRs = $newUser.SetInfo()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))

      DBG ("Setting password")
      DBGSTART
      $newUser.SetPassword($pwd)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBG ("Setting userAccountControl: {0}" -f (Get-FlagsString $global:uacFlags $uac))
      DBGSTART
      $newUser.Put('userAccountControl', $uac)
      $adsiRs = $newUser.SetInfo()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))

      if ($aes -gt 0) {

        DBG ('Configure AES/RC4 Kerberos etype support: {0}' -f $aes)
        DBGSTART
        $newUser.Put('msDs-SupportedEncryptionTypes', $aes)
        $adsiRs = $newUser.SetInfo()
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
      }
    }

    if (Is-NonNull $outObjDN) {

      # Note: GDES is safe
      $outObjDN.Value = GDES $newUser distinguishedName
    }
    
    if ((Is-ValidString $newUser.Path) -and (Is-ValidString $groups)) {
    
      Add-GroupMember $groups $domainDN (GDES $newUser distinguishedName)
    }
  }
  
  Dispose-List ([ref] $deList)
}


function global:Create-Group ([string] $cn, [string] $ou, [string] $domainDN, [int] $type = 0x80000002, [string] $groups)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $cn }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $ou }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domainDN }
  DBGIF $MyInvocation.MyCommand.Name { $type -eq 0 }
  
  [System.Collections.ArrayList] $deList = @()

  if (Is-ValidString $cn) {
  
    $domainDE = Get-DE $domainDN ([ref] $deList)
    $ouDE = Get-SubDE $ou $domainDE ([ref] $deList)
    
    if ($cn -notlike 'cn=*') { $cn = 'CN=' + $cn }

    DBG ("Checking if the group exists")
    $createdGroup = Find-DE $domainDE 'sAMAccountName' $cn.SubString(3) '(objectClass=group)' ([ref] $deList)

    if (Is-Null $createdGroup) {

      DBG ("Creating group: {0}, type = {1}" -f $cn, (Get-FlagsString $global:groupFlags $type))
      DBGSTART
      $createdGroup = $ouDE.Create('group', $cn) 
      $deList.Add($createdGroup) | Out-Null      
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBGSTART
      $createdGroup.Put('sAMAccountName', $cn.SubString(3))
      $createdGroup.Put('groupType', $type)
      $adsiRs = $createdGroup.SetInfo()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
    }
    
    if (Is-ValidString $groups) {
    
      Add-GroupMember $groups $domainDN (GDES $createdGroup distinguishedName)
    }
  }
  
  Dispose-List ([ref] $deList)
}


function global:Create-Computer ([string] $cn, [string] $ou, [string] $domainDN, [string] $joinAccount)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $cn }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $ou }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domainDN }
  
  [System.Collections.ArrayList] $deList = @()

  if (Is-ValidString $cn) {
  
    $domainDE = Get-DE $domainDN ([ref] $deList)
    $ouDE = Get-SubDE $ou $domainDE ([ref] $deList)
    
    if ($cn -notlike 'cn=*') { $cn = 'CN=' + $cn }
    
    $compSAM = $cn.SubString(3) + '$'
    DBG ("Checking if the computer exists")
    $createdComp = Find-DE $domainDE 'sAMAccountName' $compSAM '(objectClass=computer)' ([ref] $deList)

    if (Is-Null $createdComp) {

      DBG ("Creating computer: {0}" -f $cn)
      DBGSTART
      $createdComp = $ouDE.Create('computer', $cn) 
      $deList.Add($createdComp) | Out-Null      
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBGSTART
      $createdComp.Put('sAMAccountName', $compSAM)
      $createdComp.Put('userAccountControl', 4128)
      $adsiRs = $createdComp.SetInfo()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
    }
    
    if (Is-ValidString $joinAccount) {

      $compDN = GDES $createdComp distinguishedName

      DBG ("Going to delegate join operation: {0} | {1}" -f $compDN, $joinAccount)
      Assert-AccountExists $joinAccount 'Join operation identity'

      Run-Process 'DSACLS' ('{0} /G "{1}:RPWP;description"' -f $compDN, $joinAccount)
      Run-Process 'DSACLS' ('{0} /G "{1}:RPWP;displayName"' -f $compDN, $joinAccount)
      Run-Process 'DSACLS' ('{0} /G "{1}:RPWP;sAMAccountName"' -f $compDN, $joinAccount)
      Run-Process 'DSACLS' ('{0} /G "{1}:RPWP;Logon Information"' -f $compDN, $joinAccount)
      Run-Process 'DSACLS' ('{0} /G "{1}:RPWP;Account Restrictions"' -f $compDN, $joinAccount)
      Run-Process 'DSACLS' ('{0} /G "{1}:WS;Validated Write to DNS Host Name"' -f $compDN, $joinAccount)
      Run-Process 'DSACLS' ('{0} /G "{1}:WS;Validated Write to Service Principal Name"' -f $compDN, $joinAccount)
      Run-Process 'DSACLS' ('{0} /G "{1}:GRSDDTCA"' -f $compDN, $joinAccount)
    }
  }
  
  Dispose-List ([ref] $deList)
}


function global:Create-Site ([string] $nameAndFlags, [string] $ip)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $nameAndFlags }
  
  [System.Collections.ArrayList] $deList = @()

  if (Is-ValidString $nameAndFlags) {
  
    $rootDSE = Get-DE 'RootDSE' ([ref] $deList)
    $configDN = GDES $rootDSE configurationNamingContext
    $sitesDE = Get-DE "CN=Sites,$configDN" ([ref] $deList)
    $subnetsDE = Get-DE "CN=Subnets,CN=Sites,$configDN" ([ref] $deList)

    $cn = Strip-ValueFlags $nameAndFlags
    if ($cn -notlike 'cn=*') { $cn = 'CN=' + $cn }
    $name = $cn.SubString(3)

    DBG ('Check if only rename Default-First-Site-Name: {0} | cn = {1} | name = {2}' -f $nameAndFlags, $cn, $name)
    if (Has-ValueFlags $nameAndFlags 'D') {
  
      Rename-Object "CN=Default-First-Site-Name,CN=Sites,$configDN" $cn
      $siteDE = Get-SubDE $cn $sitesDE ([ref] $deList)
    }
    
    else {
    
      DBG ('Non Default-First-Site-Name operation.')
      DBG ('Checking if the site exists')
      $siteDE = Find-DE $sitesDE 'cn' $name '(objectClass=site)' ([ref] $deList)

      if (Is-Null $siteDE) {

        DBG ("Creating site: {0}" -f $cn)
        DBGSTART
        $siteDE = $sitesDE.Create('site', $cn) 
        $deList.Add($siteDE) | Out-Null      
        $adsiRs = $siteDE.SetInfo()
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
        
        DBG ("Creating NTDS site settings: {0}" -f $cn)
        DBGSTART
        $siteSettingsDE = $siteDE.Create('nTDSSiteSettings', 'CN=NTDS Site Settings') 
        $deList.Add($siteSettingsDE) | Out-Null      
        $adsiRs = $siteSettingsDE.SetInfo()
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))

        DBG ("Creating servers container: {0}" -f $cn)
        DBGSTART
        $siteServersDE = $siteDE.Create('serversContainer', 'CN=Servers') 
        $deList.Add($siteServersDE) | Out-Null      
        $adsiRs = $siteServersDE.SetInfo()
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
      }
    }
        
    DBG ('Checking if subnet exists: {0}' -f $ip)
    $subnetDE = Find-DE $subnetsDE 'cn' $ip '(objectClass=subnet)' ([ref] $deList)

    if (Is-Null $subnetDE) {

      DBG ("Creating subnet: {0}" -f $ip)
      DBGSTART
      $subnetDE = $subnetsDE.Create('subnet', "CN=$ip") 
      $deList.Add($subnetDE) | Out-Null      
      $adsiRs = $subnetDE.SetInfo()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
    }
    
    DBG ("Bind subnet with site: subnet = {0} | site = {1}" -f (GDES $subnetDE distinguishedName), (GDES $siteDE distinguishedName))
    DBGSTART
    $subnetDE.Put('siteObject', (GDES $siteDE distinguishedName))
    $adsiRs = $subnetDE.SetInfo()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
  }
  
  Dispose-List ([ref] $deList)
}


function global:Create-SiteLink ([string] $nameAndFlags, [string] $siteList, [bool] $notify, [int] $cost, [int] $interval)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $nameAndFlags }
  
  [System.Collections.ArrayList] $deList = @()

  if (Is-ValidString $nameAndFlags) {
  
    $rootDSE = Get-DE 'RootDSE' ([ref] $deList)
    $configDN = GDES $rootDSE configurationNamingContext
    $linksDE = Get-DE "CN=IP,CN=Inter-Site Transports,CN=Sites,$configDN" ([ref] $deList)

    $cn = Strip-ValueFlags $nameAndFlags
    if ($cn -notlike 'cn=*') { $cn = 'CN=' + $cn }
    $name = $cn.SubString(3)

    DBG ('Checking if the siteLink exists')
    $linkDE = Find-DE $linksDE 'cn' $name '(objectClass=siteLink)' ([ref] $deList)

    if (Is-Null $linkDE) {

      DBG ('SiteLink does not exist. Will create.')

      # Note: must be in braces, the pipe operator has preference
      $sites = (Split-MultiValue $siteList) | % {
      
        $siteDN = 'CN={0},CN=Sites,{1}' -f $_, $configDN
        DBG ('Will connect site: {0}' -f $siteDN)
        $siteDN
      }

      DBG ("Creating siteLink: {0}" -f $cn)
      DBGSTART
      $linkDE = $linksDE.Create('siteLink', $cn) 
      $deList.Add($linkDE) | Out-Null      
      # Note: siteList must be filled during initial object creation
      #       PutEx must receive [object[]] instead of [string[]]
      $linkDE.PutEx($global:PutExUpdate, 'siteList', @($sites))
      $adsiRs = $linkDE.SetInfo()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ('ADSI Result: {0}' -f ($adsiRs | Out-String))
    }
    
    if (Is-NonNull $linkDE) {
    
      DBG ('Use NOTIFY for the siteLink: {0}' -f $notify)
      
      if ($notify) {
      
        DBGSTART
        $origOptions = [int] (GDES $linkDE options)
        $linkDE.Put('options', $origOptions -bor 1)
        $adsiRs = $linkDE.SetInfo()
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
      }
      
      DBG ('Setting: cost = {0} | interval = {1}' -f $cost, $interval)
      DBGSTART
      $linkDE.Put('cost', $cost)
      $linkDE.Put('replInterval', $interval)
      $adsiRs = $linkDE.SetInfo()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
    }
  }
  
  Dispose-List ([ref] $deList)
}


function global:Run-Process ([string] $process, [string] $arguments, [bool] $doNotRedirOut, [string] $login, [string] $domain, [string] $password, [bool] $adjustWorkDir, [string] $workDir, [bool] $returnExitCode, [int[]] $ignoreExitCodes, [ref] $refStdOut, [switch] $disableWER, [switch] $showWindow, [switch] $doNotCheckExistance, [switch] $doNotWait, [switch] $newWindow)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBG ('Starting process: {0}' -f $process)
  DBG ('Arguments: {0}' -f $arguments)
  DBG ('Do not redir output: {0}' -f $doNotRedirOut)

  [string] $stdOut = ''
  [int] $startExceptionErrorCode = 0

  [bool] $processExists = $true

  if (-not $doNotCheckExistance) {

    DBGSTART
    $existingProcess = Get-Command -Name $process
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $existingProcess }

    $processExists = Is-NonNull $existingProcess
  }

  if ($processExists) {

    DBGIF $MyInvocation.MyCommand.Name { $showWindow -and (-not $doNotRedirOut) }

    DBG ('Prepare the process initialization structure')
    DBGSTART
    
    $redirOut = -not $doNotRedirOut

    $newProc = New-Object System.Diagnostics.Process
    $newProc.StartInfo.UseShellExecute = $newWindow
    $newProc.StartInfo.CreateNoWindow = -not $showWindow
    $newProc.StartInfo.RedirectStandardError = $redirOut
    $newProc.StartInfo.RedirectStandardOutput = $redirOut
    $newProc.StartInfo.RedirectStandardInput = $redirOut  # as noted on MSDN, if we want to have StdErr redirected, we must redirect StdIn as well
    $newProc.StartInfo.FileName = $process
    $newProc.StartInfo.Arguments = $arguments
    $newProc.StartInfo.LoadUserProfile = $true

    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    if ($adjustWorkDir) {

      if (Is-ValidString $workDir) {
      
        $startDir = $workDir
      
      } else {    

        $startDir = Split-Path -Path $process -Parent:$true
      }

      DBG ('Will start from: {0}' -f $startDir)
      DBGSTART
      $newProc.StartInfo.WorkingDirectory = $startDir
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }

    if (Is-ValidString $login) {

      <# Note: seems unnecessary
      
      if ((Is-EmptyString $domain) -and $login.Contains('\'))
      {
        $loginTokens = $login.Split('\')
        
        $domain = $loginTokens[0]
        $login = $loginTokens[1]
      }

      if ((Is-EmptyString $domain) -and $login.Contains('@'))
      {
        $loginTokens = $login.Split('@')
        
        $domain = $loginTokens[1]
        $login = $loginTokens[0]
      }#>

      if (($global:thisOSVersionNumber -lt 6) -and ($domain -eq '.')) {

        # Note: Windows XP and Windows 2003 do not understand the '.' domain when starting the process
        #       under alternate identity - "Unknown user name or bad password"
        $domain = Get-LocalDomain
      }

      Assert-AccountExists (Get-SAMLogin $login $domain) -stubborn

      DBG ('Starting process with another identity: {0} | {1}' -f $login, $domain)
      DBGSTART
      $newProc.StartInfo.UserName = $login
      $newProc.StartInfo.Domain = $domain
      $newProc.StartInfo.Password = ConvertTo-SecureString $password -AsPlainText -Force
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
    

    $werDisabled = $false
    [Collections.ArrayList] $werDisabledBackup = @()

      if ($disableWER) {

        DBG ('Disable Windows Error Reporting (werfault.exe) for the duration of the command')

        DBGSTART
        Set-RegistryValue -key 'HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting' -name Disabled -value 1 -type dword -withBackup ([ref] $werDisabledBackup)
        Set-RegistryValue -key 'HKLM:\SOFTWARE\Microsoft\Windows\Windows Error Reporting' -name DontShowUI -value 1 -type dword -withBackup ([ref] $werDisabledBackup)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        
        $werDisabled = $true
      }



      DBG ('Start the process')
      DBGSTART
      [bool] $startRs = $newProc.Start()
      DBGER ('Exception starting the process') $error
      DBGEND
      DBG ('Process started: {0}' -f $startRs)

      if ($startRs) {

        DBG ('Wait for the process to stop: {0}' -f (-not $doNotWait))
        DBGIF $MyInvocation.MyCommand.Name { $doNotWait -and $redirOut }
        DBGIF $MyInvocation.MyCommand.Name { $doNotWait -and $returnExitCode }

        if (-not $doNotWait) {
      
          if ($redirOut) {

            DBG ('Obtain the standard outputs')
            DBGSTART
            $stdOut = $newProc.StandardOutput.ReadToEnd()
            $stdErr = $newProc.StandardError.ReadToEnd()
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
          }
    
          DBGSTART
          $newProc.WaitForExit()
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND

          if ((Is-NonNull $newProc.StartTime) -and (Is-NonNull $newProc.ExitTime)) {

            DBG ('Runtime: {0} | {1} | {2:N1} sec' -f $newProc.StartTime.ToString('s'), $newProc.ExitTime.ToString('s'), ($newProc.ExitTime - $newProc.StartTime).TotalSeconds)
          }
    
          DBG ('Process exit code: {0} | 0x{1:X}' -f $newProc.ExitCode, $newProc.ExitCode)

          if ($redirOut) {
    
            if (Is-ValidString $stdErr) {

              DBG ("Process stdError:`r`n{0}" -f $stdErr)
            }

            if (Is-ValidString $stdOut) {

              DBG ("Process stdOutput:`r`n{0}" -f $stdOut)
            }
          }
        }
      }


      if ($werDisabled) {

        DBG ('We have previously disabled the Windows Error Reporting (werfault.exe) so enable it back again')
        Restore-RegistryChangeBackup $werDisabledBackup
      }
  }
  
  else {
  
    DBG ('Command does not exist. Will not start: {0}' -f $process)
  }


  if (-not $doNotWait) {

  if (Is-NonNull $refStdOut) {

    DBG ('Returning STDOUT to the caller')
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $refStdOut.Value }
    DBGIF $MyInvocation.MyCommand.Name { ($refStdOut.Value -isnot [string[]]) -and ($refStdOut.Value -isnot [string])}

    if (Is-ValidString $stdOut) {

      if ($refStdOut.Value -is [string[]]) {

        # Split() accepts [char[]] so this string actually gets converted to [char[]] and the order of its individual characters does not matter at all
        [string[]] $stdOutLines = $stdOut.Split("`n`r") | ? { Is-ValidString $_ }
        DBG ('Lines of STDOUT to be returned: {0}' -f (Get-CountSafe $stdOutLines))
        $refStdOut.Value = $stdOutLines
      
      } else {

        DBG ('Returning STDOUT as a single string: len = {0}' -f $stdOut.Length)
        $refStdOut.Value = $stdOut
      }
    
    } else {

      # be sure to return some non-null value always
      $refStdOut.Value = ''
    }
  }


  $finalExitCode = [int] $newProc.ExitCode
  
  if ($finalExitCode -eq 0) {

    $finalExitCode = $startExceptionErrorCode
  }


  if ($returnExitCode) {

    DBG ('Returning exit code to the caller: {0} | 0x{1:X}' -f $finalExitCode, $finalExitCode)
    return $finalExitCode
  
  } else {

    # 3010 = ERROR_SUCCESS_REBOOT_REQUIRED
    # 2    = ERROR_SUCCESS_REBOOT_REQUIRED

    [int[]] $exitCodesToIgnore = @($global:win32_ERROR_SUCCESS_REBOOT_REQUIRED, 2, 0)
    
    if ((Get-CountSafe $ignoreExitCodes) -gt 0) {

      $exitCodesToIgnore = $exitCodesToIgnore + $ignoreExitCodes
    }

    [string] $argumentsTrimmed = [string]::Empty
    if (Is-ValidString $arguments) {

      if ($arguments.Length -gt 15) {

        $argumentsTrimmed = '{0}...' -f $arguments.SubString(0, 15)

      } else {

        $argumentsTrimmed = $arguments
      }
    }

    DBGIF ('Exit code warning: {0} {1} | {2} = 0x{3:X}' -f $process, $argumentsTrimmed, $finalExitCode, $finalExitCode) { -not (Contains-Safe $exitCodesToIgnore $finalExitCode) }
  }
  
  } else {

    DBG ('Continue without waiting')
  }
}



function global:Replace-ArgumentsInFile ([string] $sourceFilePath, [string] $arguments, [string] $outFilePath, [string] $encoding, [bool] $removeEmptyLines)
# $removeEmptyLines - this is used by SQL server, when NETWORK SERVICE is specified as a user account
# the password entry must be completelly removed and cannot appear at all
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $sourceFilePath }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $arguments }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $outFilePath }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $encoding }

  $argList = Split-MultiValue $arguments
  
  DBG ("Loading source file: {0}" -f $sourceFilePath)
  DBGSTART
  $sourceFileCnt = $null
  $sourceFileCnt = Get-Content -Path $sourceFilePath
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $sourceFileCnt }

  if (Is-NonNull $sourceFileCnt) {

    DBG ('Replacing argument tokens: sourceLines = #{0} | argList = #{1}' -f (Get-CountSafe $sourceFileCnt), (Get-CountSafe $argList))
    [System.Collections.ArrayList] $outFileCnt = @()

    [bool] $changed = $false
    foreach ($oneLine in $sourceFileCnt) {
    
      foreach ($oneArg in $argList) {
      
        $separatorIdx = $oneArg.IndexOf('$')
        $oneArgName = $oneArg.SubString(0, $separatorIdx)
        $oneArgValue = $oneArg.SubString(($separatorIdx + 1))

        DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneArgName }
        #DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oneArgValue }

        if ($removeEmptyLines -and (Is-EmptyString $oneArgValue) -and (($oneLine -like ('*=${0}$' -f $oneArgName)) -or ($oneLine -like ('*= ${0}$' -f $oneArgName)) -or ($oneLine -like ('*="${0}$"' -f $oneArgName)) -or ($oneLine -like ('*= "${0}$"' -f $oneArgName)))) {
          
          DBG ('Removing line because of the $removeEmptyLines requested: {0}' -f $oneLine)
          $oneLine = ''
          $changed = $true
        
        } elseif ($oneLine -clike ('*${0}$*' -f $oneArgName)) {
        
          DBG ('Replacing line with token: {0} | {1} | {2}' -f $oneLine, $oneArgName, $oneArgValue)
          $oneLine = $oneLine.Replace(('${0}$' -f $oneArgName), $oneArgValue)
          $changed = $true
        }
      }
      
      [void] $outFileCnt.Add($oneLine)
    }

    if ($changed) {

      DBG ('Saving output text file to: {0}' -f $outFilePath)
      DBGSTART
      Set-Content -Path $outFilePath -Value $outFileCnt -Encoding $encoding -Force
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
  }
}


function global:Set-RegistrySD ([string] $key, [string] $value, [string] $newSDDL)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $key }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $value }

  DBG ('Read the binary SD from registry: {0} | {1}' -f $key, $value)
  $originalBinSD = (Get-ItemProperty $key -EV er -EA SilentlyContinue).$value
  DBGER $MyInvocation.MyCommand.Name $er


  if ($global:thisOSVersionNumber -ge 6.0) {

    DBG ('Will work with SDDL using the WMI converter')

    DBGSTART
    $converter = [wmiclass] 'Win32_SecurityDescriptorHelper'
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBGSTART
    $originalSDDL = $converter.BinarySDtoSDDL($originalBinSD).SDDL
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

  } else {

    # Note: we are going to use NTFS file security class and hope, it can accept any SDDL
    #       regardless its NTFS (non)meaning
    DBG ('Will work with SDDL using the C# FileSecurity converter')

    DBGSTART
    $converter = New-Object System.Security.AccessControl.FileSecurity
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBGSTART
    $converter.SetSecurityDescriptorBinaryForm($originalBinSD)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBGSTART
    $originalSDDL = $converter.GetSecurityDescriptorSddlForm('All')
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }

  DBG ("Original SDDL: {0}" -f $originalSDDL)
  

  if ($newSDDL.Contains('\')) {

    DBG ("Going to replace user principal references with SIDs")

    $regex = [regex] ';([-.a-zA-Z0-9]+\\[-. a-zA-Z0-9]+)\)'
    $principals = $regex.Matches($newSDDL)
    
    if ($principals.Count -gt 0) {

      foreach ($princ in $principals) {

        $princName = $princ.Groups[1].Captures[0].Value
        
        DBG ("Translating: {0}" -f $princ.Groups[1].Captures[0].Value)

        DBGSTART
        $princSID = $null          
        $princSID = (New-Object Security.Principal.NTAccount $princName).Translate([Security.Principal.SecurityIdentifier]).Value
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        
        if (Is-EmptyString $princSID) {

          DBG ("Error translating security principal to SID.")

        } else {
        
          $newSDDL = $newSDDL.Replace($princName, $princSID)             
          DBG ("Found SID: {0}" -f $princSID)
        }
      }
    
    } else {

      DBG ("The NewSDDL does not require translation.")
    } 
  }

  DBG ("Final translated SDDL to be applied: {0}" -f $newSDDL)


  DBG ("Converting the new SDDL to binary form.")
  [byte[]] $newBinSD = $null
  
  if ($global:thisOSVersionNumber -ge 6.0) {

    DBG ('Will use WMI interface on Win 6x')

    DBGSTART
    $wmiRs = $null
    $wmiRs = $converter.SDDLtoBinarySD($newSDDL)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGWMI $wmiRs

    if ($wmiRs.ReturnValue -eq 0) {
 
      $newBinSD = $wmiRs.BinarySD
    }
  
  } else {

    DBG ('Will use the C# interface on 5.x')

    DBGSTART
    $converter.SetSecurityDescriptorSddlForm($newSDDL)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBGSTART
    $newBinSD = $converter.GetSecurityDescriptorBinaryForm()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }


  if ((Get-CountSafe $newBinSD) -gt 0) {

    DBG ("Applying the new SD.")
       
    Set-ItemProperty -Path $key -Name $value -Value $newBinSD -Force -EV er -EA SilentlyContinue | Out-Null
    DBGER $MyInvocation.MyCommand.Name $er
  }
  
  else {
  
    DBG ("Error: the format of the new SDDL is invalid.")
  }
}


function global:Reset-UserPassword ([string] $domain, [string] $user, [string] $password, [bool] $nonExpirePassword)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBG ('Resetting user password for: {0}\{1}' -f $domain, $user)
  DBGSTART
  # Note: you must use forward slashes, not backslashes!
  $oneUsr = $null
  $oneUsr = [ADSI] "WinNT://$domain/$user"
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $oneUsr }

  if (Is-NonNull $oneUsr) {

    DBG ('Set the password')    
    DBGSTART
    $oneUsr.SetPassword($password)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    
    if ($nonExpirePassword) {
    
      DBG ('Setting password to non-expire')

      DBGSTART
      $usrFlags = $oneUsr.UserFlags.Value
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBG ('Current user flags: {0} = 0x{1:X8}' -f $usrFlags, $usrFlags)

      DBGSTART
      $oneUsr.UserFlags = $usrFlags -bor 0x10000
      $oneUsr.SetInfo()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
  }
}


function global:Enable-LocalUser ([string] $name, [bool] $assertIfNotDisabled)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $name }

  if (Is-ValidString $name) {

    $adsiObjPath = 'WinNT://./{0},user' -f $name
    DBG ('Open local account with ADSI: {0}' -f $adsiObjPath)

    DBGSTART
    $adsiObj = $null
    $adsiObj = [ADSI] $adsiObjPath
    DBG ('Local account ADSI path opened: {0}' -f $adsiObj.Path)
    DBGER $MyInvocation.MyCommand.Name $error

    DBG ('Enable local account.')

    DBGSTART
    $originalUAC = [int] $adsiObj.UserFlags.Value
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Current UAC value: {0}' -f $originalUAC)
    DBGIF $MyInvocation.MyCommand.Name { $assertIfNotDisabled -and (($originalUAC -band 2) -ne 2) }

    $newUAC = [int] ($originalUAC -band (-bnot ([int] 2)))
    DBG ('New UAC value: {0}' -f $newUAC)

    DBGSTART
    $adsiObj.UserFlags.Value = $newUAC
    $adsiRs = $adsiObj.SetInfo()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
  }
}


function global:Is-LocalUserEnabled ([string] $name)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $name }

  [bool] $isEnabled = $false

  if (Is-ValidString $name) {

    $adsiObjPath = 'WinNT://./{0},user' -f $name
    DBG ('Open local account with ADSI: {0}' -f $adsiObjPath)

    DBGSTART
    $adsiObj = $null
    $adsiObj = [ADSI] $adsiObjPath
    DBG ('Local account ADSI path opened: {0}' -f $adsiObj.Path)
    DBGER $MyInvocation.MyCommand.Name $error

    DBGSTART
    $originalUAC = [int] $adsiObj.UserFlags.Value
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Current UAC value: {0}' -f $originalUAC)

    $isEnabled = -not ($originalUAC -band 2)
    DBG ('Current state is enabled: {0}' -f $isEnabled)
  }

  return $isEnabled
}


function global:Disable-LocalUser ([string] $name)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $name }

  if (Is-ValidString $name) {

    $adsiObjPath = 'WinNT://./{0},user' -f $name
    DBG ('Open local account with ADSI: {0}' -f $adsiObjPath)

    DBGSTART
    $adsiObj = $null
    $adsiObj = [ADSI] $adsiObjPath
    DBG ('Local account ADSI path opened: {0}' -f $adsiObj.Path)
    DBGER $MyInvocation.MyCommand.Name $error

    DBG ('Disable local account.')

    DBGSTART
    $originalUAC = [int] $adsiObj.UserFlags.Value
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Current UAC value: {0}' -f $originalUAC)
    DBG ('New UAC value: {0}' -f ($originalUAC -bor 2))

    DBGSTART
    $adsiObj.UserFlags.Value = $originalUAC -bor 2
    $adsiRs = $adsiObj.SetInfo()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
  }
}


function global:Rename-LocalObj ([string] $type, [string] $name, [string] $newName)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $type }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $name }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $newName }

  if ((Is-ValidString $name) -and (Is-ValidString $type) -and (Is-ValidString $newName)) {

    $adsiObjPath = 'WinNT://./{0},{1}' -f $name, $type
    DBG ('Open local object with ADSI: {0}' -f $adsiObjPath)

    DBGSTART
    $adsiObj = $null
    $adsiObj = [ADSI] $adsiObjPath
    DBG ('Local object ADSI path opened: {0}' -f $adsiObj.Path)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Rename local object: from = {0} | to = {1}' -f $name, $newName)

    DBGSTART
    $adsiRs = $adsiObj.Rename($newName)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
  }
}


function global:Change-LocalUserPassword ([string] $name, [string] $oldPwd, [string] $newPwd, [bool] $resetIfComplexityOrCurrentFails)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $name }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $oldPwd }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $newPwd }

  if ((Is-ValidString $name) -and (Is-ValidString $oldPwd) -and (Is-ValidString $newPwd)) {

    $adsiObjPath = 'WinNT://./{0},user' -f $name
    DBG ('Open local user with ADSI: {0}' -f $adsiObjPath)

    DBGSTART
    $adsiObj = $null
    $adsiObj = [ADSI] $adsiObjPath
    DBG ('Local user ADSI path opened: {0}' -f $adsiObj.Path)
    DBGER $MyInvocation.MyCommand.Name $error

    DBG ('Change local user password.')

    DBGSTART
    $adsiRs = $adsiObj.ChangePassword($oldPwd, $newPwd)
    #DBGER $MyInvocation.MyCommand.Name $error

    if ($error.Count -gt 0) {

      [int] $errorValue = $error[0].Exception.InnerException.ErrorCode
      
      # Note: error 0x800708C5 = -2147022651 = NERR_PasswordTooShort = "The password does not meet the password policy requirements. Check the minimum password length, password complexity and password history requirements"
      #       we still can try ResetPassword() as the problem might be PasswordHistory or the MinimumPasswordAge constraints
      # Note: error 0x80070056 = -2147024810 = ERROR_INVALID_PASSWORD = "The specified network password is not correct"
      #       This error appears for some reason as well when we try changing when ($newPwd -eq $oldPwd)
      if ($resetIfComplexityOrCurrentFails -and (($errorValue -eq 0x800708C5) -or ($errorValue -eq 0x80070056))) {
      
        DBGEND; DBGSTART
        DBGIF ('Change password failed with policy requirements or existing password error, try resetting the password: {0} | error = 0x{1:X8}' -f $name, $errorValue) { $newPwd -ne $oldPwd }
        $adsiRs = $adsiObj.SetPassword($newPwd)
        DBGER $MyInvocation.MyCommand.Name $error

      } else {

        DBGER $MyInvocation.MyCommand.Name $error
      }
    }

    DBGEND
    DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
  }
}


function global:Add-MemberLocalGroup ([string] $localGroup, [string] $memberLogin, [string] $memberDomain, [string] $localObjType, [bool] $removeInstead)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $localGroup }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $memberLogin }
  DBGIF $MyInvocation.MyCommand.Name { ($memberLogin -notlike '?*@?*') -and (Is-EmptyString $memberDomain) }

  [System.Collections.ArrayList] $deList = @()

  DBG ("Going to obtain local group: {0}" -f $localGroup)
  DBGSTART
  $localGroupDE = $null
  $localGroupDE = [ADSI] ('WinNT://./{0},Group' -f $localGroup)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  if (Is-NonNull $localGroupDE) {

    $deList.Add($localGroupDE) | Out-Null

    if ((Is-EmptyString $memberDomain) -and ($memberLogin -like '?*@?*')) {

      $memberDomain = ($memberLogin -split '@')[1]
      $memberLogin = ($memberLogin -split '@')[0]
      DBG ('Member login specified as UPN, domain name extracted as: {0} | {1}' -f $memberLogin, $memberDomain)
    }

    DBGIF ('Either login or domain not specified: login = {0} | domain = {1}' -f $memberLogin, $memberDomain) { (Is-EmptyString $memberLogin) -or (Is-EmptyString $memberDomain) }

    if ((Is-ValidString $memberLogin) -and (Is-ValidString $memberDomain)) {

      $memberADSIPath = 'WinNT://{0}/{1}' -f $memberDomain, $memberLogin

      if (Is-ValidString $localObjType) {
       
        $memberADSIPath = '{0},{1}' -f $memberADSIPath, $localObjType
      }

      if (-not $removeInstead) {

        DBG ("Adding: {0} @ {1} | {2}" -f $memberLogin, $memberDomain, $memberADSIPath)
        DBGSTART
        $adsiRs = $null
        $adsiRs = $localGroupDE.Add($memberADSIPath)
      
        if ($error[0].Exception.InnerException.HResult -ne -2147023518) {
      
          DBGER $MyInvocation.MyCommand.Name $error
      
        } else {

          DBG ('The requested principal is already member of the group. Ok.')
        }

        DBGEND
        DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
      
      } else {

        DBG ('Removing: {0} @ {1} | {2}' -f $memberLogin, $memberDomain, $memberADSIPath)
        DBGSTART
        $adsiRs = $null
        $adsiRs = $localGroupDE.Remove($memberADSIPath)
        DBGEND
        DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
      }
    }
  }

  Dispose-List ([ref] $deList)
}


function global:Create-LocalObj ([string] $type, [string] $name, [string] $pwd, [int] $userFlags, [string] $groups)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $type }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $name }
  DBGIF $MyInvocation.MyCommand.Name { ($type -eq 'user') -and (Is-EmptyString $pwd) }

  if ((Is-ValidString $name) -and (Is-ValidString $type)) {
  
    DBG ("Going to obtain local SAM handle.")
    DBGSTART
    $localSAM = $null
    $localSAM = [ADSI] 'WinNT://.'
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    
    if (Is-NonNull $localSAM) {

      DBG ("Going to create or open the new object: {0} = {1}" -f $type, $name)
      
      DBGSTART
      $newObj = $null
      $newObj = [ADSI] ('WinNT://./{0},{1}' -f $name, $type)
      DBGEND

      if (Is-EmptyString $newObj.Path) {

        DBG ("The object does not exist, going to create it.")
        DBGSTART
        $newObj = $null
        $newObj = $localSAM.Create($type, $name)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }
      
      if (Is-ValidString $newObj.Path) {

        if ($type -eq 'user') {
        
          if (Is-ValidString $pwd) {

            DBG ("Setting password.")
            DBGSTART
            $adsiRs = $null
            $adsiRs = $newObj.SetPassword($pwd)
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
            DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
          }

          DBG ('Setting user flags: 0x{0:X}' -f $userFlags)
          DBGSTART
          $adsiRs = $null
          $adsiRs = $newObj.Put('userFlags', $userFlags)
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
          DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
        }

        DBG ("Saving the newly created object.")
        DBGSTART
        $adsiRs = $null
        $adsiRs = $newObj.SetInfo()
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))

        if (Is-ValidString $groups) {

          $groupList = Split-MultiValue $groups
          DBG ("Adding the new object to groups: {0}x" -f $groupList.Count)
          
          $groupList | % { 

            $oneGroup = $_
            
            Add-MemberLocalGroup $oneGroup $name $global:thisComputerNetBIOS $type
          }
        }
      }
    }
  }
}


function global:Set-RegPermissions ([string] $key, [string] $user, [string] $permissions, [string] $inheritance, [bool] $returnBackup, [object] $newACL)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $key }
  DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $user) -and (Is-Null $newACL) }
  DBGIF $MyInvocation.MyCommand.Name { (Is-NonNull $newACL) -and ((Is-ValidString $permissions) -or (Is-ValidString $inheritance) -or (Is-ValidString $user)) }

  if ((Is-EmptyString $permissions) -and (Is-Null $newACL)) {
  
    $permissions = 'FullControl'
  }
  
  if (Is-ValidString $permissions) { 
   
    $regPermissions = [System.Security.AccessControl.RegistryRights] ((Split-MultiValue $permissions) -join ',')
  }

  if ((Is-EmptyString $inheritance) -and (Is-Null $newACL)) {
  
    $inheritance = 'ContainerInherit|ObjectInherit'
  }

  if (Is-ValidString $inheritance) {

    $inherit = [System.Security.AccessControl.InheritanceFlags] ((Split-MultiValue $inheritance) -join ',')
  }
  

  DBGSTART
  $keyObj = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($key, [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree, [System.Security.AccessControl.RegistryRights]::ChangePermissions)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  $aclBackup = $null

  if (Is-NonNull $keyObj) {

    DBG ('Get ACL for backup')
    DBGSTART
    $aclBackup = $keyObj.GetAccessControl()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    if (Is-Null $newACL) {

      DBG ('Get ACL again to work on with')
      DBGSTART
      $acl = $keyObj.GetAccessControl()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBG ('Define new ACL based on input parameters')
      DBGSTART
      $rule = New-Object System.Security.AccessControl.RegistryAccessRule ($user, $regPermissions, $inherit, 'None', "Allow")
      $acl.SetAccessRule($rule)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBG ('Apply the new ACL to the registry key')
      DBGSTART
      $keyObj.SetAccessControl($acl)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

    } else {

      # Note: there is a bug in SetAccessControl(), at least for the RegistrySecurity as of what I have observed on NetFx 3 and 4.5
      #       which does not apply the ACL if the original ACL haven't changed since it has been obtained from a key
      #       If you apply unmodified ACL, it does not get applied. So here I do just a nondestructive modification which
      #       probably sets some internal member.

      DBG ('Just hit the ACL in order to mark it modified internally and overcome the builtin bug')
      $newACL.SetAccessRuleProtection($newACL.AreAccessRulesProtected, $true)

      DBG ('Apply explicit new ACL to the registry key: {0}' -f ($newACL | fl * | Out-String))
      DBGSTART
      $keyObj.SetAccessControl($newACL)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }

  } else {

    DBG ('Key does not exist or other error occured. Skipping.')
  }


  if ($returnBackup) {

    DBG ('Return the original ACL')
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $aclBackup }

    return $aclBackup
  }
}


function global:Take-RegOwnership ( [string] $key )
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $key }

  Adjust-Privilege (Lookup-Privilege SeTakeOwnershipPrivilege) $true

  DBGSTART
  $keyObj = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($key, [Microsoft.Win32.RegistryKeyPermissionCheck]::ReadWriteSubTree, [System.Security.AccessControl.RegistryRights]::TakeOwnership)
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  if (Is-NonNull $keyObj) {

    DBG ('Do the ownership transfer')
    DBGSTART
    $acl = $keyObj.GetAccessControl()
    $acl.SetOwner([System.Security.Principal.NTAccount] ".\Administrators")
    $keyObj.SetAccessControl($acl)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

  } else {

    DBG ('Key does not exist or other error occured. Skipping.')
  }
}


function global:Mount-RegistryHive ([string] $hiveFile, [string] $relativeRegPath, [string] $testSubPath)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $hiveFile) }
  DBGIF $MyInvocation.MyCommand.Name { Test-Path "HKLM:\$relativeRegPath" }

  [bool] $mountRes = $false

  if (Test-Path $hiveFile) {

    $trialCount = 10
    do {

      DBG ('Mounting hive: {0} | {1}' -f $hiveFile, $relativeRegPath)
      Run-Process 'REG' ('LOAD "HKLM\{0}" "{1}"' -f $relativeRegPath, $hiveFile)
      
      $trialCount --
      DBG ('Sleeping 3sec to allow registry mount to complete. Remaining trial count: {0}' -f $trialCount)
      Start-Sleep 3
      
    } while ((-not (Test-Path "HKLM:\$relativeRegPath\$testSubPath")) -and ($trialCount -gt 0))

    DBGIF $MyInvocation.MyCommand.Name { $trialCount -le 0 }
    if ($trialCount -gt 0) { $mountRes = $true }
  }
  
  return $mountRes
}

function global:Dismount-RegistryHive ([string] $relativeRegPath, [string] $testSubPath)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path "HKLM:\$relativeRegPath") }

  if (Test-Path "HKLM:\$relativeRegPath") {

    $trialCount = 10
    do {
    
      $Error.Clear() ; [GC]::Collect() 
      DBG ('Unloading hive: {0}' -f $relativeRegPath)

      $Error.Clear() ; [GC]::Collect() 
      DBG ('Waiting 5sec. to allow GC to collect garbage')
      Start-Sleep 5

      $Error.Clear() ; [GC]::Collect() 
      Run-Process 'REG' ('UNLOAD "HKLM\{0}"' -f $relativeRegPath)
      
      $trialCount --
      DBG ('Waiting 3sec. to allow for full hive unload. Remaining trial count: {0}' -f $trialCount)
      Start-Sleep 3
    
      $testPath = "HKLM:\$relativeRegPath\$testSubPath"
      $testResult = Test-Path $testPath
      #DBG ('Tested path: {0} = {1}' -f $testPath, $testResult)
    
    } while ($testResult -and ($trialCount -gt 0))

    DBGIF $MyInvocation.MyCommand.Name { $trialCount -le 0 }
  }
}


function global:Rename-Object ([string] $pathDN, [string] $newCN)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $pathDN }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $newCN }

  [System.Collections.ArrayList] $deList = @()
  $objDE = Get-DE $pathDN ([ref] $deList)

  if (Is-NonNull $objDE) {

    if ($newCN -notlike 'cn=*') { $newCN = 'CN={0}' -f $newCN }
  
    DBGSTART
    $adsiRs = $objDE.psbase.Parent.MoveHere($objDE.Path, $newCN)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
  }
  
  Dispose-List ([ref] $deList)
}



function global:Define-StaticRoute ([string] $if, [string] $destination, [string] $mask, [string] $gw)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $if }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $destination }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $mask }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $gw }
  
  if ($thisOSVersion -like '5.*') {

    DBG ("Adding/Resetting static route by using NETSH in Win5.x")
      
    Run-Process 'NETSH' ('ROUTING IP ADD PERSISTENTROUTE Dest="{0}" Mask="{1}" NHop="{2}" Name="{3}"' -f $destination, $mask, $gw, $if)
    Run-Process 'NETSH' ('ROUTING IP SET PERSISTENTROUTE Dest="{0}" Mask="{1}" NHop="{2}" Name="{3}"' -f $destination, $mask, $gw, $if)
  }
    
  else {

    switch ([bool] $enableRouting) {
    
      $true { $stateKeyWord = 'ENABLED' }
      $false { $stateKeyWord = 'DISABLED' }
    }

    DBG ("Adding/Resetting static route by using NETSH in Win6.x")

    Run-Process 'NETSH' ('INTERFACE IPv4 ADD ROUTE Prefix="{0}/{1}" Interface="{2}" NextHop="{3}"' -f $destination, $mask, $if, $gw)
    Run-Process 'NETSH' ('INTERFACE IPv4 SET ROUTE Prefix="{0}/{1}" Interface="{2}" NextHop="{3}"' -f $destination, $mask, $if, $gw)
  }
}


function global:Define-NATRule ([string] $if, [string] $protocol, [string] $publicIP, [string] $publicPort, [string] $privateIP, [string] $privatePort)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $if }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $protocol }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $publicIP }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $publicPort }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $privateIP }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $privatePort }
  
  DBGIF $MyInvocation.MyCommand.Name { $thisOSVersionNumber -gt 6.3 }


  DBG ("Adding NAT rule by using NETSH")
      
  Run-Process 'NETSH' ('ROUTING IP NAT ADD PORTMAPPING Name="{0}" Proto="{1}" PublicIP="{2}" PublicPort="{3}" PrivateIP="{4}" PrivatePort="{5}"' -f $if, $protocol, $publicIP, $publicPort, $privateIP, $privatePort)
}


function global:Define-NATFilter ([string] $if, [string] $action, [string] $proto, [string] $dir, [string] $srcAddr, [string] $srcMask, [string] $dstAddr, [string] $dstMask)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $if }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $proto }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dir }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $srcAddr }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $srcMask }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dstAddr }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dstMask }
  
  DBGIF $MyInvocation.MyCommand.Name { $thisOSVersionNumber -gt 6.3 }

  DBG ("Adding NAT filter rule by using NETSH")
      
  Run-Process 'NETSH' ('ROUTING IP SET FILTER Name="{0}" FilterType="{1}" Action="{2}"' -f $if, $dir, $action)
  Run-Process 'NETSH' ('ROUTING IP ADD FILTER Name="{0}" FilterType="{1}" Proto="{2}" SrcAddr="{3}" SrcMask="{4}" DstAddr="{5}" DstMask="{6}"' -f $if, $dir, $proto, $srcAddr, $srcMask, $dstAddr, $dstMask)
}


function global:Set-DCOMLaunchPermissions ([string] $appID, [string] $userOrSid, [string] $domain)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $appID }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $userOrSid }

  $app = Get-WMIQuerySingleObject '.' ('SELECT * FROM Win32_DCOMApplicationSetting WHERE AppId = "{0}"' -f $appId) $true

  if (Is-NonNull $app) {

    DBG ('Get launch security descriptor')
    DBGSTART
    $wmiRs = $null
    $wmiRs = $app.GetLaunchSecurityDescriptor()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGWMI $wmiRs

    $sd = $wmiRs.Descriptor
    DBG ('Current launch descriptor: {0} | DACL = {1}x' -f $sd.ControlFlags, $sd.DACL.Count)

    DBG ('Create WMI trustee')
    $trustee = Spawn-WMIInstance '.' 'Win32_Trustee'

    if (Is-NonNull $trustee) {
    
      if ($domain -eq 'NT AUTHORITY') {

        DBGSTART
        $sid = [wmi] "\\.\root\cimv2:Win32_SID.SID='$userOrSid'"
        DBGEND
        DBGER $MyInvocation.MyCommand.Name $error
    
        DBG ('NT AUTHORITY user SID: {0} | {1} | {2}' -f $sid.ReferencedDomainName, $sid.AccountName, $sid.SID)
    
        $trustee.SID = $sid.BinaryRepresentation
        $trustee.SIDLength = $sid.SIDLength
        $trustee.SIDString = $userOrSid
        $trustee.Domain = $sid.ReferencedDomainName
        $trustee.Name = $sid.AccountName
      }
  
      else
      {
        $trustee.Domain = $domain
        $trustee.Name = $userOrSid
      }

      DBG ('Trustee: {0} | {1} | {2}' -f $trustee.Domain, $trustee.Name, $trustee.SIDString)

      $fullControl = 31
      $localLaunchActivate = 11

      DBG ('Create WMI ACE')
      $ace = Spawn-WMIInstance '.' 'Win32_ACE'

      DBGSTART
      $ace.AccessMask = $localLaunchActivate
      $ace.AceFlags = 0
      $ace.AceType = 0
      $ace.Trustee = $trustee
      [System.Management.ManagementBaseObject[]] $newDACL = $sd.DACL + @($ace)
      $sd.DACL = $newDACL
      DBGEND
      DBGER $MyInvocation.MyCommand.Name $error

      DBG ('Set the new launch security descriptor')
      DBGSTART
      $wmiRs = $null
      $wmiRs = $app.SetLaunchSecurityDescriptor($sd)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGWMI $wmiRs
    }

  } else {

    DBG ('The DCOM application does not exist or any other error occured. Skipping.')
  }
}


function global:Wait-SPWebApplication ([ref] $webAppRef)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $webAppRef.Value }

  if (Is-NonNull $webAppRef.Value) {

    $maxTrialCount = 17
    $sleepSec = 3
    $trialCount = $maxTrialCount
    do {

      $webApp = $webAppRef.Value
      DBGIF $MyInvocation.MyCommand.Name { $webApp.Url -notmatch $global:rxURL }

      DBG ('Waiting for the web application to become Online: {0} | {1}' -f $webApp, $webApp.Status)
      
      $trialCount --
      DBG ('Sleeping {0}sec to allow the operation to complete. Remaining trial count: {1} of {2}' -f $sleepSec, $trialCount, $maxTrialCount)
      Start-Sleep $sleepSec
     
      DBG ('Update the in-memory web application parameters')
      DBGSTART
      $webAppRef.Value = Get-SPWebApplication $webApp.Url
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

    } while (($webApp.Status -ne 'Online') -and ($trialCount -gt 0))

    DBG ('Wait finished after: {0}x = {1}sec' -f ($maxTrialCount - $trialCount), (($maxTrialCount - $trialCount) * $sleepSec))
    DBGIF $MyInvocation.MyCommand.Name { $trialCount -le 0 }
  }  
}


function global:Add-SPWebApplicationPolicy ([object] $webApp, [string] $login, [string] $displayName, [string] $policy, [bool] $doNotTranslateIdentity, [string] $loginType = 'user', [bool] $returnAccountId)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $webApp }
  DBGIF $MyInvocation.MyCommand.Name { $webApp.Url -notmatch $global:rxURL }

  [string] $accountToPolicy = [string]::Empty

  if (Is-NonNull $webApp) {

    DBG ('Is the supplied login already translated: {0}' -f $doNotTranslateIdentity)

    if (($webApp.UseClaimsAuthentication) -and (-not $doNotTranslateIdentity)) {

      switch ($loginType) {

        'user' { $identityType = 'WindowsSamAccountName' }

        'group' { 
        
          $identityType = 'WindowsSecurityGroupSid'

          DBG ('Translate group login to group SID: {0}' -f $login)
          DBGSTART
          $login = (New-Object Security.Principal.NTAccount $login).Translate([Security.Principal.SecurityIdentifier]).Value
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
          DBGIF $MyInvocation.MyCommand.Name { $login -notmatch $global:rxSID }
        }

        'formsUser' { $identityType = 'FormsUser' }

        'formsRole' { $identityType = 'FormsRole' }

        default { DBGIF ('Invalid loginType supplied: {0}' -f $loginType) { $true } }
      }

      DBG ('The web application requires claim principals: {0} | {1}' -f $webApp.Url, $login)
      DBGSTART
      $accountToPolicy = (New-SPClaimsPrincipal -IdentityType $identityType -Identity $login).ToEncodedString()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

    } else {

      DBG ('The web application uses the classic authentication provider: classic = {0} | url = {1} | {2}' -f $webApp.UseClaimsAuthentication, $webApp.Url, $login)
      $accountToPolicy = $login
    }

    if (Is-EmptyString $displayName) {

      $displayName = $accountToPolicy
    }
  
    DBG ('Create a new user policy on the application level: {0} | {1} | {2}' -f $webApp.Url, $accountToPolicy, $displayName)
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $accountToPolicy }
    DBGSTART
    $newWebAppPolicy = $null
    $newWebAppPolicy = $webApp.Policies.Add($accountToPolicy, $displayName)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $newWebAppPolicy }

    if (Is-NonNull $newWebAppPolicy) {

      DBG ('Get the special policy role: {0}' -f $policy)
      DBGSTART
      $newWebAppPolicyRole = $null
      $newWebAppPolicyRole = $webApp.PolicyRoles.GetSpecialRole([Microsoft.SharePoint.Administration.SPPolicyRoleType]::$policy)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGIF $MyInvocation.MyCommand.Name { Is-Null $newWebAppPolicyRole }

      if (Is-NonNull $newWebAppPolicyRole) {

        DBG ('Bind the special policy role to the login policy')
        DBGSTART
        $newWebAppPolicy.PolicyRoleBindings.Add($newWebAppPolicyRole)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

        DBG ('Update the web application')
        DBGSTART
        $webApp.Update()
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }
    }
  }

  if ($returnAccountId) {

    DBG ('Returning account ID: {0}' -f $accountToPolicy)
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $accountToPolicy }
    return $accountToPolicy
  }
}


function global:Assert-SPQuotaTemplate ([string] $name, [Int64] $storageMaximumLevel, [Int64] $storageWarningLevel, [double] $userCodeMaximumLevel, [double] $userCodeWarningLevel)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBG ('Get the ContentService object first')
  DBGSTART
  $cntService = [Microsoft.SharePoint.Administration.SPWebService]::ContentService
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  DBG ('Check if the quota exists: {0}' -f $name)
  DBGSTART
  $foundQuota = $null
  $foundQuota = $cntService.QuotaTemplates[$name]
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  if (Is-Null $foundQuota) {

    DBG ('The requested quota does not exist, create one')

    DBG ('Instantiate the quota template object')
    DBGSTART
    $quota = New-Object Microsoft.SharePoint.Administration.SPQuotaTemplate
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    $quota.Name = $name
    $quota.StorageMaximumLevel = $storageMaximumLevel
    $quota.StorageWarningLevel = $storageWarningLevel
    $quota.UserCodeMaximumLevel = $userCodeMaximumLevel
    $quota.UserCodeWarningLevel = $userCodeWarningLevel

    DBG ('Add the quota into the ContentService object')
    DBGSTART
    $cntService.QuotaTemplates.Add($quota)
    $cntService.Update()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

  } else {

    DBG ('The quota exists. Assert its properties: {0} | stgMax = {1} | stgWrn = {2} | userMax = {3} | userWrn = {4}' -f $name, $foundQuota.StorageMaximumLevel, $foundQuota.StorageWarningLevel, $foundQuota.UserCodeMaximumLevel, $foundQuota.UserCodeWarningLevel)
    DBGIF $MyInvocation.MyCommand.Name { $foundQuota.StorageMaximumLevel -ne $storageMaximumLevel }
    DBGIF $MyInvocation.MyCommand.Name { $foundQuota.StorageWarningLevel -ne $storageWarningLevel }
    DBGIF $MyInvocation.MyCommand.Name { $foundQuota.UserCodeMaximumLevel -ne $userCodeMaximumLevel }
    DBGIF $MyInvocation.MyCommand.Name { $foundQuota.UserCodeWarningLevel -ne $userCodeWarningLevel }
  }
}


function global:Update-SPServiceInstanceAccount ([string] $svcInstance, [string] $userName, [string] $assertServiceName, [string] $password)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $spVersion = Get-SPVersionDetails

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $svcInstance }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $userName }

  if ((Is-ValidString $svcInstance) -and (Is-ValidString $userName)) {
  
    DBG ('Get SPServiceInstance: {0}' -f $svcInstance)
    DBGSTART
    $spSvc = Get-SPServiceInstance | ? { $_.TypeName -eq $svcInstance }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('Found SPServiceInstances: # = {0}' -f (Get-CountSafe $spSvc))

    if (Get-CountSafe $spSvc) {
    
      $spSvc | % {
      
        # Note: SharePoint Server Search service has a script property that hides this real member
        #       and converts it to [string] for some uknown reason. So we just go for the actual ProcessIdentity
        #       member directly and ignore this incoherence
        $procIdentity = $spSvc.Service.psbase.ProcessIdentity
        DBG ('Processing SPServiceInstance: {0} | {1} | {2} | {3} | svcName = {4}' -f $spSvc.TypeName, $spSvc.Id, $procIdentity.UserName, $spSvc.Status, $spSvc.Service.Name)
        DBGIF ('The ServiceName for this ServiceInstance does not match asserted value: asserting = {0} | actual = {1}' -f $assertServiceName, $spSvc.Service.Name) { (Is-ValidString $assertServiceName) -and ($spSvc.Service.Name -ne $assertServiceName) }
        
        if (Is-EmptyString $procIdentity.UserName) {
        
          DBGIF ('There is currently no ProcessIdentity: {0} | {1} | {2} | {3}' -f $svcInstance, (Is-Null $procIdentity), ($procIdentity | Out-String), $procIdentity.GetType().Name) { $true }
          #$procIdentity = New-Object Microsoft.SharePoint.Administration.SPProcessIdentity
          #$spSvc.Service.ProcessIdentity = $procIdentity
        
        } elseif ($procIdentity.UserName -ne $userName) {
  
          if (($spVersion.Version -eq 2010) -or ($svcInstance -ne 'SharePoint Server Search')) {

            DBG ('Update service account: {0}' -f $userName)
            DBGSTART
            $procIdentity.CurrentIdentityType = 'SpecificUser'
            $procIdentity.UserName = $userName
            $procIdentity.Update()
            $procIdentity.Deploy()
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND

          } else {

              # Note: the process for updating the service accounts of Enterprise Search is the following as it appears
              #       we either use the Set-SPEnterpriseSearchService which changes the identity for both automatically
              #       or we use the normal ProcessIdentity approach for both the services individually
              #       If the standard ProcessIdentity approach is used, there are some additional configuration settings that need to be
              #       done (such as the HKLM\..\Services\Vss\VssAccessControl) and (SMSvcHost.exe.config section allowAccounts) thus
              #       this Set-SPEnteriseSearchService is better which does this itself automatically

              #Update-SPServiceInstanceAccount 'SharePoint Server Search' $samLogin -assertServiceName 'OSearch15' -pwd $ssSearchSvc.pwd
              #Update-SPServiceInstanceAccount 'Search Host Controller Service' $samLogin -assertServiceName 'SPSearchHostController'
            
              DBG ('Configure the Enterprise Search windows services: svc = {0}' -f $userName)
              DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $password }
              Set-SPEnterpriseSearchService -ServiceAccount $userName -ServicePassword (ConvertTo-SecureString $password -AsPlainText -Force)
              DBGSTART
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND

              # Note: this process identity change also requests the Net.TCP Port Sharing Service and Net.TCP Listener Adapter services to restart
              #       in order to let them reload the smsvchost.exe.config file where the WSS_WPG group has been added by the Set-SPEnterpriseSearchService
              #       which is the most easily done by restarting the whole WAS and the NetTcpPortSharing

              RestartUpdate-NetTcpPortSharing
          }
        }
      }
    }
  }
}


function global:RestartUpdate-NetTcpPortSharing ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBG ('Restart Net.Tcp related services to update their config')
  # Note: we must STOP all the services first because they all share the same process SMSvcHost
  #       and the configuration file is not read until the process is completelly stopped
  
  [WMI] $msmqActivator = $null
  $msmqActivator = Get-WmiQuerySingleObject '.' 'SELECT * FROM Win32_Service WHERE Name = "NetMsmqActivator" AND Started = "true"'

  DBGSTART
  [object[]] $smSvcHosts = 
  $smSvcHosts = Get-Process smsvchost
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBG ('SmSvcHost processes currently running: #{0} | {1}' -f $smSvcHosts.Length, (($smSvcHosts | Select -Expand Handle) -join ','))
  #DBGIF $MyInvocation.MyCommand.Name { (Is-Null $msmqActivator) -and ($smSvcHosts.Length -ne 1) }
  #DBGIF $MyInvocation.MyCommand.Name { (Is-NonNull $msmqActivator) -and ($smSvcHosts.Length -ne 2) }
  DBGIF $MyInvocation.MyCommand.Name { $smSvcHosts.Length -gt 2 }
    
  DBGSTART

  #Restart-Service WAS -Force
  Restart-Service VSS -Force

  if (Is-NonNull $msmqActivator) {
  
    Stop-Service NetMsmqActivator -Force
  }
  
  Stop-Service NetPipeActivator -Force
  Stop-Service NetTcpActivator -Force
  Stop-Service NetTcpPortSharing -Force

  DBG ('Give it some 5 seconds to stop properly')
  Start-Sleep -Sec 5

  # Note: it should be stopped by now so just be sure we didn't forget anything
  $smSvcHosts = $null
  $smSvcHosts = Get-Process | ? { $_.Name -eq 'smsvchost' }
  DBG ('SmSvcHosts still running: #{0} | {1}' -f $smSvcHosts.Length, (($smSvcHosts | Select -Expand Handle) -join ','))
  DBGIF $MyInvocation.MyCommand.Name { $smSvcHosts.Length -gt 0 }

  Start-Service NetTcpPortSharing
  Start-Service NetTcpActivator
  Start-Service NetPipeActivator

  if (Is-NonNull $msmqActivator) {

    Start-Service NetMsmqActivator
  }

  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
}


function global:Wait-SPServiceApplicationOnline ([string] $id)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $id }

  if (Is-ValidString $id) {

    Wait-Periodically -maxTrialCount 11 -sleepSec 3 -sleepMsg ('Waiting for the SP service application to become online: {0}' -f $id) -scriptBlockWhichReturnsTrueToStop {

      DBG ('Get the service application: {0}' -f $id)
      DBGSTART
      $svcApplicationToWait = $null
      $svcApplicationToWait = Get-SPServiceApplication $id
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ('Service application status: name = {0} | id = {1} | status = {2}' -f $svcApplicationToWait.Name, $svcApplicationToWait.Id, $svcApplicationToWait.Status)
    
      if ($svcApplicationToWait.Status -eq 'Online') {

        return $true

      } else {

        return $false
      }
    }
  }
}


function global:Update-SPServiceAppPoolAccount ([string] $svcApplication, [string] $userName)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $svcApplication }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $userName }

  if ((Is-ValidString $svcApplication) -and (Is-ValidString $userName)) {
  
    DBG ('Get SPServiceApplicationPool: {0}' -f $svcApplication)
    DBGSTART
    $spAppPool = Get-SPServiceApplicationPool -EA SilentlyContinue | ? { $_.Name -eq $svcApplication }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('Found SPServiceApplicationPool: # = {0}' -f (Get-CountSafe $spAppPool))
    
    if ((Get-CountSafe $spAppPool) -gt 0) {

      $spAppPool | % {
      
        DBG ('Processing SPServiceApplicationPool: {0} | {1} | {2} | {3}' -f $spAppPool.Name, $spAppPool.Id, $spAppPool.ProcessAccountName, $spAppPool.Status)
        
        if ($spAppPool.ProcessAccountName -ne $userName) {
        
          DBG ('Update service AppPool account: {0}' -f $userName)

          DBGSTART
          $spManagedAccount = Get-SPManagedAccount $userName -EA SilentlyContinue
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND

          DBGSTART
          Set-SPServiceApplicationPool -Identity $spAppPool -Account $spManagedAccount -EA SilentlyContinue | Out-Null
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
        }
      }
    }
  }
}


function global:Get-SPServiceInstanceOnAServer ([string] $svcInstance, [string] $server)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $svcInstance }

  $spSvc = $null

  if (Is-ValidString $svcInstance) {

    DBG ('Get SPServiceInstance: {0}' -f $svcInstance)
    DBGSTART
    $spSvc = Get-SPServiceInstance | ? { ($_.TypeName -eq $svcInstance) -and ($_.Server.Name -eq $server) }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('Found SPServiceInstances: # = {0}' -f (Get-CountSafe $spSvc))
    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $spSvc) -ne 1 }
    
    DBG ('Found SP service instance: {0} | {1} | {2} | {3}' -f $spSvc.TypeName, $spSvc.Id, $spSvc.Service.ProcessIdentity.UserName, $spSvc.Status)
  }

  return $spSvc   
}


function global:Get-SPServiceInstanceOnThisServer ([string] $svcInstance)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $svcInstance }

  $spSvc = $null

  if (Is-ValidString $svcInstance) {

    DBG ('Get SPServiceInstance: {0}' -f $svcInstance)
    DBGSTART
    $spSvc = Get-SPServiceInstance | ? { ($_.TypeName -eq $svcInstance) -and ($_.Server.Name -eq $global:thisComputerNetBIOS) }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('Found SPServiceInstances: # = {0}' -f (Get-CountSafe $spSvc))
    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $spSvc) -ne 1 }
    
    DBG ('Found SP service instance: {0} | {1} | {2} | {3}' -f $spSvc.TypeName, $spSvc.Id, $spSvc.Service.ProcessIdentity.UserName, $spSvc.Status)
  }

  return $spSvc   
}


function global:EnableDisable-SPServiceInstanceOnThisServer ([string] $svcInstance, [string] $operation)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $svcInstance }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $operation }

  if (Is-ValidString $svcInstance) {
  
    $spSvc = Get-SPServiceInstanceOnThisServer $svcInstance

    if ((Get-CountSafe $spSvc) -gt 0) {

      foreach ($oneSpSvc in $spSvc) {
      
        DBG ('Processing SPServiceInstance: {0} | {1} | {2} | {3}' -f $oneSpSvc.TypeName, $oneSpSvc.Id, $oneSpSvc.Service.ProcessIdentity.UserName, $oneSpSvc.Status)
        
        if (($operation -eq '+') -and ($oneSpSvc.Status -ne 'Online')) {

          if ($svcInstance -eq 'SharePoint Server Search') {

            RestartUpdate-NetTcpPortSharing

            DBG ('Start Enterprise Search service instance')
            DBGSTART
            Start-SPEnterpriseSearchServiceInstance -Identity $oneSpSvc | Out-Null
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND

          } elseif ($svcInstance -eq 'Search Query and Site Settings Service') {
          
            DBG ('Start Enterprise Search Query and Site Settings service instance')
            DBGSTART
            Start-SPEnterpriseSearchQueryAndSiteSettingsServiceInstance -Identity $oneSpSvc -Confirm:$false | Out-Null
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND

          } else {

            DBG ('Start service instance')
            DBGSTART
            Start-SPServiceInstance -Identity $oneSpSvc | Out-Null
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
          }
        }

        if (($operation -eq '-') -and ($oneSpSvc.Status -ne 'Offline')) {
        
          if ($svcInstance -eq 'SharePoint Server Search') {

            DBG ('Stop Enterprise Search service instance')
            DBGSTART
            Stop-SPEnterpriseSearchServiceInstance -Identity $oneSpSvc -Confirm:$false | Out-Null
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
          
          } elseif ($svcInstance -eq 'Search Query and Site Settings Service') {
          
            DBG ('Stop Enterprise Search Query and Site Settings service instance')
            DBGSTART
            Stop-SPEnterpriseSearchQueryAndSiteSettingsServiceInstance -Identity $oneSpSvc -Confirm:$false | Out-Null
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND

          } else {

            DBG ('Stop service instance')
            DBGSTART
            Stop-SPServiceInstance -Identity $oneSpSvc -Confirm:$false | Out-Null
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND
          }
        }

        #
        #
    
        $trialCount = 107
        do {

          DBG ('Waiting for the service instance to change its state: {0}' -f $svcInstance)
      
          $trialCount --
          DBG ('Sleeping 4sec to allow the operation to complete. Remaining trial count: {0}' -f $trialCount)
          Start-Sleep 4
     
          $tempSpSvc = Get-SPServiceInstanceOnThisServer $svcInstance

        } while ((($tempSpSvc.Status -eq 'Provisioning') -or ($tempSpSvc.Status -eq 'Unprovisioning')) -and ($trialCount -gt 0))

        DBGIF $MyInvocation.MyCommand.Name { $trialCount -le 0 }

        #
        #

        if ($svcInstance -eq 'SharePoint Server Search') {

          $spVersion = Get-SPVersionDetails

          if ($spVersion.Version -ne 2010) {

            Wait-Periodically -maxTrialCount 49 -sleepSec 2 -sleepMsg 'Waiting for the Search Host Controller Service to start' -scriptBlockWhichReturnsTrueToStop {
            
              $ssSearchHostControllerSvc = $null
              $ssSearchHostControllerSvc = Get-SPServiceInstanceOnThisServer 'Search Host Controller Service'
              return (-not (($ssSearchHostControllerSvc.Status -eq 'Provisioning') -or ($ssSearchHostControllerSvc.Status -eq 'Unprovisioning')))
            }
          }
        }
      }
    }
  }
}


function global:Wait-SPServiceApplicationProxyOnline ([string] $id)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { $id -notmatch $global:rxGUID }

  Wait-Periodically -maxTrialCount 37 -sleepSec 2 -sleepMsg ('Waiting for the Service Application Proxy to start: {0}' -f $id) -scriptBlockWhichReturnsTrueToStop {

    $spServiceAppProxy = $null
    $spServiceAppProxy = Get-SPServiceApplicationProxy -Identity $id
    DBG ('The current proxy state: {0} | {1} | version = {2}' -f $spServiceAppProxy.TypeName, $spServiceAppProxy.Status, $spServiceAppProxy.Version)
    return (-not (($spServiceAppProxy.Status -eq 'Provisioning') -or ($spServiceAppProxy.Status -eq 'Unprovisioning')))
  }
}


# Microsoft.Office.Server.Search.Administration.CrawlTopologyActivationJobDefinition
# CrawlTopologyActivationJob*
# Microsoft.Office.Server.Search.Administration.CrawlTopologyCleanupJobDefinition
# CrawlTopologyCleanupJob*

function global:Wait-SPJobAlreadyStarted ([DateTime] $startedSince, [string] $jobNameWildcard, [string] $jobType)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $jobNameWildcard }

  if (Is-ValidString $jobNameWildcard) {

    [scriptblock] $searchCondition = { ($_.Name -like $jobNameWildcard) -and ($_.LastRunTime -ge $startedSince) -and ((Is-EmptyString $jobType) -or ($_.GetType().FullName -eq $jobType)) }

    Wait-Periodically -maxTrialCount 40 -sleepSec 7 -sleepMsg ('Waiting for the jobs to appear: {0} | {1:s}' -f $jobNameWildcard, $startedSince) -scriptBlockWhichReturnsTrueToStop { 

      DBGSTART
      [Microsoft.SharePoint.Administration.SPJobDefinition[]] $jobsToGetStatus = $null
      $jobsToGetStatus = Get-SPTimerJob | ? $searchCondition | Sort LastRunTime
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ('Found such jobs: #{0} | {1}' -f (Get-CountSafe $jobsToGetStatus), (($jobsToGetStatus | Select -Expand Name) -join ', '))

      return ((Get-CountSafe $jobsToGetStatus) -gt 0)
    }

    DBG ('Get the jobs again: {0} | {1:s}' -f $jobNameWildcard, $startedSince)
    DBGSTART
    [Microsoft.SharePoint.Administration.SPJobDefinition[]] $jobsToGetStatus = $null
    $jobsToGetStatus = Get-SPTimerJob | ? $searchCondition | Sort LastRunTime
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('Found such jobs: #{0} | {1}' -f (Get-CountSafe $jobsToGetStatus), (($jobsToGetStatus | Select -Expand Name) -join ', '))

    if ((Get-CountSafe $jobsToGetStatus) -gt 0) {

      [Microsoft.SharePoint.Administration.SPJobDefinition] $jobToGetStatus = $jobsToGetStatus[0]
      DBG ('The first job: {0} | {1:s}' -f $jobToGetStatus.Name, $jobToGetStatus.LastRunTime)

      DBGSTART
      [Microsoft.SharePoint.Administration.SPJobHistory[]] $historyEntries = $null
      $historyEntries = $jobToGetStatus.HistoryEntries | ? { $_.StartTime.ToLocalTime() -ge $startedSince } | Sort StartTime
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ('We have history entries newer than the time: #{0}' -f (Get-CountSafe $historyEntries))

      [Microsoft.SharePoint.Administration.SPJobHistory] $firstHistory = $historyEntries | Select -First 1
      DBG ('The first history entry: since = {0:s} | start = {1:s} | startDelta = {2:N1} sec | end = {3:s} | runTime = {4:N1} sec | status = {5}' -f $startedSince, $firstHistory.StartTime.ToLocalTime(), ($firstHistory.StartTime.ToLocalTime() - $startedSince).TotalSeconds, $firstHistory.EndTime.ToLocalTime(), ($firstHistory.EndTime.ToLocalTime() - $firstHistory.StartTime.ToLocalTime()).TotalSeconds, $firstHistory.Status)
      DBGIF ('Job status: {0}' -f $firstHistory.Status) { $firstHistory.Status -ne 'Succeeded' }
    }
  }
}


function global:StartWait-SPJob ([string] $jobName, [int] $maxTrialCount = 17)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $jobName }

  if (Is-ValidString $jobName) {

    [DateTime] $preJobStart = [DateTime]::Now

    DBG ('Open the timer job: {0}' -f $jobName)
    DBGSTART
    $jobToStart = $null
    $jobToStart = Get-SPTimerJob $jobName
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF ('Cannot find the timer job: {0}' -f $jobName) { Is-Null $jobToStart }

    if (Is-NonNull $jobToStart) {

      DBG ('Start the job: {0} | {1}' -f $jobToStart.Name, $jobToStart.DisplayName)
      DBGSTART
      Start-SPTimerJob $jobToStart | Out-Null
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      Wait-Periodically -maxTrialCount $maxTrialCount -sleepSec 7 -sleepMsg ('Waiting for job to finish: {0}' -f $jobToStart.DisplayName) -scriptBlockWhichReturnsTrueToStop { (Get-SPTimerJob $jobName).LastRunTime -gt $preJobStart }

      DBG ('Chech the job status: {0}' -f $jobName)
      DBGSTART
      $jobToGetStatus = $null
      $jobToGetStatus = Get-SPTimerJob $jobName
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGIF $MyInvocation.MyCommand.Name { Is-Null $jobToGetStatus }
      DBG ('The last time the job run: {0}' -f $jobToGetStatus.LastRunTime)     

      [object[]] $failedJobEntries = $jobToGetStatus.HistoryEntries | ? { $_.StartTime.ToLocalTime() -gt $preJobStart } | ? { $_.Status -ne 'Succeeded' }
      DBGIF ("The job failed on some farm members: {0} | {1} | servers = {2} | -->`r`n{3}" -f $jobToGetStatus.Name, $jobToGetStatus.DisplayName, (($failedJobEntries | Select -Expand ServerName) -join ','), (($failedJobEntries | % { '{0}: {1}' -f $_.ServerName,$_.ErrorMessage } | select -Unique) -join ', ')) { $failedJobEntries.Length -gt 0 }
    }
  }
}



function global:Get-SPVersionDetails ()
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $spVersion = New-Object PSCustomObject
  Add-Member -Input $spVersion -MemberType NoteProperty -Name version -Value 0
  Add-Member -Input $spVersion -MemberType NoteProperty -Name edition -Value ''
  Add-Member -Input $spVersion -MemberType NoteProperty -Name product -Value ''

  DBG ('Get the farm')
  DBGSTART
  $farm = Get-SPFarm
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $farm }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $farm.Products }
  # We cannot use the Get-CountSafe as the .Products is something else than ArrayList or Array
  # and the method does not work
  #DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $farm.Products) }

  DBG ('Farm opened: {0}' -f $farm.BuildVersion)
  switch ($farm.BuildVersion) {

    { $_ -like '14.0.*' } { $spVersion.version = 2010 }
    { $_ -like '15.0.*' } { $spVersion.version = 2013 }
    { $_ -like '16.0.*' } { $spVersion.version = 2016 }

    default { DBGIF ('Unknown sharepoint build version: {0}' -f $farm.BuildVersion) { $true } }
  }


  foreach ($oneProduct in $farm.Products) {

    DBG ('One product found: {0}' -f $oneProduct)

    switch ($oneProduct) {

     'BEED1F75-C398-4447-AEF1-E66E1F0DF91E' {

              $spVersion.edition = 'foundation'
              $spVersion.product = 'foundation'
              DBGIF $MyInvocation.MyCommand.Name { $spVersion.version -ne 2010 }
            }

     '3FDFBCC8-B3E4-4482-91FA-122C6432805C' {

              $spVersion.edition = 'server'
              $spVersion.product = 'standard'
              DBGIF $MyInvocation.MyCommand.Name { $spVersion.version -ne 2010 }
            }

     'D5595F62-449B-4061-B0B2-0CBAD410BB51' {

              $spVersion.edition = 'server'
              $spVersion.product = 'enterprise'
              DBGIF $MyInvocation.MyCommand.Name { $spVersion.version -ne 2010 }
            }


     '9FF54EBC-8C12-47D7-854F-3865D4BE8118' {

              $spVersion.edition = 'foundation'
              $spVersion.product = 'foundation'
              DBGIF $MyInvocation.MyCommand.Name { $spVersion.version -ne 2013 }
            }

     'C5D855EE-F32B-4A1C-97A8-F0A28CE02F9C' {

              $spVersion.edition = 'server'
              $spVersion.product = 'standard'
              DBGIF $MyInvocation.MyCommand.Name { $spVersion.version -ne 2013 }
            }

     'B7D84C2B-0754-49E4-B7BE-7EE321DCE0A9' {

              $spVersion.edition = 'server'
              $spVersion.product = 'enterprise'
              DBGIF $MyInvocation.MyCommand.Name { $spVersion.version -ne 2013 }
            }

     '4F593424-7178-467A-B612-D02D85C56940' {

              $spVersion.edition = 'server'
              $spVersion.product = 'standard'
              DBGIF $MyInvocation.MyCommand.Name { $spVersion.version -ne 2016 }
            }

     '716578D2-2029-4FF2-8053-637391A7E683' {

              $spVersion.edition = 'server'
              $spVersion.product = 'enterprise'
              DBGIF $MyInvocation.MyCommand.Name { $spVersion.version -ne 2016 }
            }
    }
  }


  DBG ('Current SharePoint version determined as: version = {0} | edition = {1} | product = {2}' -f $spVersion.version, $spVersion.edition, $spVersion.product)

  DBGIF $MyInvocation.MyCommand.Name { $spVersion.version -lt 2010 }
  DBGIF $MyInvocation.MyCommand.Name { ($spVersion.edition -ne 'foundation') -and ($spVersion.edition -ne 'server') }

  return $spVersion
}


function global:EnableDisable-SPFeatures ([string] $url, [string] $featuresToModify)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $url }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $featuresToModify }

  if ((Is-ValidString $url) -and (Is-ValidString $featuresToModify)) {

    $featureList = Split-MultiValue $featuresToModify
    DBG ('Going to modify features: {0}' -f (Get-CountSafe $featureList))
    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $featureList) -lt 1 }

    $spVersion = Get-SPVersionDetails

    foreach ($oneFeature in $featureList) {

        DBG ('One feature to modify: {0}' -f $oneFeature)
        [string] $featureOperation = $oneFeature[0]
        [string] $featureIdentity = $oneFeature.SubString(1)

    
        if (
            (($featureIdentity -eq 'MDSFeature') -and ($spVersion.version -eq '2010')) -or
            (($featureIdentity -eq 'Publishing') -and ($spVersion.edition -eq 'foundation')) -or
            (($featureIdentity -eq 'PublishingSite') -and ($spVersion.edition -eq 'foundation'))
            ) {

          DBGIF ('Feature does not exist on this version. Skipping: {0} | {1} | {2} | {3}' -f $featureIdentity, $spVersion.version, $spVersion.edition, $spVersion.product) { $true }
          continue
        }

        
        DBG ('Get the feature first: {0}' -f $featureIdentity)
        DBGSTART
        $theFeature = $null
        $theFeature = Get-SPFeature -Identity $featureIdentity
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBGIF ('The requested feature does not exist: {0}' -f $featureIdentity) { Is-Null $theFeature }
        DBG ('Feature opened: {0} | {1} | {2} | {3}' -f $theFeature.DisplayName, $theFeature.Id, $theFeature.Scope, $theFeature.CompatibilityLevel)

        if (Is-NonNull $theFeature) {

          if ($featureOperation -eq '+') {

              DBG ('Enable the feature: {0} | {1} | {2}' -f $theFeature.DisplayName, $theFeature.Id, $url)
              DBGSTART
              # Note: with -Force here, even if the feature is already active, the command succeeds without errors
              Enable-SPFeature -Url $url -Identity $theFeature -Confirm:$false -Force
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND
              
          } else {

              DBG ('Disable the feature: {0} | {1} | {2}' -f $theFeature.DisplayName, $theFeature.Id, $url)
              DBGSTART
              # Note: although there is the -Force parameter,
              #       yet in case the feature is not active at the scope
              #       an error occures which we try to simply ignore
              Disable-SPFeature -Url $url -Identity $theFeature -Confirm:$false -Force
              
              if (($error.Count -gt 0) -and ($error[0].Exception.Message -like 'Feature ''????????-????-????-????-????????????'' is not activated at this scope.')) {

                DBG ('The feature was not active at the scope, so nothing happened.')
                
              } else {

                DBGER $MyInvocation.MyCommand.Name $error
              }
              DBGEND
          }
        }
    }
  }
}


function global:Assert-SPWebTemplate ([string] $template, [string] $language)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBG ('Check if the web site template exists: {0} | {1}' -f $template, $language)
  DBGSTART
  $webTemplateFound = $null
  $webTemplateFound = Get-SPWebTemplate | ? { ($_.Name -eq $template) -and ($_.LocaleId -eq $language) }
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  # Note: if the template does not exist, the web will be created with no template
  #       and the user will have to specify the template at the first attempt to access the site
  DBG ('Web site template found: {0} | {1}' -f $webTemplateFound.Name, $webTemplateFound.Title)
  DBGIF ('The specified web template does not exist: {0}' -f $template) { Is-Null $webTemplateFound }
}


function global:Enable-SPDatabaseAdminAccess ([string] $dbName, [string] $admins, [object] $databaseObject)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $dbName) -and (Is-Null $databaseObject) }

  if (Is-Null $databaseObject) {

    DBGSTART
    $databaseToSetAdmins = Get-SPDatabase | ? { $_.Name -eq $dbName }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF ('Databases found: {0}' -f (Get-CountSafe $databaseToSetAdmins)) { (Get-CountSafe $databaseToSetAdmins) -ne 1 }

    if ((Get-CountSafe $databaseToSetAdmins) -eq 1) {

      $databaseObject = $databaseToSetAdmins
    }
  }

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $databaseObject }

  if (Is-NonNull $databaseObject) {

    DBG ('Obtained the database to update shell admins: {0} | {1} | {2}' -f $databaseObject.Name, $databaseObject.Id, $databaseObject.Type)

    DBGIF ('Weird server reference type: {0} | {1}' -f $databaseObject.Server.GetType().FullName, $databaseObject.Server) { (-not ($databaseObject.Server -is [string])) -and (-not ($databaseObject.Server -is [Microsoft.SharePoint.Administration.SPServer])) }
    [string] $serverName = ''
    if ($databaseObject.Server -is [string]) {

      $serverName = $databaseObject.Server

    } else {

      $serverName = $databaseObject.Server.Name
    }

    DBG ('Database server determined: {0}' -f $serverName)
    DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $serverName }

    DBG ('Enabling shell admin access to the database: {0}' -f $admins)
    DBGSTART
    Add-SPShellAdmin -Database $databaseObject -UserName $admins | Out-Null
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Grant SPAdmins the db_owner role of the database in order to allow some later actions during regular lifecycle, such as Upgrade-SPContentDatabase or Backup-SPSite')
    Execute-NonQueryRemote -sqlServer $serverName -database $databaseObject.Name -nonQuery ('EXECUTE sp_addrolemember @rolename = "db_owner", @membername = "{0}"' -f $spAdmins)
  }
}


function global:Create-DNSZone ([string] $zoneDef)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $zoneDef }

  if (Is-ValidString $zoneDef) {

    $cmdSwitches = ""
    $zoneName = (Strip-ValueFlags $zoneDef)

    if (Has-ValueFlags $zoneDef D) { $cmdSwitches = $cmdSwitches + ' /DsPrimary' }
    if (Has-ValueFlags $zoneDef F) { $cmdSwitches = $cmdSwitches + ' /DP /Forest' }
    if (Has-ValueFlags $zoneDef O) { $cmdSwitches = $cmdSwitches + ' /DP /Domain' }
    if (Has-ValueFlags $zoneDef P) { $cmdSwitches = $cmdSwitches + ' /Primary' }
    if (Has-ValueFlags $zoneDef S) { $cmdSwitches = $cmdSwitches + ' /Secondary' }
    if (Has-ValueFlags $zoneDef R) { 
      
      $ipNetList = [System.Collections.ArrayList] $zoneName.Trim('.').Split('.')
      $ipNetList.Reverse()
      $zoneName = ($ipNetList -join '.') + '.in-addr.arpa'
    }

    Run-Process 'DNSCMD' ('localhost /ZoneAdd "{0}" {1}' -f $zoneName, $cmdSwitches)

    # Note: the zone properties must be set individually, they cannot be combined on a single command line
    # Note: AllowUpdate: 0 = no update, 1 = all updates, 2 = only secure
    $cmdSwitches = @()
    if (Has-ValueFlags $zoneDef 0) { $cmdSwitches += '/AllowUpdate 0' }
    if (Has-ValueFlags $zoneDef 1) { $cmdSwitches += '/AllowUpdate 1' }
    if (Has-ValueFlags $zoneDef 2) { $cmdSwitches += '/AllowUpdate 2' }
    if (Has-ValueFlags $zoneDef A) { $cmdSwitches += ('/Aging 1', '/RefreshInterval 192', '/NoRefreshInterval 24') }

    if ($cmdSwitches.Count -ge 0) {
       
      $cmdSwitches | % {
        
        Run-Process 'DNSCMD' ('localhost /Config "{0}" {1}' -f $zoneName, $_)
      }
    }
      
    if (Is-ValidString $vmConfig.dns.scavenge) {
      
      # Note: you can use dash (-) as a means to reset the server's scavenging to none
      # Note: note also, that the DNS server settings show in GUI only after the DNS service is restarted
      Run-Process 'DNSCMD' ('localhost /Config /ScavengingInterval {0}' -f $vmConfig.dns.scavenge)
    }
  }
}


function global:Add-DNSRecord ([string] $type, [string] $name, [string] $zone, [string] $value)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $type }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $name }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $zone }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $value }

  if ($thisOSVersion -like '5.*') {

    Run-Process 'DNSCMD' ('localhost /RecordAdd {0} {1} {2} {3} ' -f $zone, $name, $type, $value)
  }
  
  else {
  
    Run-Process 'DNSCMD' ('localhost /RecordAdd {0} {1} /CreatePTR {2} {3} ' -f $zone, $name, $type, $value) -ignoreExitCodes @(0x25F3)
  }
}



function global:Apply-NtfsDacl ([string] $ntfs, [string] $dacl, [bool] $addOnly, [bool] $enableSeRestorePrivilege)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $ntfs }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $ntfs) }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dacl }
  
  if ((Is-ValidString $ntfs) -and (Test-Path $ntfs) -and (Is-ValidString $dacl)) {
  
    DBG ('Get current ACL with Get-Acl for: {0}' -f $ntfs)
    DBGSTART
    $acl = Get-Acl -Path $ntfs
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

<#    
    # Note: this method is no different from the Get-Acl
    #       both return DACL+Owner regardless

    DBG ('Get current ACL with GetAccessControl for: {0}' -f $ntfs)
    DBGSTART
    $acl = (Get-Item $ntfs).GetAccessControl('Access')
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
#>    

    DBG ('Current ACL: {0} | {1} | {2} | {3}' -f $acl.AccessToString.Replace("`r", '').Replace("`n", ', ').Replace('  ', ':'), $acl.Owner, $acl.AreAccessRulesProtected, $acl.Sddl)

    if (-not $addOnly) {
    
      DBG ('Disable inheritance as requested.')
      DBGSTART
      $acl.SetAccessRuleProtection($true, $false)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
    
    foreach ($oneDACL in (Split-MultiValue $dacl)) { 
    
      $principal = Strip-ValueFlags $oneDACL
      
      [string] $access = $null
      $propagate = 'None'
      $subObjects = 'ContainerInherit,ObjectInherit'
      $allowDeny = 'Allow'
      
      if (Has-ValueFlags $oneDACL 'F') { $access = 'FullControl' }
      if (Has-ValueFlags $oneDACL 'R') { $access = 'Read' }
      if (Has-ValueFlags $oneDACL 'X') { $access = 'ReadAndExecute' }
      if (Has-ValueFlags $oneDACL 'M') { $access = 'Modify' }
      if (Has-ValueFlags $oneDACL 'W') { $access = 'Write' }
      if (Has-ValueFlags $oneDACL 'C') { $access = 'CreateFiles,CreateDirectories' }
      if (Has-ValueFlags $oneDACL 'O') { $access = 'CreateFiles,CreateDirectories,WriteAttributes,WriteExtendedAttributes' } # just what ROBOCOPY needs on root folder
        
      if (Has-ValueFlags $oneDACL 'I') { $propagate = 'InheritOnly' }
      if (Has-ValueFlags $oneDACL 'S') { $propagate = 'NoPropagateInherit' }

      if (Has-ValueFlags $oneDACL 'T') { $subObjects = 'None' }
      if (Has-ValueFlags $oneDACL 'U') { $subObjects = 'ObjectInherit' }
      if (Has-ValueFlags $oneDACL 'V') { $subObjects = 'ContainerInherit' }
      
      if (Has-ValueFlags $oneDACL 'D') { $allowDeny = 'Deny' }
      

      if (-not (Has-ValueFlags $oneDACL '-')) {

        DBG ('Build new ACE: path = {0} | princ = {1} | access = {2} | prop = {3} | sub = {4}' -f $ntfs, $principal, $access, $propagate, $subObjects)
        DBGSTART
        $accessRule = New-Object System.Security.AccessControl.FileSystemAccessRule ($principal, $access, $subObjects, $propagate, $allowDeny)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBG ('New ACE created: {0} | {1} | {2} | {3} | {4} | {5}' -f $accessRule.FileSystemRights, $accessRule.AccessControlType, $accessRule.IdentityReference, $accessRule.IsInherited, $accessRule.InheritanceFlags, $accessRule.PropagationFlags)

        DBG ('Add the new ACE into the ACL')
        DBGSTART
        $acl.AddAccessRule($accessRule)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

      } else {

        DBG ('We should find and delete ACEs: {0} | {1} | {2}' -f $principal, $access, $propagate)
        $existingAccessRules = $acl.Access
        DBG ('Current access rules: {0}' -f (Get-CountSafe $existingAccessRules))
        DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $existingAccessRules) -lt 1 }

        if (Get-CountSafe $existingAccessRules) {

          [int] $nonInheritedCount = 0
          [bool] $atLeastOneProcessed = $false

          foreach ($oneExistingAccessRule in $existingAccessRules) {

            DBG ('Existing ACE: {0} | inherited = {1} | {2}' -f $oneExistingAccessRule.IdentityReference.Value, $oneExistingAccessRule.IsInherited, $oneExistingAccessRule.FileSystemRights)
            
            $matchingACE = $null

            if (-not $oneExistingAccessRule.IsInherited) {

              $nonInheritedCount ++

              if ($oneExistingAccessRule.IdentityReference.Value -eq $principal) {

                if (Is-ValidString $access) {

                  if ($oneExistingAccessRule.FileSystemRights -eq $access) {

                    DBG ('We hit a matching ACE by the both principal reference and access mask')
                    $matchingACE = $oneExistingAccessRule
    
                  } else {

                    DBG ('Although this ACE matches the principal, it does not match the access mask requested: is = {0} | shouldBe = {1}' -f $oneExistingAccessRule.FileSystemRights, $access)
                  }

                } else {

                  DBG ('We hit a matching ACE by the mere principal reference')
                  $matchingACE = $oneExistingAccessRule
                }
              }
            }

            if (Is-NonNull $matchingACE) {

              DBG ('Existing ACE matched the filter, remove it')
              DBGSTART
              $removed = $acl.RemoveAccessRule($matchingACE)
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND
              DBGIF $MyInvocation.MyCommand.Name { -not $removed }

              $atLeastOneProcessed = $atLeastOneProcessed -or $removed
            }
          }

          DBGIF $MyInvocation.MyCommand.Name { $nonInheritedCount -lt 1 }
          DBGIF $MyInvocation.MyCommand.Name { -not $atLeastOneProcessed }
        }
      }
    }

    if ($enableSeRestorePrivilege) {

      # Note: in case of different Owner than the current user is. The Set-Acl can either
      #       leave the owner intact if it is the current user or Administrators, or we
      #       must have the SeRestorePrivilege to impose a different Owner than us
      #       The error without having the privilege enabled would be: The security identifier is not allowed to be the owner of this ob
      Adjust-Privilege (Lookup-Privilege SeRestorePrivilege) -enable $true -threadToken $false

    } else {

      $currentUser = [Security.Principal.WindowsIdentity]::GetCurrent($false).Name

      # Note: just some asserts

      if (Is-LocalComputerDomainController) {

        $domainAdmins = "$global:thisComputerDomainNetBIOS\Domain Admins"
        $memberOfDomainAdmins = Is-CurrentUser -domainAdmins $true

        DBGIF ('Object owner is different than the current user: {0} | {1}' -f $acl.Owner, $currentUser) { ($acl.Owner -ne $currentUser) -and (-not (($acl.Owner -eq 'BUILTIN\Administrators') -and ($global:memberOfAdministrators))) -and (-not (($acl.Owner -eq $domainAdmins) -and $memberOfDomainAdmins)) }

      } else {

        DBGIF ('Object owner is different than the current user: {0} | {1}' -f $acl.Owner, $currentUser) { ($acl.Owner -ne $currentUser) -and (-not (($acl.Owner -eq 'BUILTIN\Administrators') -and ($global:memberOfAdministrators))) }
      }
    }

    DBG ('New ACL: {0} | {1} | {2} | {3}' -f $acl.AccessToString.Replace("`r", '').Replace("`n", ', ').Replace('  ', ':'), $acl.Owner, $acl.AreAccessRulesProtected, $acl.Sddl)
    DBG ('Apply the new ACL to: {0}' -f $ntfs)
    DBGSTART
    Set-Acl -Path $ntfs -AclObject $acl -EV er -EA SilentlyContinue
    DBGER $MyInvocation.MyCommand.Name $er
    DBGEND
  }
}


function global:New-WmiSecurityDescriptor ([string[]] $ACEs)
# Note: ACE format: 'who|allow/deny|accessMask|inheritFlags
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $ACEs) -lt 1 }
  [WMI] $wmiSD = $null

  if ((Get-CountSafe $ACEs) -gt 0) {

    DBG ('Create new WMI SD')
    DBGSTART
    $wmiSD = ([wmiclass] 'Win32_SecurityDescriptor').CreateInstance()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    
    if (Is-NonNull $wmiSD) {
      
      [Collections.ArrayList] $wmiACEList = @()

      foreach ($oneACE in $ACEs) {

        DBG ('Create new WMI ACE')
        DBGSTART
        [WMI] $wmiACE = $null
        $wmiACE = ([wmiclass] 'Win32_ACE').CreateInstance()
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBGIF $MyInvocation.MyCommand.Name { Is-Null $wmiACE }

        if (Is-NonNull $wmiACE) {

          DBG ('Create new WMI Trustee')
          DBGSTART
          [WMI] $wmiTrustee = ([wmiclass] 'Win32_Trustee').CreateInstance()
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
          DBGIF $MyInvocation.MyCommand.Name { Is-Null $wmiTrustee }

          if (Is-NonNull $wmiTrustee) {

            $aceTokens = Split-MultiValue $oneACE

            [string] $who = Get-SAMLogin $aceTokens[0]
            
            [int] $type = 0
            switch ($aceTokens[1]) {

              'allow' { $type = 0 }
              'deny' { $type = 1 }
            }

            [int] $accessMask = $aceTokens[2]
            [int] $inheritFlags = $aceTokens[3]

            DBG ('Populate the trustee parameters into the new ACE: who = {0} | type = {1} | accessMask = {2} | inheritFlags = {3}' -f $who, $type, $accessMask, $inheritFlags)
            DBGSTART
            $wmiTrustee.Name = $who
            $wmiTrustee.Domain = $null
            $wmiACE.AccessMask = $accessMask
            $wmiACE.AceFlags = $inheritFlags
            $wmiACE.AceType = $type
            $wmiACE.Trustee = $wmiTrustee
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND

            [void] $wmiACEList.Add($wmiACE)
          }
        }
      }

      DBG ('Add the new ACEs into the DACL of the security descriptor: {0}' -f (Get-CountSafe $wmiACEList))
      DBGSTART
      $wmiSD.DACL = $wmiACEList
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
  }

  DBGIF $MyInvocation.MyCommand.Name { Is-Null $wmiSD }
  return $wmiSD
}


function global:Create-NTFSFolderAndShare ([string] $ntfsPath, [string] $share, [string] $dacl, [string] $quota, [bool] $addDaclOnly, [bool] $exists, [bool] $adminFullEveryoneChange, [string] $parentDaclIfCreating)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $ntfsPath }
  
  if (Is-ValidString $ntfsPath) {
  
    if (Test-Path $ntfsPath) { 
    
      DBG ('Folder exists: {0}' -f $ntfsPath)
    }
    
    else {

      DBGIF ('The folder does not exist which should exist: {0}' -f $ntfsPath) { $exists }

      DBG ('Folder does not exist. Create parent tree: {0}' -f $ntfsPath)
      
      $pathComponents = Split-PathComponents $ntfsPath
      DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $pathComponents) -lt 1 }
      
      if ((Get-CountSafe $pathComponents) -gt 0) {

        if (Is-EmptyString $parentDaclIfCreating) {

          $parentDaclIfCreating = 'F$Administrators'
        }
      
        for ($i = 1; $i -lt ($pathComponents.Count - 1); $i ++) {

          if (-not (Test-Path $pathComponents[$i])) {

            DBG ('Create parent folder: {0}' -f $pathComponents[$i])
            DBGSTART
            New-Item -Path $pathComponents[$i] -ItemType Directory -Force | Out-Null
            DBGER $MyInvocation.MyCommand.Name $error
            DBGEND

            Apply-NTFSDacl $pathComponents[$i] $parentDaclIfCreating

          } else {

            DBG ('Parent folder exists ok: {0}' -f $pathComponents[$i])
          }
        }

        DBG ('Create the folder itself: {0}' -f $ntfsPath)    
        DBGSTART
        New-Item -Path $ntfsPath -ItemType Directory -Force | Out-Null
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }
    }


    if (Is-ValidString $dacl) {

      if ($addDaclOnly) {

        Apply-NTFSDacl $ntfsPath $dacl $true

      } else {
       
        Apply-NTFSDacl $ntfsPath ('F$Administrators|{0}' -f $dacl)
      }
    }


    if (Is-ValidString $share) {
    
      DBG ('Share folder {0} as {1}' -f $ntfsPath, $share)

      if ((($ntfsPath -like '[a-z]:') -or ($ntfsPath -like '[a-z]:\')) -or $adminFullEveryoneChange) {

        DBG ('Path to be shared is disk root which needs sharing with WMI or we required Admin/Everyone permissions: {0}' -f $adminFullEveryoneChange)
        DBGSTART
        $wmiShare = [wmiclass] 'Win32_Share'
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

        # Note: 2032127 = FullControl = 1 1111 0000 0001 1111 1111
        #       1245631 = Change      = 1 0011 0000 0001 1011 1111
        #       1179817 = Read        = 1 0010 0000 0000 1010 1001
        #
        # Note:  1 = object inherit
        #        2 = container inherit
        #        4 = no propagate inherit
        #        8 = iherit only
        #       16 = inherited entry

        if (-not $adminFullEveryoneChange) {

          $wmiSD = New-WmiSecurityDescriptor 'Everyone|allow|2032127|3'

        } else {

          $wmiSD = New-WmiSecurityDescriptor 'Everyone|allow|1245631|3', 'BUILTIN\Administrators|allow|2032127|3'
        }

        DBG ('Create the share with Win32_Share and Win32_SecurityDescriptor: {0} | {1}' -f $share, $ntfsPath)
        DBGSTART
        $wmiRs = $null
        $wmiRs = $wmiShare.Create($ntfsPath, $share, 0, $null, $null, $null, $wmiSD)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBGWMI $wmiRs

      } else {

        DBG ('Going to share the folder with NET SHARE: {0} | {1}' -f $share, $ntfsPath)
        Run-Process 'NET' ('SHARE "{0}"="{1}" /GRANT:Everyone,FULL' -f $share, $ntfsPath)
      }
    }
    
    if (Is-ValidString $quota) {
    
      DBG ('Set folder qouta: {0} = {1}' -f $quota, $ntfsPath)
      Run-Process 'DIRQUOTA' ('quota add /path:"{0}" /limit:{1} /type:hard /status:enabled /overwrite' -f $ntfsPath, $quota)
    }
  }
}


function global:Register-WMIEventScript ([string] $name, [string] $query, [string] $pathAndParams)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $name }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $query }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $pathAndParams }

  if ((Is-ValidString $name) -and (Is-ValidString $query) -and (Is-ValidString $pathAndParams)) {

    #======================

    DBG ('Create new CMD consumer: {0} | {1}' -f $name, $pathAndParams)
    $cmdConsumer = Spawn-WMIInstance '.' 'CommandLineEventConsumer' 'root\subscription'

    DBG ('Define CMD consumer params')
    DBGSTART
    $cmdConsumer.Name = '{0}-CMDCons' -f $name
    $cmdConsumer.CommandLineTemplate = $pathAndParams
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Save the newly created consumer')
    DBGSTART
    $wmiRs = $null
    $wmiRs = $cmdConsumer.Put()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGWMI $wmiRs

    $cmdConsumerPath = $wmiRs.Path


    #======================

    DBG ('Create new WMI filter: {0} | {1}' -f $name, $query)
    $eventFilter = Spawn-WMIInstance '.' '__EventFilter' 'root\subscription'

    DBG ('Define event filter params for the newly create event filter')
    DBGSTART
    $eventFilter.Name = '{0}-EVTFilter' -f $name
    $eventFilter.QueryLanguage = 'WQL'
    $eventFilter.Query = $query
    $eventFilter.EventNamespace = 'root\CIMv2'
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Save the newly created event filter')
    DBGSTART
    $wmiRs = $null
    $wmiRs = $eventFilter.Put()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGWMI $wmiRs

    $eventFilterPath = $wmiRs.Path


    #======================

    DBG ('Finally create new binding between the consumer and the event filter: {0} | {1}' -f $eventFilterPath, $cmdConsumerPath)
    $fcBinding = Spawn-WMIInstance '.' '__FilterToConsumerBinding' 'root\subscription'

    DBG ('Define the binding')
    DBGSTART
    $fcBinding.Filter = $eventFilterPath
    $fcBinding.Consumer = $cmdConsumerPath
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Save the newly created FC binding')
    DBGSTART
    $wmiRs = $null
    $wmiRs = $fcBinding.Put()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGWMI $wmiRs
  }
}


function global:Schedule-Task ([string] $name, [string] $path, [string] $user, [string] $pwd, [string] $schedule, [bool] $testRun)
# $schedule Examples:
#   ONEVENT /EC System /MO "*[System/EventID=101]"
#   ONEVENT /EC Security /MO "*[System/EventID=4872]"   
#
#   'SELECT * FROM __InstanceCreationEvent WHERE TargetInstance ISA "Win32_NTLogEvent" AND TargetInstance.LogFile = "Security" AND (TargetInstance.EventCode = "776" OR TargetInstance.EventCode = "784")'
#
#
# Some event examples:
#   security, task category: Certification Services, 4872 - CRL published to FS on 2008+
#   security, task category: Certification Services, 4895 - AIA published to AD on 2008+
#   security, Category: Object Access, 776 - CRL published to FS on 2003
#   security, Category: Object Access, 784 - CA started on 2003
#
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $name }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $path }

  $wmiSchedule = $schedule -like 'SELECT* FROM *__Instance* WHERE *TargetInstance* ISA *'

  #DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $user) -and (-not $wmiSchedule) }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $schedule }

  if ($wmiSchedule) {

    DBG ('Schedule specified as a WMI query. Ignoring user and password - WMI since Vista run only under SYSTEM')
    Register-WMIEventScript $name $schedule $path
  
  } else {
    
    Run-Process 'schtasks' ('/Create /TN "SEVECEK\{0}" /TR "{1}" /RU "{2}" /RP "{3}" /SC {4} /F' -f $name, $path, $user, $pwd, $schedule)

    if ($testRun) {

      if ($global:thisOSVersionNumber -ge 6.1) {

        DBG ('Going to test run the task ignoring any constraints. Only on 6.1+')
        Run-Process 'schtasks' ('/Run /I /TN "SEVECEK\{0}"' -f $name)
      
      } else {

        DBG ('Going to test run the task ignoring any constraints. Only on 6.0 without /I switch')
        Run-Process 'schtasks' ('/Run /TN "SEVECEK\{0}"' -f $name)
      }
    }
  }
}


function global:Enable-Auditing ([string] $category, [string] $subcategory, [switch] $verify)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $category) -and (Is-EmptyString $subcategory) }
  DBGIF $MyInvocation.MyCommand.Name { (-not ((Is-ValidString $subcategory) -and (Is-ValidString $category)) -or (Is-EmptyString $subcategory)) }
  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $subcategory) -and ($global:thisOSVersion -like '5.*') }

  if ($global:thisOSVersion -like '5.*') {

    $scDBPath = Get-DataFileApp "secedit-auditing" $null '.sdb'
    $scLogPath = Get-DataFileApp "secedit-auditing" $null '.log'
    $scTemplatePath = Join-Path $global:rootDir ('Security\audit-{0}.inf' -f $category.Trim().Replace('/', '-').Replace(' ', '-'))

    Run-Process 'secedit' ('/configure /db "{0}" /cfg "{1}" /log "{2}" /overwrite /quiet' -f $scDBPath, $scTemplatePath, $scLogPath)
    DBG ('SECEDIT produced the following log file: {0}' -f (Get-Content $scLogPath | Out-String))

  } else {
  
    if (Is-ValidString $subcategory) {

      DBG ('Get current local auditing settings')
      Run-Process 'auditpol' ('/get /subcategory:"{0}"' -f $subcategory)

      DBG ('Set auditing for: {0}' -f $subcategory)
      Run-Process 'auditpol' ('/set /subcategory:"{0}" /success:Enable /failure:Enable' -f $subcategory)
    
    } else {

      DBG ('Get current local auditing settings')
      Run-Process 'auditpol' ('/get /category:"{0}"' -f $category)

      DBG ('Set auditing for: {0}' -f $category)
      Run-Process 'auditpol' ('/set /category:"{0}" /success:Enable /failure:Enable' -f $category)
    }
  }

  if ($verify) {

    Run-Process 'gpupdate'
    $auditPolicy = Get-AuditPolicy
    $successFailure = Format-Multivalue 'Success', 'Failure'

    if (Is-EmptyString $subcategory) {

      DBGIF ('Invalid resultant auditing settings: {0} | {1}' -f $category, $auditPolicy[$category]) { $auditPolicy[$category] -ne $successFailure }

    } else {

      DBGIF ('Invalid resultant auditing settings: {0} | {1} | {2}' -f $category, $subcategory, $auditPolicy["$category|$subcategory"]) { $auditPolicy["$category|$subcategory"] -ne $successFailure }
    }
  }
}



function global:Add-BackConnectionHostName ([string] $hostName)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $hostName }
  DBGIF $MyInvocation.MyCommand.Name { $hostName -notmatch (RxFullStr $global:rxDns) }

  if (Is-ValidString $hostName) {

    $currentBCHN = Get-RegValue '.' 'SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0' 'BackConnectionHostNames' 'MultiSZ'
    DBG ('Current BackConnectionHostNames value: {0}' -f $currentBCHN)

    $newBCHN = Add-MultiValue $currentBCHN $hostName $true
    DBG ('Should be new BackConnectionHostNames: {0}' -f $newBCHN)
    Set-RegistryValue 'HKLM:\SYSTEM\CurrentControlSet\Control\Lsa\MSV1_0' BackConnectionHostNames (Split-MultiValue $newBCHN) MultiString
  }
}


function global:Import-WMIFilter ([string] $mofFile, [string] $domainDN)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $mofFile }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $mofFile) }

  if (Test-Path $mofFile) {

    DBG ('Read WMI Filter MOF: {0}' -f $mofFile)
    DBGSTART
    $mofContents = Get-Content $mofFile -EA SilentlyContinue
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    $filterName = [System.IO.Path]::GetFileNameWithoutExtension($mofFile)
 
    [Collections.ArrayList] $allWqlFilters = @()
    $allWqlFilters += $mofContents | ? { $_.Trim() -like 'Query = "*";' }
    DBG ('WQL filters found: {0}' -f (Get-CountSafe $allWqlFilters))

    [Collections.ArrayList] $allNamespaces = @()
    $allNamespaces += $mofContents | ? { $_.Trim() -like 'TargetNameSpace = "*";' }
    DBG ('WQL filter namespaces found: {0}' -f (Get-CountSafe $allNamespaces))

    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $allWqlFilters) -ne (Get-CountSafe $allNamespaces) }
    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $allWqlFilters) -le 0 }

    if ((Get-CountSafe $allWqlFilters) -gt 0) {

      $filterGUID = Get-GuidString $true
      $filterDate = '{0}.000000-000' -f ((Get-Date).ToUniversalTime().ToString('s').Replace('T', '').Replace(':', '').Replace('-', ''))

      [string] $fullWqlFilter = '{0};' -f (Get-CountSafe $allWqlFilters)

      for ($i = 0; $i -lt (Get-CountSafe $allWqlFilters); $i++) {

        $wqlFilterRaw = $allWqlFilters[$i] #$mofContents | ? { $_.Trim() -like 'Query = "*";' } | Select-Object -First 1
        $wqlNamespaceRaw = $allNamespaces[$i] #$mofContents | ? { $_.Trim() -like 'TargetNameSpace = "*";' } | Select-Object -First 1

        $tempLeft = $wqlFilterRaw.Trim().SubString($wqlFilterRaw.IndexOf('"'))
        $wqlFilter = $tempLeft.SubString(0, $tempLeft.LastIndexOf('"'))

        $tempLeft = $wqlNamespaceRaw.Trim().SubString($wqlNamespaceRaw.IndexOf('"'))
        $wqlNamespace = $tempLeft.SubString(0, $tempLeft.LastIndexOf('"')).Replace('\\', '\')

        DBG ('One WMI subfilter: {0} | {1}' -f $wqlNamespace, $wqlFilter)

        $fullWqlFilter += '3;{0};{1};WQL;{2};{3};' -f $wqlNamespace.Length.ToString(), $wqlFilter.Length.ToString(), $wqlNamespace, $wqlFilter
      }


      DBG ('Create new WMI filter: {0} | {1} | {2} | {3}' -f $filterGUID, $filterName, $fullWqlFilter, $filterDate)

      [System.Collections.ArrayList] $deList = @()

      $somRoot = Get-DE ('CN=SOM,CN=WMIPolicy,CN=System,{0}' -f $domainDN) ([ref] $deList)

      if (Is-NonNull $somRoot) {

        $newWmiFilter = $null
        DBGSTART
        $newWmiFilter = $somRoot.Create('msWMI-Som', ('CN={0}' -f $filterGUID))
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

        if (Is-NonNull $newWmiFilter) {

          $deList.Add($newWmiFilter) | Out-Null      

          DBG ('Save WMI Filter parameters')

          DBGSTART
          # http://gallery.technet.microsoft.com/scriptcenter/f1491111-9f5d-4c83-b436-537eca9e8d94

          $newWmiFilter.Put('msWMI-Name', $filterName)
          $newWmiFilter.Put('msWMI-ID', $filterGUID)
          #$newWmiFilter.Put('msWMI-Parm1', )
          $newWmiFilter.Put('msWMI-Parm2', $fullWqlFilter)
          $newWmiFilter.Put('msWMI-Author', 'ondrej@sevecek.com')
          #$newWmiFilter.Put('instanceType', 4)
          #$newWmiFilter.Put('showInAdvancedViewOnly', $true)
          $newWmiFilter.Put('msWMI-ChangeDate', $filterDate)
          $newWmiFilter.Put('msWMI-CreationDate', $filterDate)

          $adsiRs = $newWmiFilter.SetInfo()
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
          DBG ("ADSI Result: {0}" -f ($adsiRs | Out-String))
        }
      }
    }

    Dispose-List ([ref] $deList)
  }
}


function global:Import-WMIFilters ([string] $folder, [string] $domainDN = $global:thisComputerDomainDN)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $folder }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $folder) }
  
  if ((Is-ValidString $folder) -and (Test-Path $folder)) {

    DBG ('Going to import all WMI filters from: {0}' -f $folder)
    DBGSTART
    $wmiFilters = Get-ChildItem -Path $folder -Filter '*.MOF'
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Found WMI filters: {0}' -f (Get-CountSafe $wmiFilters))

    if ((Get-CountSafe $wmiFilters) -gt 0) {
    
      foreach ($oneWmiFilter in $wmiFilters) {
       
        Import-WMIFilter $oneWmiFilter.FullName $domainDN
      }
    }
  }
}


function global:Copy-Acl ([string] $sourcePath, [string] $targetPath)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $sourcePath }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $targetPath }

  #$sourcePath = 'C:\Program Files\Microsoft Office Servers\14.0\Data'
  #$targetPath = 'F:\SP-Data'

  if ((Is-ValidString $sourcePath) -and (Is-ValidString $targetPath)) {

    DBG ('Get source path ACL of the source path: {0}' -f $sourcePath)
    $sourceACL = Get-Acl $sourcePath -EV er -EA SilentlyContinue
    DBGER $MyInvocation.MyCommand.Name $er

    DBG ('Obtain new owner')
    $newOwner = New-Object System.Security.Principal.NTAccount('BUILTIN\Administrators') -EV er -EA SilentlyContinue
    DBGER $MyInvocation.MyCommand.Name $er

    DBG ('Change owner of the ACL')
    DBGSTART
    $sourceACL.SetOwner($acc)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Disable inheritance in the ACL')
    DBGSTART
    $sourceACL.SetAccessRuleProtection($true, $true)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Apply the ACL to the target path: {0}' -f $targetPath)
    Set-Acl $targetPath -AclObject $sourceACL -EV er -EA SilentlyContinue
    DBGER $MyInvocation.MyCommand.Name $er
  }
}


function global:Obtain-HttpSslBindings ([string[]] $debugNetshOutput)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  if ($debugNetshOutput.Length -lt 1) {

    DBG ('Give me the textual output from netsh')
    DBGSTART
    [string[]] $debugNetshOutput = @()
    Run-Process netsh 'http show sslcert' -refStdOut ([ref] $debugNetshOutput)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }

  [string[]] $netshOut = $debugNetshOutput | % { $_.Trim() } | ? { Is-ValidString $_ }
  
  [hashtable] $bindings = @{}

  [string] $rxIPPort = '\AIP:port[ ]+: (\d+\.\d+\.\d+\.\d+:\d+)\Z'
  [string] $rxHostnamePort = '\AHostname:port[ ]+: ([a-zA-Z0-9-\.]+:\d+)\Z'
  [string] $rxThumbprint = ': ([a-fA-F0-9]{40})\Z'
  [string] $rxAppId = ": (\{$global:rxGUID\})\Z"

  $i = 0
  while ($i -lt $netshOut.Length) {

   $oneBindingIP = $null
   $oneBindingHost = $null
   [int] $oneBindingPort = $null
   $oneBindingKey = $null
   $oneBindingNetshKey = $null

   if ($netshOut[$i] -match $rxIPPort) {

     $oneBindingKey = [regex]::Match($netshOut[$i], $rxIPPort).Groups[1].Value
     DBGIF ('Weird: {0}' -f $netshOut[$i]) { Is-EmptyString $oneBindingKey }
     $oneBindingTokens = $oneBindingKey.Split(':')
     DBGIF ('Weird: {0}' -f $netshOut[$i]) { $oneBindingTokens.Length -ne 2 }
     DBGIF ('Weird: {0}' -f $netshOut[$i]) { Is-EmptyString $oneBindingTokens[0] }
     DBGIF ('Weird: {0}' -f $netshOut[$i]) { Is-EmptyString $oneBindingTokens[1] }
     $oneBindingIP = $oneBindingTokens[0].Trim()
     DBGSTART
     $oneBindingPort = [int]::Parse($oneBindingTokens[1])
     DBGER $MyInvocation.MyCommand.Name $error
     DBGEND
     DBGIF ('Weird: {0}' -f $netshOut[$i]) { $oneBindingKey -ne ('{0}:{1}' -f $oneBindingIP, $oneBindingPort) }
     $oneBindingNetshKey = 'ipport={0}' -f $oneBindingKey
   }

   if ($netshOut[$i] -match $rxHostnamePort) {

     $oneBindingKey = [regex]::Match($netshOut[$i], $rxHostnamePort).Groups[1].Value
     DBGIF ('Weird: {0}' -f $netshOut[$i]) { Is-EmptyString $oneBindingKey }
     $oneBindingTokens = $oneBindingKey.Split(':')
     DBGIF ('Weird: {0}' -f $netshOut[$i]) { $oneBindingTokens.Length -ne 2 }
     DBGIF ('Weird: {0}' -f $netshOut[$i]) { Is-EmptyString $oneBindingTokens[0] }
     DBGIF ('Weird: {0}' -f $netshOut[$i]) { Is-EmptyString $oneBindingTokens[1] }
     $oneBindingHost = $oneBindingTokens[0].Trim()
     DBGSTART
     $oneBindingPort = [int]::Parse($oneBindingTokens[1])
     DBGER $MyInvocation.MyCommand.Name $error
     DBGEND
     DBGIF ('Weird: {0}' -f $netshOut[$i]) { $oneBindingKey -ne ('{0}:{1}' -f $oneBindingHost, $oneBindingPort) }
     $oneBindingNetshKey = 'hostnameport={0}' -f $oneBindingKey
   }

   if (Is-ValidString $oneBindingKey) {

     DBGIF ('Weird: {0}' -f $netshOut[$i + 1]) { $netshOut[$i + 1] -notmatch $rxThumbprint }
     $oneBindingThumbprint = [regex]::Match($netshOut[$i + 1], $rxThumbprint).Groups[1].Value
     DBGIF ('Weird: {0}' -f $netshOut[$i + 1]) { Is-EmptyString $oneBindingThumbprint }

     DBGIF ('Weird: {0}' -f $netshOut[$i + 2]) { $netshOut[$i + 2] -notmatch $rxAppId }
     DBGSTART
     $oneBindingAppId = [Guid] (([regex]::Match($netshOut[$i + 2], $rxAppId).Groups[1].Value))
     DBGER $MyInvocation.MyCommand.Name $error
     DBGEND
     DBGIF ('Weird: {0}' -f $netshOut[$i + 2]) { Is-EmptyString $oneBindingAppId.ToString() }

     $newBinding = New-Object PSObject
     Add-Member -Input $newBinding -MemberType NoteProperty -Name ip -Value $oneBindingIP
     Add-Member -Input $newBinding -MemberType NoteProperty -Name host -Value $oneBindingHost
     Add-Member -Input $newBinding -MemberType NoteProperty -Name port -Value $oneBindingPort
     Add-Member -Input $newBinding -MemberType NoteProperty -Name cert -Value $oneBindingThumbprint
     Add-Member -Input $newBinding -MemberType NoteProperty -Name appid -Value $oneBindingAppId
     Add-Member -Input $newBinding -MemberType NoteProperty -Name netshKey -Value $oneBindingNetshKey

     [void] $bindings.Add($oneBindingKey, $newBinding)

     $i += 2
   }

   $i ++
 }

 DBG ("Parsed out HTTP SSL bindings: {0} | -->`r`n{1}" -f $bindings.Keys.Count, ($bindings.Values | Select ip,host,port,cert,appid | ft -Auto | Out-String))
 DBGIF $MyInvocation.MyCommand.Name { ($netshOut.Length -gt 5) -and ($bindings.Keys.Count -lt 1) }

 DBGIF ('Weird input/output: lines = {0} | bindings = {1}' -f $netshOut.Length, $bindings.Keys.Count) { ($global:thisOSVersionNumber -lt 10) -and ($bindings.Keys.Count -ge 1) -and ((($netshOut.Length - 2) / 13) -ne $bindings.Keys.Count) }

 # Note: there is new "Reject Connections" setting on Windows 10+
 DBGIF ('Weird input/output on Win10+: lines = {0} | bindings = {1}' -f $netshOut.Length, $bindings.Keys.Count) { (($global:thisOSVersionNumber -ge 10) -and ($global:thisOSBuild -lt 17763)) -and ($bindings.Keys.Count -ge 1) -and ((($netshOut.Length - 2) / 14) -ne $bindings.Keys.Count) }

 # Note: there are also four new lines on Windows 2019
 #       Disable HTTP2
 #       Disable QUIC
 #       Disable TLS1.3
 #       Disable OCSP Stapling

 DBGIF ('Weird input/output on Win10+: lines = {0} | bindings = {1}' -f $netshOut.Length, $bindings.Keys.Count) { (($global:thisOSVersionNumber -ge 10) -and ($global:thisOSBuild -ge 17763)) -and ($bindings.Keys.Count -ge 1) -and ((($netshOut.Length - 2) / 18) -ne $bindings.Keys.Count) }

 return $bindings
}


function global:Ensure-HttpsCertificateBound ([string] $ip, [int] $port, [string] $hostHeader, [string] $webCert)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  # Note: this is [hashtable] in fact
  $httpsBindingsExisting = Obtain-HttpSslBindings
  
  [bool] $bindingAlreadyExists = $false
  
  DBG ('Some HTTPS bindins exist, parsing: {0}' -f (Get-CountSafe $httpsBindingsExisting.Values))
  if ((Get-CountSafe $httpsBindingsExisting.Values) -gt 0) {

    foreach ($oneHttpsBindingExisting in $httpsBindingsExisting.Values) {

      DBG ('Checking one HTTPS existing binding: ip = {0} | port = {1} | host = {2}' -f $oneHttpsBindingExisting.ip, $oneHttpsBindingExisting.port, $oneHttpsBindingExisting.host)

      if (
          (($oneHttpsBindingExisting.ip -eq $ip) -and ($oneHttpsBindingExisting.port -eq $port) -and (Is-EmptyString $oneHttpsBindingExisting.host)) -or 
          ((Is-EmptyString $oneHttpsBindingExisting.ip) -and ($oneHttpsBindingExisting.port -eq $port) -and ($oneHttpsBindingExisting.host -eq $hostHeader))
         )
      {
        $bindingAlreadyExists = $true
        DBG ('Binding really exists')
        DBGIF ('Different certificate bound: {0}:{1} | is = {2} | should = {3}' -f $ip, $port, $oneHttpsBindingExisting.cert, $webCert) { $oneHttpsBindingExisting.cert -ne $webCert } 
      }
    }
  }

  DBG ('The HTTPS certificate already bound: {0}' -f $bindingAlreadyExists)  
  if (-not $bindingAlreadyExists) {

    DBGIF ('Cannot use SNI binding on Windows 6.0 and older: {0} | {1} | {2}' -f $hostHeader, $port, $webCert) { (Is-ValidString $hostHeader) -and ($global:thisOSVersionNumber -lt 6.2) }

    if ((Is-EmptyString $hostHeader) -or ($global:thisOSVersionNumber -lt 6.2)) {

      if (Is-ValidString $hostHeader) {

        $ip = '0.0.0.0'
      }

      Run-Process netsh ('http add sslcert ipport={0}:{1} certhash={2} appId={{4dc3e181-e14b-4a21-b022-59fc669b0914}} certstore=my' -f $ip, $port, $webCert)

    } else {

      Run-Process netsh ('http add sslcert hostnameport={0}:{1} certhash={2} appId={{4dc3e181-e14b-4a21-b022-59fc669b0914}} certstore=my' -f $hostname, $port, $webCert)
    }
  }
}


function global:Clean-IIS ([string] $defaultWebSiteOperation, [string] $poolsOperation)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $defaultWebSiteOperation) -and ($defaultWebSiteOperation -ne 'stop') -and ($defaultWebSiteOperation -ne 'delete') -and ($defaultWebSiteOperation -ne $global:emptyValueMarker) }
  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $poolsOperation) -and ($poolsOperation -ne 'stop') -and ($poolsOperation -ne 'delete') -and ($poolsOperation -ne $global:emptyValueMarker) }

  DBG ('Start with enumerating the current objects')

  $currentSites = Get-IISSites
  $currentAppPools = Get-IISAppPools    
  DBG ('Enum current sites: {0}' -f ($currentSites | Out-String))
  DBG ('Enum current IIS apppools: {0}' -f ($currentAppPools | Out-String))

  if ((Is-ValidString $defaultWebSiteOperation) -and ($defaultWebSiteOperation -ne $global:emptyValueMarker)) {

    DBGIF $MyInvocation.MyCommand.Name { -not (Contains-Safe $currentSites 'Default Web Site') }
  
    DBG ('Handle Default Web Site: {0}' -f $defaultWebSiteOperation)
    Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('{0} site "Default Web Site" /commit:appHost' -f $defaultWebSiteOperation)
    #& $env:WINDIR\system32\inetsrv\appcmd.exe 'list' 'apppool' '/xml' | & $env:windir\system32\inetsrv\appcmd.exe 'delete' 'apppool' '/in'

    if ($defaultWebSiteOperation -eq 'stop') {

      DBG ('We are stopping the Default Web Site so come and make the setting permanent')
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe 'set site "Default Web Site" /serverAutoStart:"false"'
    }
  }    


  DBG ('Determine AppPools to be handled if any')
  [System.Collections.ArrayList] $poolsToHandle = @()

  if ((Is-ValidString $poolsOperation) -and ($poolsOperation -ne $global:emptyValueMarker)) {

    # Win6.x app pools first
    DBGIF $MyInvocation.MyCommand.Name { -not (Contains-Safe $currentAppPools 'DefaultAppPool') }
    [void] $poolsToHandle.Add('DefaultAppPool')

    DBGIF $MyInvocation.MyCommand.Name { -not (Contains-Safe $currentAppPools 'Classic .NET AppPool') }
    [void] $poolsToHandle.Add('Classic .NET AppPool')

    if (Contains-Safe $currentAppPools 'ASP.NET v4.0 Classic') {

      [void] $poolsToHandle.Add('ASP.NET v4.0 Classic')
    }

    if (Contains-Safe $currentAppPools 'ASP.NET v4.0') {

      [void] $poolsToHandle.Add('ASP.NET v4.0')
    }


    if ($global:thisOSVersionNumber -ge 6.2) {

      DBG ('Running on 6.2+ so we have to handle additional AppPools as well')
      # Win6.2 app pools then
    
      $aspnet20FeatureState = Get-WindowsFeature Web-Asp-Net
      $aspnet45FeatureState = Get-WindowsFeature Web-Asp-Net45

      [string[]] $aspnetAllFeatures = Get-WindowsFeature Web-Asp-Net* | sort Name | select -Expand Name
      DBGIF ('Unknown ASP.NET features installed: {0}' -f ($aspnetAllFeatures -join ',')) { ((Get-CountSafe $aspnetAllFeatures) -ne 2) -or (-not (Contains-Safe $aspnetAllFeatures 'Web-Asp-Net')) -or (-not (Contains-Safe $aspnetAllFeatures 'Web-Asp-Net45')) }

      if ($aspnet20FeatureState.Installed) {

        DBGIF $MyInvocation.MyCommand.Name { -not (Contains-Safe $currentAppPools '.NET v2.0 Classic') }
        [void] $poolsToHandle.Add('.NET v2.0 Classic')

        DBGIF $MyInvocation.MyCommand.Name { -not (Contains-Safe $currentAppPools '.NET v2.0') }
        [void] $poolsToHandle.Add('.NET v2.0')
      }

      if ($aspnet45FeatureState.Installed) {

        DBGIF $MyInvocation.MyCommand.Name { -not (Contains-Safe $currentAppPools '.NET v4.5 Classic') }
        [void] $poolsToHandle.Add('.NET v4.5 Classic')

        DBGIF $MyInvocation.MyCommand.Name { -not (Contains-Safe $currentAppPools '.NET v4.5') }
        [void] $poolsToHandle.Add('.NET v4.5')
      }
    }

    DBGIF $MyInvocation.MyCommand.Name { $poolsToHandle.Count -le 0 }
  }


  DBG ('Going to handle the following AppPools: {0} | {1} | {2}' -f $poolsToHandle.Count, $poolsOperation, ($poolsToHandle -join ','))

  if ($poolsToHandle.Count -gt 0) {

    foreach ($onePoolToHandle in $poolsToHandle) {

      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('{0} apppool "{1}" /commit:appHost' -f $poolsOperation, $onePoolToHandle)

      if ($poolsOperation -eq 'stop') {

        DBG ('We are stopping the app pools so come and make the setting permanent')
        Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set appPool "{0}" /autoStart:"false"' -f $onePoolToHandle)
      }
    }
  }
}


function global:Define-IISDnsAliases ([string[]] $dnsAliases)
{
  Define-DnsHostAliases $dnsAliases -addHosts $true -disableLoopbackCheck $true
}


function global:Define-DnsHostAliases ([string[]] $dnsAliases, [bool] $addHosts, [bool] $disableLoopbackCheck)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBG ('The requested DNS aliases: #{0} | {1}' -f (Get-CountSafe $dnsAliases), ($dnsAliases -join ','))
  DBG ('We have the following local computer names: {0}' -f ($global:thisComputerLocalMachineNames -join ','))

  if ((Get-CountSafe $dnsAliases) -gt 0) {

    [hashtable] $loopbackAliases = @{}
    foreach ($oneDNSAlias in $dnsAliases) { 

      $oneDNSAlias = $oneDNSAlias.Trim().Trim('.')
      DBG ('One DNS alias: {0}' -f $oneDNSAlias)

      if ((Is-ValidString $oneDNSAlias) -and (-not (Contains-Safe $global:thisComputerLocalMachineNames $oneDNSAlias))) {

        DBGIF ('Weird DNS alias: {0}' -f $oneDNSAlias) { ($oneDNSAlias -notmatch $global:rxDns) -or ($oneDNSAlias -eq $global:emptyValueMarker) }

        if ($disableLoopbackCheck) {

          #DBG ('Disable NTLM loopback check for DNS aliases: #{0} | {1}' -f (Get-CountSafe $dnsAliases), ($dnsAliases -join ','))
          #Set-RegistryValue 'HKLM:\SYSTEM\CurrentControlSet\Control\LSA' 'DisableLoopbackCheck' 1 DWord

          DBG ('Disable NTLM loopback check for: {0}' -f $oneDNSAlias)
          Add-BackConnectionHostName $oneDNSAlias
        }

        if ($addHosts) {

          DBG ('Add the DNS alias into list of updated HOSTS entries: {0}' -f $oneDNSAlias)
          $loopbackAliases.Add($oneDNSAlias, '127.0.0.1')
        }
      }
    }


    DBG ('Define loopback DNS names in HOSTS file: #{0} | {1}' -f $loopbackAliases.Keys.Count, ($loopbackAliases.Keys -join ','))
    if ($loopbackAliases.Keys.Count -gt 0) {

      Update-HostsFile $loopbackAliases
    }
  }
}


function global:Assert-NtfsFolderWithDacl ([string] $folderPath, [string] $dacl)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [string] $realPath = Resolve-VolumePath $folderPath
  if (-not (Test-Path -Literal $realPath)) {

    DBG ('Creating the not yet existing directory: {0}' -f $realPath)
    DBGSTART
    [void] (New-Item -Path $realPath -ItemType Directory)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGER
  }

  Apply-NtfsDacl -ntfs $realPath -dacl $dacl
}


function global:Is-AspNetIsapiRegisteredInIIS ([string] $installPath32, [string] $installPath64)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  <#
  DBG ('Test if ASPNET is registered within IIS with aspnet_regiis.exe: {0}' -f $version)
  DBGSTART 
  [IO.FileInfo[]] $aspnetisapiDLL32finds = Get-Item $env:WINDIR\Microsoft.NET\Framework\v$version.*\aspnet_filter.dll
  [IO.FileInfo[]] $aspnetisapiDLL64finds = Get-Item $env:WINDIR\Microsoft.NET\Framework64\v$version.*\aspnet_filter.dll
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $aspnetisapiDLL32finds) -ne 1 }
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $aspnetisapiDLL64finds) -ne 1 }

  if ((Is-NonNull $aspnetisapiDLL32finds) -and (Is-NonNull $aspnetisapiDLL64finds)) {

    [string] $aspnetisapiDLL32 = $aspnetisapiDLL32finds[0].FullName
    [string] $aspnetisapiDLL64 = $aspnetisapiDLL64finds[0].FullName
    DBG ('ASPNET ISAPI filters found: {0} | {1}' -f $aspnetisapiDLL32, $aspnetisapiDLL64)
  }
  #>

  DBGIF $MyInvocation.MyCommand.Name { ((Is-EmptyString $installPath32) -or (Is-EmptyString $installPath64)) }

  [bool] $correctIsapiRegistered = $false

  if ((Is-ValidString $installPath32) -and (Is-ValidString $installPath64)) {

    [string] $aspnetisapiDLL32 = Join-Path $installPath32 'aspnet_filter.dll'
    [string] $aspnetisapiDLL64 = Join-Path $installPath64 'aspnet_filter.dll'

    DBGIF ('ASPNET ISAPI x32 filter does not exist: {0}' -f $aspnetisapiDLL32) { -not (Test-Path $aspnetisapiDLL32) }
    DBGIF ('ASPNET ISAPI x64 filter does not exist: {0}' -f $aspnetisapiDLL64) { -not (Test-Path $aspnetisapiDLL64) }
 
    if ((Test-Path $aspnetisapiDLL32) -and (Test-Path $aspnetisapiDLL64)) {
    
      $isapiFiltersRegistered = $null
      $isapiFiltersRegistered = (Get-IISGenericXml 'config','-section:system.webServer/isapiFilters').SelectNodes('appcmd/CONFIG/system.webServer-isapiFilters/filter')
      DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $isapiFiltersRegistered) -lt 1 }
        
      [bool] $precondition32 = $false
      [bool] $precondition64 = $false

      if ((Get-CountSafe $isapiFiltersRegistered) -ge 1) {

        foreach ($oneIsapiFilterRegistered in $isapiFiltersRegistered) {
   
          DBG ('One ISAPI filter registered: {0} | {1}' -f $oneIsapiFilterRegistered.path, $oneIsapiFilterRegistered.preCondition)
          [string] $oneIsapiPath = [System.Environment]::ExpandEnvironmentVariables($oneIsapiFilterRegistered.path)
          if ($oneIsapiPath -eq $aspnetisapiDLL64) {

            $precondition64 = $true
            DBGIF ('Weird ISAPI filter precondition: {0}' -f $oneIsapiFilterRegistered.preCondition) { $oneIsapiFilterRegistered.preCondition -notlike '*bitness64*' }
            $correctIsapiRegistered = $true
          }

          if ($oneIsapiPath -eq $aspnetisapiDLL32) {

            $precondition32 = $true
            # Note: if the runtime is NetFx 1.1 it is 32bit from definition
            DBGIF ('Weird ISAPI filter precondition: {0}' -f $oneIsapiFilterRegistered.preCondition) { ($oneIsapiFilterRegistered.preCondition -notlike '*bitness32*') -and ($oneIsapiFilterRegistered.preCondition -notlike '*runtimeVersionv1.1*') }
            $correctIsapiRegistered = $true
          }
        }
     }

      DBGIF ('One ASPNET bit version not registered in IIS: x32 = {0} | x64 = {1}' -f $precondition32, $precondition64) { $precondition32 -ne $precondition64 }
    }
  }

  DBG ('The ASPNET version registration: {0}' -f $correctIsapiRegistered)
  return $correctIsapiRegistered
}


function global:Prepare-IISWebServer ([bool] $cleanIIS, [string] $iisLogPath, [string[]] $dnsAliases, [switch] $otherPathsFollowLogs = $true)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  if ($cleanIIS) {

    Clean-IIS 'delete' 'delete'
  }

  DBG ('Set default IIS logging parameters')
  Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe 'set config "/section:httpLogging" "/dontLog:False" "/selectiveLogging:LogAll"'
  Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe 'set config "/section:sites" "-siteDefaults.logFile.logExtFileFlags:Date,Time,ClientIP,UserName,SiteName,ComputerName,ServerIP,Method,UriStem,UriQuery,HttpStatus,Win32Status,BytesSent,BytesRecv,TimeTaken,ServerPort,UserAgent,Cookie,Referer,ProtocolVersion,Host,HttpSubStatus"'
  Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe 'set config "/section:sites" "-siteDefaults.logFile.logFormat:W3C"'

  DBG ('Add NLB-Host custom header to distinguish NLM members')
  # Note: APPCMD does not accept "NLB-Host" in double quotes, but it rather requires single apostrophes only
  Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config /section:httpProtocol /+customHeaders.[name=''NLB-Host'',value=''{0}'']' -f $global:thisComputerHost)


  if (Is-ValidString $iisLogPath) {  
  
    DBG ('Move IIS logs to: {0}' -f $iisLogPath)
    Assert-NtfsFolderWithDacl $iisLogPath 'F$SYSTEM|F$Administrators'
    Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "/section:sites" "-siteDefaults.logFile.Directory:{0}" /commit:apphost' -f (Resolve-VolumePath $iisLogPath))

    if ($otherPathsFollowLogs) {

      # Note: must use the [System.IO.Path]::GetDirectoryName() method because the Split-Path fails with our @1:\ disk moniker on PS 2.0
      [string] $iisLogsBase = '{0}\' -f ([System.IO.Path]::GetDirectoryName($iisLogPath).TrimEnd('\'))
      DBG ('Going to redirect all other IIS locations into the same base folder as the iisLogs: {0}' -f $iisLogsBase)
      DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $iisLogsBase }
      DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path (Resolve-VolumePath $iisLogsBase)) }

      # Note: must use the [System.IO.Path]::Combine() method because the Join-Path fails with our @1:\ disk moniker on PS 2.0
      [string] $iisFailedRequestTracing = [System.IO.Path]::Combine($iisLogsBase, 'IIS-Tracing')
      [string] $iisCompressionTemp = [System.IO.Path]::Combine($iisLogsBase, 'IIS-Compress')
      [string] $iisAppPoolsTemp = [System.IO.Path]::Combine($iisLogsBase, 'IIS-AppPools')
      [string] $iisAspTemp = [System.IO.Path]::Combine($iisLogsBase, 'IIS-AspTemp')
      [string] $iisAspNetTemp = [System.IO.Path]::Combine($iisLogsBase, 'IIS-AspNetTemp')

      DBG ('The following paths will be used for IIS: {0} | {1} | {2} | {3} | {4}' -f $iisFailedRequestTracing, $iisCompressionTemp, $iisAppPoolsTemp, $iisAspTemp, $iisAspNetTemp)

      Assert-NtfsFolderWithDacl $iisFailedRequestTracing 'F$SYSTEM|F$Administrators|M$IIS_IUSRS'
      Assert-NtfsFolderWithDacl $iisCompressionTemp 'F$SYSTEM|F$Administrators|F$IIS_IUSRS'
      Assert-NtfsFolderWithDacl $iisAppPoolsTemp 'F$SYSTEM|F$Administrators|X$IIS_IUSRS'
      Assert-NtfsFolderWithDacl $iisAspTemp 'F$Administrators|F$IIS_IUSRS'
      Assert-NtfsFolderWithDacl $iisAspNetTemp 'F$SYSTEM|F$Administrators|M$IIS_IUSRS|X$Users'

      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "/section:system.applicationHost/log" "-centralBinaryLogFile.directory:{0}" /commit:apphost' -f (Resolve-VolumePath $iisLogPath))
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "/section:system.applicationHost/log" "-centralW3CLogFile.directory:{0}" /commit:apphost' -f (Resolve-VolumePath $iisLogPath))
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "/section:sites" "-siteDefaults.traceFailedRequestsLogging.Directory:{0}" /commit:apphost' -f (Resolve-VolumePath $iisFailedRequestTracing))
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "/section:httpCompression" "-directory:{0}" /commit:apphost' -f (Resolve-VolumePath $iisCompressionTemp))
      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "/section:system.webServer/asp" "-cache.diskTemplateCacheDirectory:{0}" /commit:apphost' -f (Resolve-VolumePath $iisAspTemp))

      [bool] $aspnet2 = $false
      [bool] $aspnet4 = $false

      $netfxVersionInfo = Detect-NetFxVersions

      if ($netfxVersionInfo.v20EngineInstalled) {

        $aspnet2 = Is-AspNetIsapiRegisteredInIIS $netfxVersionInfo.v20path32 $netfxVersionInfo.v20path64

        if ($aspnet2) {

          Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "/section:compilation" "-tempDirectory:{0}" /commit:webroot /clr:2' -f (Resolve-VolumePath $iisAspNetTemp))
          Run-Process $env:WINDIR\syswow64\inetsrv\appcmd.exe ('set config "/section:compilation" "-tempDirectory:{0}" /commit:webroot /clr:2' -f (Resolve-VolumePath $iisAspNetTemp))
        }
      }

      if ($netfxVersionInfo.v40EngineInstalled) {

        $aspnet4 = Is-AspNetIsapiRegisteredInIIS $netfxVersionInfo.v40path32 $netfxVersionInfo.v40path64

        if ($aspnet4) {

          Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "/section:compilation" "-tempDirectory:{0}" /commit:webroot /clr:4' -f (Resolve-VolumePath $iisAspNetTemp))
          Run-Process $env:WINDIR\syswow64\inetsrv\appcmd.exe ('set config "/section:compilation" "-tempDirectory:{0}" /commit:webroot /clr:4' -f (Resolve-VolumePath $iisAspNetTemp))
        }
      }

      DBGIF $MyInvocation.MyCommand.Name { (-not $aspnet2) -and (-not $aspnet4) }

      DBG ('Set the WAS apppool temp path: {0}' -f $iisAppPoolsTemp)
      DBGSTART
      Set-ItemProperty -Path 'HKLM:\System\CurrentControlSet\Services\WAS\Parameters' -Name ConfigIsolationPath -Value (Resolve-VolumePath $iisAppPoolsTemp)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGER
    }
  }

  DBGIF 'The parameter dnsAliases is an array, not multivalue' { $dnsAliases -like '*|*' }
  Define-IISDnsAliases $dnsAliases
}


# Note: although this is very specific SP procedure, it fits into this module as a reusable function
function global:Prepare-SPWebServer ([string] $spVersion, [string[]] $dbAliasesAndServers, [bool] $encrypt, [bool] $cleanIIS, [string] $iisLogPath, [string[]] $dnsAliases, [string] $dataDir, [string] $spLogPath)
{
<#
.SYNOPSIS
Sample usage: Prepare-SPWebServer 2013 'spdb|data1.gopas.virtual\SPINTRANET','spfailover|data2.gopas.virtual\SPINTRANET' $true $true F:\IIS-Logs intranet.gopas.virtual,intranet F:\SP-Data F:\SP-Logs
#>

  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $spVersion }
  DBGIF $MyInvocation.MyCommand.Name { ($spVersion -ne '2010') -and ($spVersion -ne '2013') -and ($spVersion -ne '2016') }

  
  if (($spVersion -eq '2010') -or ($spVersion -eq '14')) { 
  
    $spVerID = '14.0'
    $spVerIDShort = '14'
  
  } elseif (($spVersion -eq '2013') -or ($spVersion -eq '15')) { 
  
    $spVerID = '15.0'
    $spVerIDShort = '15'
  
  } elseif (($spVersion -eq '2016') -or ($spVersion -eq '16')) { 
  
    $spVerID = '16.0'
    $spVerIDShort = '16'
  
  } else {

    DBGER ('Error, unsupported SharePoint version specified: {0}' -f $spVersion)
    #return
  }

  DBG ('Processing SharePoint version: {0} | {1} | {2}' -f $spVersion, $spVerID, $spVerIDShort)
  
  DBGSTART
  $spReallyInstalled = (Get-ItemProperty -Path ('HKLM:\Software\Microsoft\Shared Tools\Web Server Extensions\{0}' -f $spVerID) -Name 'SharePoint').SharePoint
  DBGEND
  # not yet present in registry before connect to DB
  #$spRealVersion = (Get-ItemProperty -Path ('HKLM:\Software\Microsoft\Shared Tools\Web Server Extensions\{0}' -f $spVerID) -Name 'Version' -EA SilentlyContinue).Version
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $spReallyInstalled }
  #DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $spRealVersion }
  #DBG ('SharePoint installation: {0} | {1}' -f $spReallyInstalled, $spRealVersion)
  DBG ('SharePoint installation: {0}' -f $spReallyInstalled)

  if (Is-ValidString $spReallyInstalled) {


  if ((Get-CountSafe $dbAliasesAndServers) -gt 0) {

    if ($encrypt) { $encryptInt = 1 } else { $encryptInt = 0 }

    DBG ('Set DB aliases: {0}' -f (Get-CountSafe $dbAliasesAndServers))
    foreach ($oneDbServer in $dbAliasesAndServers) { 

      DBGIF $MyInvocation.MyCommand.Name { (Count-MultiValue $oneDbServer) -ne 2 }
      
      $dbAlias = (Split-MultiValue $oneDbServer)[0]
      $dbServer = (Split-MultiValue $oneDbServer)[1]

      # This gets called much sooner than the SQL server must be supposed to be running
      #[void] (Test-SQL $dbServer -encrypt $encrypt)

      DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dbAlias }
      DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $dbServer }

      DBG ('Set DB alias: {0} = {1} | {2} = {3}' -f $dbAlias, $dbServer, $encrypt, $encryptInt)
      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\MSSQLServer\Client\ConnectTo' $dbAlias ('DBMSSOCN,{0}' -f $dbServer) String
      Set-RegistryValue 'HKLM:\SOFTWARE\Microsoft\MSSQLServer\Client\SuperSocketNetLib' 'Encrypt' $encryptInt DWord

      Set-RegistryValue 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\MSSQLServer\Client\ConnectTo' $dbAlias ('DBMSSOCN,{0}' -f $dbServer) String
      Set-RegistryValue 'HKLM:\SOFTWARE\Wow6432Node\Microsoft\MSSQLServer\Client\SuperSocketNetLib' 'Encrypt' $encryptInt DWord
    }
  }


  Prepare-IISWebServer -cleanIIS $cleanIIS -iisLogPath $iisLogPath -dnsAliases $dnsAliases


  DBG ('Correct C2WTS dependency')
  Run-Process 'sc' 'config c2wts "depend=" CryptSvc'


  DBG ('Pre-create local groups')
  Create-LocalObj 'group' 'WSS_WPG'
  Create-LocalObj 'group' 'WSS_ADMIN_WPG'
  Create-LocalObj 'group' 'WSS_RESTRICTED_WPG_V4'

  
  DBG ('Correct registry keys')
  # Server Search (registered with its service)
  Set-RegistryValue 'HKLM:\SYSTEM\CurrentControlSet\Services\VSS\Diag\OSearch14 VSS Writer'
  Set-RegistryValue 'HKLM:\SYSTEM\CurrentControlSet\Services\VSS\Diag\OSearch15 VSS Writer'
  Set-RegistryValue 'HKLM:\SYSTEM\CurrentControlSet\Services\VSS\Diag\OSearch16 VSS Writer'
  # Foundation Search (registered with its service)
  Set-RegistryValue 'HKLM:\SYSTEM\CurrentControlSet\Services\VSS\Diag\SPSearch4 VSS Writer'
  # SharePoint Foundation VSS Writer service 
  Set-RegistryValue 'HKLM:\SYSTEM\CurrentControlSet\Services\VSS\Diag\SharePoint Services Writer'

  Set-RegistryValue 'HKLM:\SYSTEM\CurrentControlSet\Control\SQMServiceList'
  

  DBG ('Correct registry permissions')
  Set-RegPermissions 'SYSTEM\CurrentControlSet\Services\VSS\Diag' 'WSS_WPG' 'QueryValues|SetValue|CreateSubKey|EnumerateSubKeys|Notify|Delete|ReadPermissions' 'None'
  Set-RegPermissions 'SYSTEM\CurrentControlSet\Services\VSS\Diag\OSearch14 VSS Writer' 'WSS_WPG'
  Set-RegPermissions 'SYSTEM\CurrentControlSet\Services\VSS\Diag\OSearch15 VSS Writer' 'WSS_WPG'
  Set-RegPermissions 'SYSTEM\CurrentControlSet\Services\VSS\Diag\OSearch16 VSS Writer' 'WSS_WPG'
  Set-RegPermissions 'SYSTEM\CurrentControlSet\Services\VSS\Diag\SPSearch4 VSS Writer' 'WSS_WPG'
  Set-RegPermissions 'SYSTEM\CurrentControlSet\Services\VSS\Diag\SharePoint Services Writer' 'WSS_ADMIN_WPG'


  if ($global:thisOSVersionNumber -gt 6.1) {

    # Note: on Windows 2012+ SYSTEM is owner and Users have Read access only
    Take-RegOwnership 'SYSTEM\CurrentControlSet\Control\SQMServiceList'
  }

  Set-RegPermissions 'SYSTEM\CurrentControlSet\Control\SQMServiceList' "Administrators"


  # MSI
  DBG ('Correct MSI DCOM launch and activation permissions')
  Take-RegOwnership 'Software\Classes\AppId\{000C101C-0000-0000-C000-000000000046}'
  Set-RegPermissions 'Software\Classes\AppId\{000C101C-0000-0000-C000-000000000046}' 'Administrators'
  Set-DCOMLaunchPermissions '{000C101C-0000-0000-C000-000000000046}' 'S-1-5-20' "NT AUTHORITY"
  Set-DCOMLaunchPermissions '{000C101C-0000-0000-C000-000000000046}' 'WSS_ADMIN_WPG' $null

  # IIS WAMREG Admin Service
  DBG ('Correct IIS WAMREG Admin Service launch and activation permissions')
  Take-RegOwnership 'Software\Classes\AppId\{61738644-F196-11D0-9953-00C04FD919C1}'
  Set-RegPermissions 'Software\Classes\AppId\{61738644-F196-11D0-9953-00C04FD919C1}' 'Administrators'
  Set-DCOMLaunchPermissions '{61738644-F196-11D0-9953-00C04FD919C1}' 'WSS_ADMIN_WPG' $null
  Set-DCOMLaunchPermissions '{61738644-F196-11D0-9953-00C04FD919C1}' 'WSS_WPG' $null

  # Storage Reports Service, {35B4B29E-0A6b-4ed7-b0a1-117bf912f497}
  # sp-search accesses the service for some unknown reason which produces the DCOM error
  DBG ('Correct Storage Reports Service launch and activation permissions')
  Take-RegOwnership 'Software\Classes\AppId\{35B4B29E-0A6b-4ed7-b0a1-117bf912f497}'
  Set-RegPermissions 'Software\Classes\AppId\{35B4B29E-0A6b-4ed7-b0a1-117bf912f497}' 'Administrators'
  Set-DCOMLaunchPermissions '{35B4B29E-0A6b-4ed7-b0a1-117bf912f497}' 'WSS_WPG' $null

  
  # Note: this is necessary in order to run APPCMD later such as listing the sites/apppools by sp-farm account 
  #       in order to update warmup paths
  [string] $inetsrvConfigPath = Join-Path $env:SystemRoot 'System32\inetsrv\Config'
  [string] $wssAdminWpgGroup = Get-SAMLogin 'WSS_ADMIN_WPG'
  DBG ('Grant WSS_ADMIN_WPG read access to the inetsrv config folder: {0} | {1}' -f $inetsrvConfigPath, $wssAdminWpgGroup)
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $inetsrvConfigPath) }
  Run-Process icacls ('"{0}" /Grant "{1}:(OI)(CI)R"' -f $inetsrvConfigPath, $wssAdminWpgGroup)


  if (Is-ValidString $dataDir) {

    DBG ('Grant WSS_WPG Read on data volume')

    $resolvedDataDir = Resolve-VolumePath $dataDir
    Run-Process 'icacls' ('{0}\ /Grant "WSS_WPG:R"' -f (Split-Path -Path $resolvedDataDir -Qualifier))
  }

  
  if (Is-ValidString $spLogPath) {

    $logDir = Resolve-VolumePath $spLogPath
    DBG ('Set log file location to use during farm join/provisioning: {0}' -f $logDir)
    Set-RegistryValue ('HKLM:\SOFTWARE\Microsoft\Shared Tools\Web Server Extensions\{0}\WSS' -f $spVerID) 'LogDir' $logDir String
    New-Item -Path (Resolve-VolumePath $logDir) -ItemType Directory -Force -EV er -EA SilentlyContinue | Out-Null
    DBGER $MyInvocation.MyCommand.Name $er 
  
    # Note: SharePoint Tracing runs under Local Service account
    Run-Process 'ICACLS' ('"{0}" /inheritance:r /grant "BUILTIN\Administrators:(OI)(CI)(F)"' -f $logDir)
    Run-Process 'ICACLS' ('"{0}" /grant "NT AUTHORITY\SYSTEM:(OI)(CI)(F)"' -f $logDir)
    Run-Process 'ICACLS' ('"{0}" /grant "NT AUTHORITY\Local Service:(OI)(CI)(F)"' -f $logDir)
  }

  
  $txnmFile = Join-Path $env:SystemDrive ('Program Files\Common Files\Microsoft Shared\Web Server Extensions\{0}\TEMPLATE\ControlTemplates\TaxonomyPicker.ascx' -f $spVerIDShort)
  DBG ('Resolve TaxonomyPicker error if the bug exists in: {0}' -f $txnmFile)
  
  if (Test-Path $txnmFile) {

    DBG ('TaxonomyPicker.ascx found, test if bug exists')

    $txnmToFind = 'Microsoft.SharePoint.Portal.WebControls.TaxonomyPicker,Microsoft.SharePoint.Portal'
    $txnmReplaceWith = 'Microsoft.SharePoint.Portal.WebControls.TaxonomyPicker,Microsoft.SharePoint.Portal'

    DBG ('Loading TaxonomyPicker.ascx file, exists: {0}' -f $txnmFile)
    $txnm = Get-Content -Path $txnmFile -EV er -EA SilentlyContinue
    DBGER $MyInvocation.MyCommand.Name $er

    $i = 0
    while (($i -lt $txnm.Count) -and (-not $txnm[$i].Contains($txnmToFind))) { $i ++ }

    if ($i -lt $txnm.Count)
    {
      DBG ('Errorneous line found, replacing')
      $txnm[$i] = $txnm[$i].Replace($txnmToFind, $txnmReplaceWith)

      DBG ('Create backup copy into: {0}' -f "$txnmFile.bak")
      Copy-Item -Path $txnmFile "$txnmFile.bak" -Force -EV er -EA SilentlyContinue | Out-Null
      DBGER $MyInvocation.MyCommand.Name $er

      DBG ('Save new content into original TaxonomyPicker.ascx')
      $txnm | Set-Content -Path $txnmFile -Force -EV er -EA SilentlyContinue
      DBGER $MyInvocation.MyCommand.Name $er
    }
  
  } else {

    DBG ('TaxonomyPicker.ascx does not exist. We are probably coping with SharePoint Foundation or SharePoint 2013 version.')
  }

  DBGSTART
  $spRealDataPath = (Get-ItemProperty -Path ('HKLM:\Software\Microsoft\Shared Tools\Web Server Extensions\{0}\WSS' -f $spVerID) -Name 'DataPath').DataPath
  DBGEND
  DBG ('Current/supplied SP data path: {0} | {1}' -f $spRealDataPath, $resolvedDataDir)
  DBGIF $MyInvocation.MyCommand.Name { $spRealDataPath -ne $resolvedDataDir }

  }
}


function global:Open-SQLDatabase ([string] $sqlServer, [string] $database)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [object] $sqlDatabase = $null
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $database }

  if (Is-ValidString $database) {

    DBG ('Connect to the SQL server: {0}' -f $sqlServer)
    DBGSTART
    $sqlSrvConnection = $null
    $sqlSrvConnection = New-Object Microsoft.SqlServer.Management.Smo.Server $sqlServer
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBGIF $MyInvocation.MyCommand.Name { Is-Null $sqlSrvConnection }

    if (Is-NonNull $sqlSrvConnection) {

      DBG ('List all current databases in the server: {0}' -f (Format-MultiValue ($sqlSrvConnection.Databases | Select-Object -Expand Name)))

      DBG ('Open the SQL database first: {0}' -f $database)
      DBGSTART
      $sqlDatabase = $sqlSrvConnection.Databases[$database]
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

    }
  }

  DBG ('Database opened: {0} | model = {1} | status = {2} | primaryFile = {3}' -f $sqlDatabase.Name, $sqlDatabase.RecoveryModel, $sqlDatabase.Status, $sqlDatabase.PrimaryFilePath)
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $sqlDatabase }
  return $sqlDatabase
}


function global:Execute-NonQuery ([object] $database, [string] $nonQuery)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $database }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $nonQuery }

  if ((Is-NonNull $database) -and (Is-ValidString $nonQuery)) {
  
    DBG ('Database specified: name = {0} | dboLogin = {1} | userName = {2} | isAccessible = {3} | owner = {4}' -f $database.Name, $database.DboLogin, $database.UserName, $database.IsAccessible, $database.Owner)

    DBG ('Executing non-query: {0}' -f $nonQuery)
    DBGSTART
    $database.ExecuteNonQuery($nonQuery)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }
}


function global:Execute-NonQueryRemote ([string] $sqlServer, [string] $database, [string] $nonQuery, [string] $login, [string] $domain, [string] $password)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $sqlServer }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $database }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $nonQuery }

  if ((Is-ValidString $sqlServer) -and (Is-ValidString $database) -and (Is-ValidString $nonQuery)) {

    [IntPtr] $accessToken = 0
    [System.Security.Principal.WindowsImpersonationContext] $impersonationContext = $null

    if (Is-ValidString $login) {

      Define-Win32Api

      [string] $samLogin = Get-SAMLogin $login $domain
      [string] $netbiosDomain = $null
      
      if ($samLogin -like '*\*') {
      
        $netbiosDomain = $samLogin.Substring(0, $samLogin.IndexOf('\'))
        $samLogin = $samLogin.Substring(($samLogin.IndexOf('\') + 1))
      }

      DBG ('Login and impersonate the arbitrary user account: {0} | {1} | {2} | {3}' -f $login, $domain, $samLogin, $netbiosDomain)
      DBGSTART
      # Note: LOGON32_LOGON_NEW_CREDENTIALS = 9
      #       LOGON32_PROVIDER_WINNT50 = 3
      # Note: not that the function would require SAM login and NetBIOS domain to be used itself
      #       but the following SqlConnection class fails if the NEW_CREDENTIALS is not in this format
      [bool] $resBool = [Sevecek.Win32Api.ADVAPI32]::LogonUser($samLogin, $netbiosDomain, $password, 9, 3, ([ref] $accessToken))
      [int] $lastError = [System.Runtime.InteropServices.Marshal]::GetLastWin32Error()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBGIF ('Logon user failed: {0} (0x{0:X8})' -f $lastError) { (-not $resBool) -or (-not (Is-ValidHandle $accessToken)) }

      if ($resBool) {

        DBG ('Impersonate the custom user account')
        DBGSTART
        $impersonationContext = [System.Security.Principal.WindowsIdentity]::Impersonate($accessToken)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }
    }

    $connectionString = 'Server = {0}; Database = {1}; Integrated Security = True' -f $sqlServer, $database
    DBG ('Will use the connection string: {0}' -f $connectionString)
    DBGSTART
    $connection = New-Object -TypeName System.Data.SqlClient.SqlConnection($connectionString)
    $connection.Open()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Upload the query: {0}' -f $nonQuery)
    DBGSTART
    $command = New-Object System.Data.SqlClient.SqlCommand($nonQuery, $connection)
    $intRes = $command.ExecuteNonQuery()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('Return value: {0}' -f $intRes)
    DBGIF $MyInvocation.MyCommand.Name { $intRes -ne -1 }

    DBG ('Finalize the SQL connection')
    DBGSTART
    $command.Dispose()
    $connection.Close()
    $connection.Dispose()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    if (Is-NonNull $impersonationContext) {

      DBG ('Revert to self')
      DBGSTART
      $impersonationContext.Undo()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }

    if (Is-ValidHandle $accessToken) {

      [Collections.ArrayList] $handleList = @()
      [void] $handleList.Add($accessToken)
      Dispose-Handles ([ref] $handleList)
    }
  }
}


function global:Execute-ScalarString ([string] $sqlServer, [string] $database, [string] $query)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $sqlServer }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $database }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $query }

  [string] $stringRes = [string]::Empty

  if ((Is-ValidString $sqlServer) -and (Is-ValidString $database) -and (Is-ValidString $query)) {

    $connectionString = 'Server = {0}; Database = {1}; Integrated Security = True' -f $sqlServer, $database
    DBG ('Will use the connection string: {0}' -f $connectionString)
    DBGSTART
    $connection = New-Object -TypeName System.Data.SqlClient.SqlConnection($connectionString)
    $connection.Open()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Upload the query: {0}' -f $query)
    DBGSTART
    $command = New-Object System.Data.SqlClient.SqlCommand($query, $connection)
    $stringRes = $command.ExecuteScalar()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('Return value: {0}' -f $stringRes)

    DBG ('Finalize the SQL connection')
    DBGSTART
    $command.Dispose()
    $connection.Close()
    $connection.Dispose()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  }

  return $stringRes
}


function global:Wait-HyperVJob ([ref] $wmiResultRef, [string] $resultMemberIfSynchronous, [string] $resultClassIfAsynchronous )
# $wmiResult either contains ReturnValue = 4096 and the .Job referencing the asynchronous job
# or has the $resultMemberIfSynchronous with a synchronous result of the operation immediatelly
# So if we need to wait, the operation result will appear once the job finishes and will be just associated
# to the job.
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $wmiResult = $wmiResultRef.Value
  DBG ('Waiting for job with result: {0} | {1}' -f $wmiResult.ReturnValue, $global:hypervErrorCodes[([int] $wmiResult.ReturnValue)])
  
  $job = $null
    
  $startTime = Get-Date
  $timeout = $false

  if ($wmiResult.ReturnValue -eq 4096) {

    $job = [WMI] $wmiResult.Job

    # 2 = New, 3 = Starting, 4 = Running, 5 = Suspended, 6 = Shutting down
    # 7 = Completed, 8 = Terminated, 9 = Killed, 10 = Exception, 11 = Service
    while (($job.JobState -ge 2 -and $job.JobState -le 6) -and (-not $timeout)) { 
    
      Start-Sleep -Milliseconds 375; $job.Get()

      if (((Get-Date) - $startTime).TotalMinutes -ge 9) {

        $timeout = $true
        break
      }
    }
  }

  DBGIF 'Job took too long to end. Timout' { $timeout }

  if ($job -ne $null) {

    $job.Get()
    DBG ('Job finished with state: {0} | {1} | {2} | {3} | error = {4} | completion = {5} | {6} | start = {7} | elapsed = {8}' -f $job.JobState, (Get-EnumString $global:hypervJobStates $job.JobState), (Format-MultiValue $job.StatusDescriptions), $job.ErrorDescription, $job.ErrorCode, $job.JobCompletionStatusCode, $job.Status, $job.StartTime, $job.ElapsedTime)
    
    if ((Is-ValidString $resultMemberIfSynchronous) -and (Is-ValidString $resultClassIfAsynchronous)) {

      DBG ('Get associated job result collection')
      DBGSTART
      $jobResultCol = $null
      $jobResultCol = $job.GetRelated($resultClassIfAsynchronous)
      DBGEND

      if (Is-NonNull $jobResultCol) {

        # This is by design: ManagementObjectCollection class does not have an indexer
        # http://connect.microsoft.com/PowerShell/feedback/details/339557/ctp-unable-to-index-into-an-object-of-type-system-management-managementobjectcollection
        $jobResult = (@($jobResultCol))[0]

        DBG ('Update wmiResult with the associated job result: {0}' -f $jobResult.__PATH)
        if (Is-Null (Get-Member -InputObject $wmiResult -Name $resultMemberIfSynchronous)) {
      
          DBG ('Result member does not exist, adding: {0}' -f $resultMemberIfSynchronous)
          $wmiResult | Add-Member -MemberType NoteProperty -Name $resultMemberIfSynchronous -Value $jobResult
           
        } else {

          DBG ('Result member exists, updating: {0}' -f $resultMemberIfSynchronous)
          $wmiResult.$resultMemberIfSynchronous = $jobResult
        }
      }
    }
  }

  else {

    $job = New-Object PSObject
    $job | Add-Member -type NoteProperty -Name JobState -Value 7
    $job | Add-Member -type NoteProperty -Name ErrorCode -Value $wmiResult.ReturnValue

    DBG ('Job finished without waiting.')
  }

  if (($job.ErrorCode -ne 0) -and ($job.JobCompletionStatusCode -eq 2147943395)) {
   
    # Note: Failed to mount the virtual disk. 
    #       The system failed to mount 'G:\MACHINES-goc173\NPS-ADFS\NPS-ADFS-HDD00.vhd': 
    #       The I/O operation has been aborted because of either a thread exit or an application request. (0x800703E3).
    DBGIF ('Retriable Hyper-V job error: {0} | {1} | {2:X8} = {3}' -f $job.Name, $job.ErrorCode, $job.JobCompletionStatusCode, $job.JobCompletionStatusCode) { $true }
  
  } else {

    DBGIF ('Hyper-V job error: {0} | {1} | {2:X8} = {3}' -f $job.Name, $job.ErrorCode, $job.JobCompletionStatusCode, $job.JobCompletionStatusCode) { $job.ErrorCode -ne 0 }
  }

  return $job
}


function global:Replace-DomainDNinLDIF ([string] $srcLDF, [string] $outLDF, [string] $toDN)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $srcLDF) }

  DBGSTART
  [System.Collections.ArrayList] $list = Get-Content $srcLDF -EA SilentlyContinue
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND

  [System.Collections.ArrayList] $outList = @()

  DBG ('Source LDF file contains lines: {0}' -f $list.Count)

  $i = 0
  while ($i -lt $list.Count) {

    [string] $oneLine = $list[$i]
    $i ++

    if (($oneLine -like 'member:: *') -or ($oneLine -like 'dn:: *') -or ($oneLine -like 'member: *') -or ($oneLine -like 'dn: *')) {

      $attrType = $oneLine.SubString(0, $oneLine.IndexOf(':'))

      # LDIF/LDF format continues lines if they start with either a single space or a single tab character
      while (($i -lt $list.Count) -and (($list[$i] -like " *") -or ($list[$i] -like "`t*"))) {

        $oneLine += $list[$i].SubString(1)
        $i ++
      }

      if ($oneLine -like '*:: *') {

        $decodedPrincipal = [System.Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($oneLine.SubString($oneLine.IndexOf(':: ') + 3)))
        $decodedBase64 = $true
      
      } else {

        $decodedPrincipal = $oneLine.SubString($oneLine.IndexOf(': ') + 2)
        $decodedBase64 = $false
      }
    
      if ($decodedPrincipal -like "*,DC=*") {

        $newPrincipal = $decodedPrincipal.SubString(0, $decodedPrincipal.IndexOf('DC=', [StringComparison]::CurrentCultureIgnoreCase)) + $toDN

      } else {

        $newPrincipal = $decodedPrincipal
      }

      if ($decodedBase64) {

        $newPrincipal = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($newPrincipal))
        $shouldAdd = '{0}:: {1}' -f $attrType, $newPrincipal

      } else {

        $shouldAdd = '{0}: {1}' -f $attrType, $newPrincipal
      }

      [void] $outList.Add($shouldAdd)

    } else {

      [void] $outList.Add($oneLine)
    }
  }

  DBG ('Saving result file: {0}' -f $outLDF)
  DBGSTART
  Set-Content -Path $outLDF -Value $outList -Force -Encoding ASCII
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
}


function global:Grant-SQLLogin ([string] $sqlServer, [string] $domainLogin)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domainLogin }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $sqlServer }

  [Microsoft.SqlServer.Management.Smo.Login] $outLogin = $null

  if ((Is-ValidString $sqlServer) -and (Is-ValidString $domainLogin)) {

    $sqlLogin = Get-SAMLogin $domainLogin

    if (Is-ValidString $sqlLogin) {

      DBG ('Connect to the SQL server: {0}' -f $sqlServer)
      DBGSTART
      $sqlSrvConnection = $null
      $sqlSrvConnection = New-Object Microsoft.SqlServer.Management.Smo.Server $sqlServer
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBGIF $MyInvocation.MyCommand.Name { Is-Null $sqlSrvConnection }

      if (Is-NonNull $sqlSrvConnection) {

        DBG ('Check if the login exists: {0}' -f (($sqlSrvConnection.Logins | select -Expand Name) -join ','))
        DBGSTART
        $oneLoginExisting = $null
        $oneLoginExisting = $sqlSrvConnection.Logins[$sqlLogin]
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND

        if (Is-Null $oneLoginExisting) {
          
          DBG ('Obtain login: {0}' -f $sqlLogin)
          DBGSTART
          $newLogin = New-Object Microsoft.SqlServer.Management.Smo.Login ($sqlSrvConnection, $sqlLogin)
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND

          DBG ('Create login because it does not exist: {0}' -f $sqlLogin)
          DBGSTART
          $newLogin.LoginType = 'WindowsUser'
          #$oneLogin.LoginType = 'WindowsGroup'
          $newLogin.Create()
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND

          $outLogin = $newLogin
          
        } else {

          DBG ('Login already exists: {0} | disabled = {1}' -f $oneLoginExisting.Name, $oneLoginExisting.IsDisabled)
          $outLogin = $oneLoginExisting
        }
      }
    }
  }

  DBG ('Returning SQL login: {0} | disabled = {1} | {2} | {3}' -f $outLogin.Name, $outLogin.IsDisabled, $outLogin.LoginType, $outLogin.State)
  return $outLogin
}



function global:Grant-SQLLoginIntoDatabase ([string] $sqlServer, [string] $database, [string] $domainLogin, [string] $access)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $database }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $domainLogin }

  if ((Is-ValidString $database) -and (Is-ValidString $domainLogin)) {

    $sqlLogin = Grant-SQLLogin $sqlServer $domainLogin

    DBG ('Connect to the SQL server: {0}' -f $sqlServer)
    DBGSTART
    $sqlSrvConnection = $null
    $sqlSrvConnection = New-Object Microsoft.SqlServer.Management.Smo.Server $sqlServer
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBGIF $MyInvocation.MyCommand.Name { Is-Null $sqlSrvConnection }

    if (Is-NonNull $sqlSrvConnection) {

      DBG ('List all current databases in the server: {0}' -f (Format-MultiValue ($sqlSrvConnection.Databases | Select-Object -Expand Name)))

      DBG ('Open the SQL database first: {0}' -f $database)
      DBGSTART
      $sqlDatabase = $sqlSrvConnection.Databases[$database]
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBGIF $MyInvocation.MyCommand.Name { Is-Null $sqlDatabase }

      if (Is-NonNull $sqlDatabase) {

        DBG ('Database opened: {0} | model = {1} | status = {2} | primaryFile = {3}' -f $sqlDatabase.Name, $sqlDatabase.RecoveryModel, $sqlDatabase.Status, $sqlDatabase.PrimaryFilePath)

        DBG ('Current DB users existing: #{0} | {1} | exists = {2}' -f (Get-CountSafe $sqlDatabase.Users), (($sqlDatabase.Users | select -Expand Name) -join ','), $sqlDatabase.Users.Contains($sqlLogin.Name))

        if (-not $sqlDatabase.Users.Contains($sqlLogin.Name)) {

          DBG ('Create the associated user in the database: {0}' -f $sqlLogin.Name)
          DBGSTART
          $newSqlUser = New-Object Microsoft.SqlServer.Management.Smo.User ($sqlDatabase, $sqlLogin.Name)
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND

          DBGIF $MyInvocation.MyCommand.Name { Is-Null $newSqlUser }

          DBG ('Set the basic parameters and create')
          DBGSTART
          $newSqlUser.Login = $sqlLogin.Name
          $newSqlUser.Create()
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND

        } else {

          DBG ('User already exists')
          DBGSTART
          $newSqlUser = $sqlDatabase.Users[$sqlLogin.Name]
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND
        }

        DBG ('DB user obtained: {0} | {1} | {2}' -f $newSqlUser.Login, $newSqlUser.Name, $newSqlUser.Urn)

        if ($newSqlUser.State -eq 'Existing') {

          DBG ('Grant the newly created user some access: {0}' -f $access)
        
          foreach ($oneAccess in (Split-MultiValue $access)) {

            $oneRoleOrSchema = Strip-ValueFlags $oneAccess

            if (Has-ValueFlags $oneAccess 'R') {
        
              DBG ('Will grant DB role: {0}' -f $oneRoleOrSchema)
              DBGSTART
              $sqlDatabase.Roles[$oneRoleOrSchema].AddMember($newSqlUser.Name)
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND

            } elseif (Has-ValueFlags $oneAccess 'S') {

              DBG ('Will grant schema: {0}' -f $oneRoleOrSchema)
            }
          }
          
        } else {

          DBG ('The user was not created. Skipping.')
        }
      }
    }
  }
}


function global:Get-IISGenericXml ([string[]] $what)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [XML] $outXml = $null

  $appCmdPath = "$env:SystemRoot\System32\InetSrv\AppCmd.exe"

  [string] $appCmdOutput = $null
  Run-Process $appCmdPath ('LIST {0} /xml' -f ($what -join ' ')) -refStdOut ([ref] $appCmdOutput)

  DBG ('Convert the AppCmd output to XML')
  DBGSTART
  $outXml = [XML] $appCmdOutput
  DBGER $MyInvocation.MyCommand.Name $error
  DBGEND
  
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $outXml }
  DBGIF $MyInvocation.MyCommand.Name { Is-Null $outXml.AppCmd }
  #DBGIF $MyInvocation.MyCommand.Name { Is-Null $outXml.AppCmd.$what } // the requested element may not be present at all, such as "no web site exists"

  return $outXml
}


function global:Get-IISGenericList ([string[]] $what)
{
  [System.Collections.ArrayList] $returnItems = @()
  
  $itemsXML = Get-IISGenericXml $what
  $returnItems += $itemsXML.AppCmd.($what[0]) | Select-Object -ExpandProperty ('{0}.name' -f $what[0])

  DBG ('Found IIS items: {0} | {1} | {2}' -f $what[0], $returnItems.Count, ($returnItems -join ';'))
<#
  if ((Get-CountSafe $itemStrings) -gt 0) {

    foreach ($oneItem in $itemStrings) {

      if ($oneItem -like ('{0} "*" (*)' -f $what)) {

        $leftTrim = $oneItem.SubString($what.Length + 2)
        $rightTrim = $leftTrim.SubString(0, $leftTrim.IndexOf('"'))

        $validationExitCode = Run-Process $appCmdPath ('LIST {0} "{1}"' -f $what, $rightTrim) -returnExitCode $true

        if ($validationExitCode -eq 0) {

          [void] $returnItems.Add($rightTrim)
        }

      } else {

        DBGIF ('Take note that the APPCMD LIST {0} returned an invalid output line: {1}' -f $what, $oneItem) { $true }
      }
    }
  }
#>

  return ,$returnItems
}


function global:Get-IISAppPools ([bool] $structured)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $what = 'APPPOOL'

  if (-not $structured) {

    return (Get-IISGenericList $what)
  
  } else {

    $appCmdXml = Get-IISGenericXml $what

    [hashtable] $output = @{}

    foreach ($oneItem in $appCmdXml.appcmd.$what) {

      $newItem = New-Object PSObject
      Add-Member -Input $newItem -MemberType NoteProperty -Name name -Value $oneItem."$what.NAME"
      Add-Member -Input $newItem -MemberType NoteProperty -Name pipe -Value $oneItem.PipelineMode
      Add-Member -Input $newItem -MemberType NoteProperty -Name netfx -Value $oneItem.RuntimeVersion
      Add-Member -Input $newItem -MemberType NoteProperty -Name state -Value $oneItem.state

      DBGIF ('Weird apppool state: {0} | {1}' -f $newItem.name, $newItem.state) { ($newItem.state -ne 'Started') -and ($newItem.state -ne 'Stopped') }
      DBGIF ('No pipe: {0}' -f $newItem.name) { Is-EmptyString $newItem.pipe }
      DBGIF ('No netfx: {0}' -f $newItem.name) { Is-EmptyString $newItem.netfx }

      [void] $output.Add($newItem.name, $newItem)
    }

    return $output
  }
}


function global:Get-IISSites ([bool] $structured)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $what = 'SITE'

  if (-not $structured) {

    return (Get-IISGenericList $what)

  } else {

    $appCmdXml = Get-IISGenericXml $what

    [hashtable] $output = @{}

    foreach ($oneItem in $appCmdXml.appcmd.$what) {

      $newItem = New-Object PSObject
      Add-Member -Input $newItem -MemberType NoteProperty -Name name -Value $oneItem."$what.NAME"
      Add-Member -Input $newItem -MemberType NoteProperty -Name id -Value $oneItem."$what.ID"
      Add-Member -Input $newItem -MemberType NoteProperty -Name bindings -Value $oneItem.bindings
      Add-Member -Input $newItem -MemberType NoteProperty -Name state -Value $oneItem.state

      DBGIF ('Weird site state: {0} | {1}' -f $newItem.name, $newItem.state) { ($newItem.state -ne 'Started') -and ($newItem.state -ne 'Stopped') }
      DBGIF ('No id: {0}' -f $newItem.name) { Is-EmptyString $newItem.id }
      DBGIF ('No bindings: {0}' -f $newItem.name) { Is-EmptyString $newItem.bindings }

      [void] $output.Add($newItem.name, $newItem)
    }

    return $output
  }
}


function global:Create-IISAppPool ([string] $name, [string] $netfxVersion, [string] $account, [string] $password, [string] $loadProfile)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  if (-not (Contains-Safe (Get-IISAppPools) $name)) {

    Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('add apppool /name:"{0}"' -f $name)
      
    DBG ('Restart Application Host Helper Service (apphostsvc) to be able to apply NTFS ACL with IIS APPPOOL identity')
    #Run-Process iisreset '/restart /timeout:60 /status'
    Restart-Service AppHostSvc -Force -EV er -EA SilentlyContinue
    DBGER $MyInvocation.MyCommand.Name $er 
  
  } else {

    DBG ('The IIS AppPool already exists. Will not create: {0}' -f $name)
  }

  
  DBG ('Should we set netfx version for the pool: {0} | {1}' -f (Is-ValidString $netfxVersion), $netfxVersion)
  if (Is-ValidString $netfxVersion) {

    DBGIF $MyInvocation.MyCommand.Name { $netfxVersion -notlike 'v?*.?*' }
    Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set apppool "{0}" /managedRuntimeVersion:{1}' -f $name, $netfxVersion)
  }


  DBG ('Should we change apppool account: {0} | {1}' -f (Is-ValidString $account), $account)
  if (Is-ValidString $account) {

    [string] $accountType = 'SpecificUser'

    switch ($account) {

      'LocalSystem' { $accountType = 'LocalSystem' }
      'System' { $accountType = 'LocalSystem' }
      'NT AUTHORITY\System' { $accountType = 'LocalSystem' }

      'LocalService' { $accountType = 'LocalService' }
      'Local Service' { $accountType = 'LocalService' }
      'Service' { $accountType = 'LocalService' }
      'NT AUTHORITY\LocalService' { $accountType = 'LocalService' }
      'NT AUTHORITY\Local Service' { $accountType = 'LocalService' }

      'NetworkService' { $accountType = 'NetworkService' }
      'Network Service' { $accountType = 'NetworkService' }
      'Service' { $accountType = 'NetworkService' }
      'NT AUTHORITY\NetworkService' { $accountType = 'NetworkService' }
      'NT AUTHORITY\Network Service' { $accountType = 'NetworkService' }
    }

    if ($accountType -ne 'SpecificUser') {

      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set apppool "{0}" /processModel.identityType:"{1}"' -f $name, $accountType)

    } else {

      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set apppool "{0}" /processModel.identityType:"{1}" /processModel.userName:"{2}" /processModel.password:"{3}"' -f $name, $accountType, $account, $password)
    }
  }

  DBG ('Should we change the loadUserProfile setting: {0} | {1}' -f (Is-ValidString $loadProfile), $loadProfile)
  if (Is-ValidString $loadProfile) {

    Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set apppool "{0}" /processModel.loadUserProfile:"{1}"' -f $name, $loadProfile)
  }
}


function global:Create-IISSite ([string] $name, [string] $path, [string] $ntfsPermissions, [string] $binding, [string] $appPool, [bool] $authWinState, [bool] $authAnonState, [bool] $authBasicState, [bool] $authDigestState, [bool] $authDisableWinKernel, [string] $compressionStates, [string] $netfxVersion, [string] $clientCache, [string] $appPoolAccount, [string] $appPoolPwd, [string] $loadProfile, [bool] $sni)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $name }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $path }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $binding }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $appPool }
  DBGIF $MyInvocation.MyCommand.Name { $binding -notlike 'http*/*:*:*' }

  DBG ('New IIS web site to be added: {0}' -f $name, $appPool, $path)

  DBG ('Start with a new appPool: {0} | {1} | account = {2}' -f $appPool, $netfxVersion, $appPoolAccount)
  Create-IISAppPool $appPool $netfxVersion $appPoolAccount $appPoolPwd -loadProfile $loadProfile


  DBG ('Apply the most restrictive NTFS permissions possible')
  
  if ($global:thisOSVersionNumber -eq 6) {

    Create-NTFSFolderAndShare $path $null ('R$NT AUTHORITY\Network Service|{0}' -f $ntfsPermissions) $null $false

  } else {

    Create-NTFSFolderAndShare $path $null ('R$IIS APPPool\{0}|{1}' -f $appPool, $ntfsPermissions) $null $false
  }

    
  DBG ('Create and configure the site')

  if (-not (Contains-Safe (Get-IISSites) $name)) {

    Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('add site /name:"{0}" /physicalPath:"{1}" /bindings:"{2}"' -f $name, $path, $binding)

    DBG ('Should we configure SNI on the bindings: {0}' -f $sni)
    DBGIF ('Cannot enable SNI on older systems') { $sni -and ($global:thisOSVersionNumber -lt 6.2) }
    
    if ($sni -and ($global:thisOSVersionNumber -ge 6.2)) {

      [string[]] $bindings = $binding.Split(',')
      DBG ('We have the following bindings: #{0} | {1}' -f $bindings.Length, ($bindings -join ' ; '))
      DBGIF $MyInvocation.MyCommand.Name { $bindings.Length -lt 1 }

      [bool] $atLeastOneSniEnabled = $false

      foreach ($oneBinding in $bindings) {

        DBG ('One binding to possibly enable SNI on: {0}' -f $oneBinding)
        [string] $bndProtocol = [string]::Empty
        
        if ($oneBinding -like 'http/?*') {
          
          $bndProtocol = 'http'
        
        } else {

          $bndProtocol = 'https'
          DBGIF $MyInvocation.MyCommand.Name { $oneBinding -notlike 'https/?*:?*:?*' }
          $atLeastOneSniEnabled = $true

          [string] $bndInfo = $oneBinding.SubString(($oneBinding.IndexOf('/') + 1))

          DBG ('The binding info parsed: {0} | {1}' -f $bndProtocol, $bndInfo)
          # Note: yes, the /site.name:"" must be in double quotes, while the protocol='' must use single quotes exactly as used bellow
          Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set site /site.name:"{0}" /bindings.[protocol=''{1}'',bindingInformation=''{2}''].sslFlags:1 /commit:appHost' -f $name, $bndProtocol, $bndInfo)
        }
      }

      DBGIF ('Cannot enable SNI on pure HTTP bindings') { -not $atLeastOneSniEnabled }
    }

  } else {

    DBG ('The IIS web site already exists. Will not create: {0}' -f $name)
  }

  
  DBG ('Configure the required IIS web site parameters')

  Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set app "{0}/" /applicationPool:"{1}"' -f $name, $appPool)

  Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:windowsAuthentication /enabled:{1} /commit:appHost' -f $name, $authWinState)
  if (($authWinState) -and ($authDisableWinKernel)) {

    DBG ('Disable kernel mode authentication as requested')
    Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:windowsAuthentication /useKernelMode:false /commit:appHost' -f $name)
  }

  Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:anonymousAuthentication /enabled:{1} /commit:appHost' -f $name, $authAnonState)
  Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:basicAuthentication /enabled:{1} /commit:appHost' -f $name, $authBasicState)
  Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:digestAuthentication /enabled:{1} /commit:appHost' -f $name, $authDigestState)


  if (Is-ValidString $compressionStates) {

    DBG ('Compression should be configured: {0}' -f $compressionStates)
    
    foreach ($oneCompressionState in (Split-MultiValue $compressionStates)) {

      DBG ('One compression state: {0}' -f $oneCompressionState)
      DBGIF $MyInvocation.MyCommand.Name { ($oneCompressionState[0] -ne '+') -and ($oneCompressionState[0] -ne '-') }

      [bool] $oneCompressionStateBool = $oneCompressionState[0] -eq '+'
      [string] $oneCompressionStateType = $oneCompressionState.SubString(1)

      DBG ('One compression state parsed: {0} | {1}' -f $oneCompressionStateBool, $oneCompressionStateType)
      DBGIF $MyInvocation.MyCommand.Name { ($oneCompressionStateType -ne 'static') -and ($oneCompressionStateType -ne 'dynamic') }

      Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:urlCompression /do{1}Compression:{2} /commit:appHost' -f $name, $oneCompressionStateType, $oneCompressionStateBool)
    }
  }


  if (Is-ValidString $clientCache) {

    DBG ('Will set client caching: {0}' -f $clientCache)
    Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:system.webServer/staticContent /clientCache.cacheControlMode:"{1}" /commit:appHost' -f $name, $clientCache)
  }
}


function global:Change-IISAnonymousIdentity ([string] $site, [string] $subPath, [string] $login, [string] $domain, [string] $password)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  [string] $iisPath = $site
  if (Is-ValidString $subPath) {

    $iisPath = '{0}/{1}' -f $iisPath.TrimEnd('/').TrimEnd('\'), $subPath.Trim('/').Trim('\')
  }

  DBG ('Will use the following IIS path: {0}' -f $iisPath)


  if (($login -eq 'ApplicationPoolIdentity') -and (Is-EmptyString $domain) -and (Is-EmptyString $pwd)) {

    DBG ('We are going for apppool identity')
    Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:anonymousAuthentication /userName:"" --password /commit:apphost' -f $iisPath)
  
  } elseif (($login -eq 'IUSR') -and ($domain -eq 'NT AUTHORITY') -and (Is-EmptyString $pwd)) {

    DBG ('We are going for IUSR')
    Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:anonymousAuthentication /userName:"NT AUTHORITY\IUSR" --password /commit:apphost' -f $iisPath)
  
  } else {

    DBG ('We are going for a specific user account')
    [string] $account = Get-SAMLogin $login $domain
    
    Run-Process $env:WINDIR\system32\inetsrv\appcmd.exe ('set config "{0}" /section:anonymousAuthentication /userName:"{1}" /password:"{2}" /commit:apphost' -f $iisPath, $account, $pwd)
  }
}


function global:Replace-MarkerSequence ([string] $sourceFile, [string] $pattern, [string] $includeFile, [char] $padChar, [string] $outFile, [switch] $justCopyIfNoInclude)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { (Is-EmptyString $sourceFile) -or (-not (Test-Path $sourceFile)) }
  DBGIF $MyInvocation.MyCommand.Name { (-not $justCopyIfNoInclude) -and ((Is-EmptyString $includeFile) -or (-not (Test-Path $includeFile))) }

  DBG ('The source file: exists = {0} | {1}' -f ((Is-ValidString $sourceFile) -and (Test-Path -Literal $sourceFile)), $sourceFile)
  DBG ('The include file: exists = {0} | {1}' -f ((Is-ValidString $includeFile) -and (Test-Path -Literal $includeFile)), $includeFile)
  DBG ('The output file: exists = {0} | {1}' -f ((Is-ValidString $outFile) -and (Test-Path -Literal $outFile)), $outFile)
  
  if ((Is-ValidString $sourceFile) -and (Test-Path $sourceFile) -and (Is-ValidString $includeFile) -and (Test-Path $includeFile)) {

    DBG ('Open the source and include files')

    DBGSTART
    $includeFl = Get-Item $includeFile
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $includeFl }
    
    if (Is-NonNull $includeFl) {

      DBG ('Include file length: {0} | {1}' -f $includeFl.Length, $includeFile)

      DBGSTART
      $sourceBytes = [System.IO.File]::ReadAllBytes($sourceFile)
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBG ('Source file length: {0} | {1}' -f $sourceBytes.Count, $sourceFile)
      DBGIF $MyInvocation.MyCommand.Name { $includeFl.Length -gt $sourceBytes.Count }

      $patternBytes = [System.Text.Encoding]::ASCII.GetBytes($pattern)
      DBG ('Pattern length: {0}' -f $patternBytes.Count)

      $seqIdxs = Find-PatternSequence $sourceBytes $patternBytes

      DBGIF $MyInvocation.MyCommand.Name { $includeFl.Length -gt ($seqIdxs.end - $seqIdxs.start + 1) }
      
      if ($includeFl.Length -le ($seqIdxs.end - $seqIdxs.start + 1)) {

        DBG ('Load include file contents: {0}' -f $includeFile)
        DBGSTART
        $includeBytes = [System.IO.File]::ReadAllBytes($includeFile)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBG ('Include bytes: {0} = 0x{0:X8}' -f $includeBytes.Count)

        [byte] $padByte = [System.Text.Encoding]::ASCII.GetBytes($padChar)[0]

        DBG ('Replace the source patterned bytes with the include bytes: start = {0:X8} | end = {1:X8}' -f $seqIdxs.start, ($seqIdxs.start + $includeBytes.Count - 1))
        for ($i = $seqIdxs.start; ($i - $seqIdxs.start) -lt $includeBytes.Count; $i ++) {

          $sourceBytes[$i] = $includeBytes[$i - $seqIdxs.start]
        }

        DBG ('Pad the rest of the source patterned bytes with the pad char: start = {0:X8} | end = {1:X8}' -f ($seqIdxs.start + $includeBytes.Count), $seqIdxs.end)
        for ($i = ($seqIdxs.start + $includeBytes.Count); $i -le $seqIdxs.end; $i ++) {
        
          $sourceBytes[$i] = $padByte
        }

        DBG ('Save output: {0}' -f $outFile)
        DBGSTART
        [System.IO.File]::WriteAllBytes($outFile, $sourceBytes)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }
    }
  
  } elseif ((Is-ValidString $sourceFile) -and (Test-Path $sourceFile) -and $justCopyIfNoInclude) {

    DBG ('The include file does not exist, just copy: {0}' -f $sourceFile, $outFile)
    DBGSTART
    Copy-Item -Path $sourceFile -Destination $outFile -Force
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  
  } else {

    DBGIF $MyInvocation.MyCommand.Name { $true }
  }
}


function global:Replace-HtmlSymbolEntitiesFromHtml ($html)
{
  $i = 0
  [System.Collections.ArrayList] $symbolEntitiesFound = @()

  while ($i -lt $html.Length) {

    if ($html[$i] -ne '&') {

      $i ++

    } else {

      $j = $i + 1
      [string] $oneSymbolEntityFound = ''

      # note the a bit different match, because we must not stop at the first # sign here yet
      while (($j -lt $html.Length) -and ($html[$j] -ne ';') -and ((Is-EmptyString $oneSymbolEntityFound) -or ($oneSymbolEntityFound -match '\A(?:(?:[a-zA-Z]+)|(?:\#[0-9]*))\Z'))) {

        $oneSymbolEntityFound += $html[$j]
        $j ++
      }

      #DBGIF ('Does not matches: {0} | {1} -----' -f $oneSymbolEntityFound, ($html[($i-5)..($j+5)] -join '')) { ($oneSymbolEntityFound -notmatch '\A(?:(?:[a-zA-Z]+)|(?:\#[0-9]+))\Z') }

      if (($j -lt $html.Length) -and ($oneSymbolEntityFound -match '\A(?:(?:[a-zA-Z]+)|(?:\#[0-9]+))\Z')) {

        if (-not $symbolEntitiesFound.Contains($oneSymbolEntityFound)) {

          DBGIF $MyInvocation.MyCommand.Name { $oneSymbolEntityFound.Length -lt 2 }
          DBGIF $MyInvocation.MyCommand.Name { $oneSymbolEntityFound.Length -gt 40 }
          [void] $symbolEntitiesFound.Add($oneSymbolEntityFound)
        }
      
        $i = $j + 1

      } else {

        $i = $i + 1
      }
    }
  }


  DBG ('Found symbol entities: {0}' -f $symbolEntitiesFound.Count)
  #DBG ('Symbol entities: {0}' -f ($symbolEntitiesFound | Out-String))

  foreach ($oneSymbolEntityFound in $symbolEntitiesFound) {

    [string] $replaceWith = $null

    if ($oneSymbolEntityFound -like '#*') {

      DBGSTART
      $replaceWith = [char]::ConvertFromUtf32([int]::Parse($oneSymbolEntityFound.SubString(1))).ToString()
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

    } else {

      DBGIF ('A replacement not defined for symbol entity: {0}' -f $oneSymbolEntityFound) { -not (Contains-Safe $global:xmlSymbolEntitiesSimplified.Keys $oneSymbolEntityFound) }

      if (Contains-Safe $global:xmlSymbolEntitiesSimplified.Keys $oneSymbolEntityFound) {
    
        $replaceWith = $global:xmlSymbolEntitiesSimplified[$oneSymbolEntityFound]

      } else {

        $replaceWith = '?'
      }
    }

    DBGIF ('Not found any replacement for: {0}' -f $oneSymbolEntityFound) { $replaceWith -eq '' }

    $html = $html.Replace(('&{0};' -f $oneSymbolEntityFound), $replaceWith)
  }

  return $html
}


function global:Extract-PureTextFromHtml ([string] $html, [bool] $compressSpaces, [bool] $replaceSymbolEntities)
{
  #DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBG ('Remove