ADLAB PowerShell source file: lib-utils.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 = 249
$adlabReleaseDateThisLib = [DateTime]::Parse('2018-10-17')
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       ||
# ============================================================


function global:Hold-FileOpen ([string] $path, [string] $sharing = 'None', [switch] $writeInfinite, [int] $delayMilliseconds = 1000, [int] $numberOf64kBBlocksToOverwrite = 0, [switch] $noTextOutput)
# [System.IO.FileShare] = None, Read, ReadWrite, Write, Delete
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $soubor = $null

  DBGSTART; DBGEND; try { $errorActionPreference = 'Stop'

    DBG ('Handle CTRL-C manually')
    [System.Console]::TreatControlCAsInput = $true

    DBG ('Opening file: {0} | sharing = {1}' -f $path, $sharing)

    [System.IO.FileStream] $fs = New-Object System.IO.FileStream $path, 
            'Create',     # [System.IO.FileMode]
            'ReadWrite',  # [System.IO.FileAccess]
            ([Enum]::Parse('System.IO.FileShare', $sharing))

    $soubor = New-Object System.IO.StreamWriter $fs

    $i = 0
    $startTime = Get-Date
    
    $current64kBBlock = 0
    [byte[]] $oneBlock = @(0..255) * 256

    while (($true) -and ($writeInfinite -or ((-not $writeInfinite) -and ($i -le 99999)))) {

      $currentTime = [DateTime]::Now
      $timeDiff = $currentTime - $startTime

      if ($numberOf64kBBlocksToOverwrite -gt 0) {

        $offset = $oneBlock.Count * $current64kBBlock
        $offsetStr = '| Offset = {0}' -f $offset
      }

      if (-not $noTextOutput) {

        $outStr = 'Step: {0,5} | Time: {1:D2}:{2:D2}:{3:D2} {4}' -f $i, $timeDiff.Hours, $timeDiff.Minutes, $timeDiff.Seconds, $offsetStr
        DBG ($outStr)
      }
      

      if ($writeInfinite) {

        if ($numberOf64kBBlocksToOverwrite -gt 0) {

          [void] $fs.Seek($offset, 'Begin')
          $fs.Write($oneBlock, 0, $oneBlock.Count)
          $current64kBBlock ++

          if ($current64kBBlock -ge $numberOf64kBBlocksToOverwrite) {

            $current64kBBlock = 0
          }
        
        } else {

          $soubor.WriteLine($outStr)
          $soubor.Flush()
        }
      }


      if ([System.Console]::KeyAvailable) {

        $key = [System.Console]::ReadKey($true)
        
        if (($key.Modifiers -band [ConsoleModifiers]::Control) -and ($key.Key -eq 'C')) {

          DBG ('User break')
          break
        }
      }

      if ($delayMilliseconds -gt 0) {

        Start-Sleep -Milliseconds $delayMilliseconds
      }

      $i ++
    }
  }

  catch [System.Exception] {
  
    DBG ('An exception occured')
    DBGER $MyInvocation.MyCommand.Name $error
  }
  
  finally { $errorActionPreference = 'Continue'

    DBG ('Restore CTRL-C behavior')
    [System.Console]::TreatControlCAsInput = $false

    DBG ('Finalizing')

    if ($soubor -ne $null) {
       
      $soubor.Close()
      $soubor.Dispose()
    }
    
    if ($fs -ne $null) {
    
      $fs.Close()
      $fs.Dispose()
    }
  
  } DBGSTART; DBGEND
}


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

  [string] $escaped = $null

  DBGSTART
  $escaped = [System.Uri]::EscapeDataString($toEscape)
  DBGER $MyInvocation.MyCommand.Name $Error
  DBGEND

  # Note: the characters not escaped by RFC 2396
  #       unreserved  = alphanum | mark
  #       mark        = "-" | "_" | "." | "!" | "~" | "*" | "'" | "(" | ")"
  #
  #       While RFC 3986 has the following unreserved chars:
  #       unreserved  = '-' | '_' | '.' |       '~'

  [string[]] $theRestToEscape = @('!', '*', "'", '(', ')')


  foreach ($oneRestToEscape in $theRestToEscape) {

    #DBGIF ('Found a special RFC 3986 char: {0}' -f $oneRestToEscape) { $escaped.Contains($oneRestToEscape) }
    $escaped = $escaped.Replace($oneRestToEscape, [System.Uri]::HexEscape($oneRestToEscape))
  }

  return $escaped
}


function global:Tweet-Status ([string] $status, [string] $consumerKey, [string] $consumerSecret, [string] $token, [string] $tokenSecret, [string] $proxy)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $status }  

  if (Is-ValidString $status) {

    # Docs from here: https://dev.twitter.com/docs/auth/oauth/single-user-with-examples
    #                 https://dev.twitter.com/docs/api/1.1/post/statuses/update
    #                 https://dev.twitter.com/docs/auth/authorizing-request

    $url = 'https://api.twitter.com/1.1/statuses/update.json'

    # Note: Must be escaped according to RFC 3986, which is not default. 
    #       By default, the method escapes according to RFC 2396,
    #       but twitter requires the RFC 3986 escaping
    #       According to RFC 3986, the only unescaped (unreserved) chars are:
    #       unreserved = ALPHA, DIGIT, '-', '.', '_', '~'
    #       The problem with the default escaping according to RFC 2396 is
    #       that it does not escape '(' ')' for example
    $statusMsg = Escape-DataStringRFC3986 $status
    DBG ('Escaped status message: {0}' -f $statusMsg)


    [string] $nowTicks = [DateTime]::Now.Ticks
    DBGSTART
    [string] $oauthNonce = [Convert]::ToBase64String([System.Text.Encoding]::ASCII.GetBytes($nowTicks))
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('Now ticks and nonce: {0} | {1}' -f $nowTicks, $oauthNonce)


    DBGSTART
    [string] $oauthTimeStamp = [int] ([DateTime]::UtcNow - [DateTime]::Parse("1970-01-01").ToUniversalTime()).TotalSeconds
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('Seconds from 1970: {0}' -f $oauthTimeStamp)

  
    $statusSignatureTemplate = 'oauth_consumer_key={0}&oauth_nonce={1}&oauth_signature_method=HMAC-SHA1&oauth_timestamp={2}&oauth_token={3}&oauth_version=1.0&status={4}'
    DBGSTART
    $signature = 'POST&{0}&{1}' -f (Escape-DataStringRFC3986 $url), (Escape-DataStringRFC3986 ($statusSignatureTemplate -f
      $consumerKey,
      $oauthNonce,
      $oauthTimeStamp,
      $token,
      $statusMsg
    ))
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('Signature to be HMACed: {0}' -f $signature)

  
    $key = '{0}&{1}' -f (Escape-DataStringRFC3986 $consumerSecret), (Escape-DataStringRFC3986 $tokenSecret)
    DBG ('Signature key: {0}' -f $key)

    DBGSTART
    $hmacsha1 = New-Object System.Security.Cryptography.HMACSHA1
    $hmacsha1.Key = [System.Text.Encoding]::ASCII.GetBytes($key)
    $oauthSignature = [System.Convert]::ToBase64String($hmacsha1.ComputeHash([System.Text.Encoding]::ASCII.GetBytes($signature)));  
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('Signature computed: {0}' -f $oauthSignature)

    DBGSTART
    $oauthAuthorizationHeader = 'OAuth oauth_consumer_key="{0}",oauth_nonce="{1}",oauth_signature="{2}",oauth_signature_method="HMAC-SHA1",oauth_timestamp="{3}",oauth_token="{4}",oauth_version="1.0"' -f
      (Escape-DataStringRFC3986 $consumerKey), 
      (Escape-DataStringRFC3986 $oauthNonce), 
      (Escape-DataStringRFC3986 $oauthSignature), 
      (Escape-DataStringRFC3986 $oauthTimeStamp), 
      (Escape-DataStringRFC3986 $token)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('OAuth Athorization header: {0}' -f $oauthAuthorizationHeader)

    if (Is-ValidString $proxy) {

      $proxyDefaultCredentials = $true
  
    } else {

      $proxyDefaultCredentials = $false
    }

    [void] (Download-WebPage $url -postParams @{'status' = $statusMsg} -postEncoding ([System.Text.UTF7Encoding]::UTF8) -otherHeaders @{'Content-Type' = 'application/x-www-form-urlencoded' ; 'Authorization' = $oauthAuthorizationHeader} -proxy $proxy -proxyDefaultCredentials $proxyDefaultCredentials -usePostData $true)
  }
}


 
function global:Recover-ExchangeMailboxes (
  [string] $dbToRecover = '\\10.101.90.95\Exchange\Mailbox',
  [string] $pstTargetPath = '\\10.101.90.95\Exchange\PST',
  [string] $restoreDbPath = '@1:\sevecek-mbxrestore-recoveryDB',
  [string] $tempDbPath = '@1:\sevecek-mbxrestore-tempDB', 
  [string] $restoreDbLogPath = '@1:\sevecek-mbxrestore-recoveryLogs',
  [string] $restoreDB = 'sevecek-mbxrestore-restoredb', 
  [string] $tempMbxDB = 'sevecek-mbxrestore-tempdb', 
  [string] $tempMailbox = 'sevecek-mbxrestore-tempMbx',
  [string] $pstTempPath = '@1:\sevecek-mbxrestore-tempPST'
  )
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  $restoreDbPathCreated = $false
  $tempDbPathCreated = $false
  $restoreDbLogPathCreated = $false
  $pstTempPathCreated = $false
  $mbxCount = -1
  $i

  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $dbToRecover) }
  if (-not (Test-Path $dbToRecover)) { 

    return
  }

  $restoreDbPath = Resolve-VolumePath $restoreDbPath
  $tempDbPath = Resolve-VolumePath $tempDbPath
  $restoreDbLogPath = Resolve-VolumePath $restoreDbLogPath
  $pstTempPath = Resolve-VolumePath $pstTempPath

  DBG ('PST temp path to be transformed to an UNC if necessary: {0}' -f $pstTempPath)
  if ($pstTempPath -like '?:\*') {

    DBGIF $MyInvocation.MyCommand.Name { $pstTempPath -like '\\*\*' }
    $pstTempPath = '\\localhost\{0}$\{1}' -f $pstTempPath[0], $pstTempPath.SubString(3)
  }


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

  DBG ('Get the source .EDB file name')
  $srcEdbFiles = Get-ChildItem $dbToRecover -Filter *.edb
  $srcEdbFile = $srcEdbFiles | Select -First 1 | Select -ExpandProperty FullName
  DBGIF $MyInvocation.MyCommand.Name { (-not (Test-Path $srcEdbFile)) -or (Is-EmptyString $srcEdbFile) }
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $srcEdbFiles) -gt 1 }

  if ((-not (Test-Path $srcEdbFile)) -or (Is-EmptyString $srcEdbFile) -or ((Get-CountSafe $srcEdbFiles) -ne 1)) {

    return
  }

  DBG ('Source EDB file found: {0} | {1:N0} MB' -f $srcEdbFile, ((Get-Item $srcEdbFile).Length / 1MB))


  $eseutilOUT = ESEUTIL '/mh' $srcEdbFile
  DBG ('ESEUTIL output: {0}' -f ($eseutilOUT | Out-String))

  $edbState = ($eseutilOUT | % { $_.Trim() } | ? { $_ -clike 'State: *' } | Select -First 1).SubString(7)
  DBG ('EDB file state: {0}' -f $edbState)
  DBGIF 'EDB file is not in clean shutdown state' { $edbState -ne 'Clean Shutdown' }

  if ($edbState -ne 'Clean Shutdown') {

    return
  }


  { DBGSTART; DBGEND; try { $errorActionPreference = 'Stop'

    DBG ('Restart MSExchangeIS')
    Restart-Service MSExchangeIS -Force

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

    DBG ('Create the restore DB folder: {0}' -f $restoreDbPath)
    DBGIF $MyInvocation.MyCommand.Name { Test-Path $restoreDbPath }

    if (Test-Path $restoreDbPath) {

      break
    }

    [void] (New-Item $restoreDbPath -ItemType Directory -Force)
    $restoreDbPathCreated = $true


    DBG ('Download the EDB folder from distribution: {0} | {1}' -f $dbToRecover, $restoreDbPath)
    Copy-Item -Path $dbToRecover -Destination $restoreDbPath -Recurse -Force


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

    DBG ('Get the local .EDB file name')
    $edbFiles = Get-ChildItem $restoreDbPath -Filter *.edb -Recurse
    $edbFile = $edbFiles | Select -First 1 | Select -ExpandProperty FullName
    DBGIF $MyInvocation.MyCommand.Name { (-not (Test-Path $edbFile)) -or (Is-EmptyString $edbFile) }
    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $edbFiles) -gt 1 }

    if ((-not (Test-Path $edbFile)) -or (Is-EmptyString $edbFile) -or ((Get-CountSafe $edbFiles) -ne 1)) {

      return
    }

    DBG ('Local EDB file found: {0} | {1:N0} MB' -f $edbFile, ((Get-Item $edbFile).Length / 1MB))


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

    DBG ('Create the temporary DB folder: {0}' -f $tempDbPath)
    DBGIF $MyInvocation.MyCommand.Name { Test-Path $tempDbPath }

    if (Test-Path $tempDbPath) {

      return
    }

    [void] (New-Item $tempDbPath -ItemType Directory -Force)
    $tempDbPathCreated = $true


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

    DBG ('Create the restore DB log folder: {0}' -f $restoreDbLogPath)
    DBGIF $MyInvocation.MyCommand.Name { Test-Path $restoreDbLogPath }

    if (Test-Path $restoreDbLogPath) {

      return
    }

    [void] (New-Item $restoreDbLogPath -ItemType Directory -Force)
    $restoreDbLogPathCreated = $true


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

    DBG ('Create the temp PST path: {0}' -f $pstTempPath)
    DBGIF $MyInvocation.MyCommand.Name { Test-Path $pstTempPath }

    if (Test-Path $pstTempPath) {

      return
    }

    [void] (New-Item -Path $pstTempPath -ItemType Directory -Force)
    $pstTempPathCreated = $true


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

    DBG ('Create new recovery database: {0} | {1} | {2} | {3}' -f $restoreDB, $edbFile, $restoreDbPath, $restoreDbLogPath)
    [void] (New-MailboxDatabase -Recovery -Name $restoreDB -EdbFilePath $edbFile -LogFolderPath $restoreDbLogPath -Server $global:thisComputerHost)

    DBG ('Create new temp database: {0} | {1}' -f $tempMbxDB, $tempDbPath)
    [void] (New-MailboxDatabase -Name $tempMbxDB -EdbFilePath "$tempDbPath\sevecek-mbxrestore-tempdb.edb" -LogFolderPath $tempDbPath -Server $global:thisComputerHost)

    DBG ('Set the temp database to use circular logging')
    Get-MailboxDatabase $tempMbxDB | Set-MailboxDatabase -CircularloggingEnabled:$true

    DBG ('Mount databases')
    Mount-Database $restoreDB
    Mount-Database $tempMbxDB
 
    DBG 'Get mailboxes to restore'
 
    DBGSTART
    $mbx = Get-MailboxStatistics -Database $restoreDB | select DisplayName, ItemCount, MailboxGuid, TotalItemSize, Identity, MapiIdentity #| Select -First 2
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBG ('Mailboxes to restore: {0} | {1}' -f (Get-CountSafe $mbx), ($mbx | Out-String))
    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $mbx) -lt 1 }

    if ((Get-CountSafe $mbx) -lt 1) {

      break
    }


    DBG ('Ensure the output PST path exists: {0}' -f $pstTargetPath)
    if (-not (Test-Path $pstTargetPath)) {

      DBG ('Create the output PST folder')
      [void] (New-Item -Path $pstTargetPath -ItemType Directory -Force)
    }


    DBG ('Process mailboxes')

    $i = 1
    $mbxCount = Get-CountSafe $mbx
    foreach ($oneMbx in $mbx) {

      $startTime = Get-Date
      DBG ('Start one: {0} of {1}' -f $i, $mbxCount)
      DBG ('Who: {0} | count: {1} | size: {2} MB' -f $oneMbx.DisplayName, $oneMbx.ItemCount, $oneMbx.TotalItemSize.Value.ToMB())
      DBG 'Going to create a new temp mailbox'

      [void] (New-Mailbox $tempMailbox -Password (ConvertTo-SecureString 'Pa$$w0rd' -AsPlain -Force) -UserPrinc $tempMailbox@gopas.virtual -Database $tempMbxDB)
      Get-Mailbox $tempMailbox | Set-Mailbox -ProhibitSendReceiveQuota 50GB -UseDatabaseQuotaDefaults:$false

      DBG 'Going to restore'

      [void] (New-MailboxRestoreRequest -SourceDatabase $restoreDB -SourceStoreMailbox $oneMbx.MailboxGuid -TargetMailbox $tempMailbox -AllowLegacyDNMismatch:$true -BadItemLimit 1000 -AcceptLargeDataLoss:$true)


      while ($true) {

        $mbxStat = Get-MailboxStatistics $tempMailbox | select DisplayName, ItemCount, MailboxGuid, TotalItemSize, Identity, MapiIdentity 
        $status = Get-MailboxRestoreRequest | Select -Expand Status

        DBG ('Restore status: {0} of {1} | {2}' -f $i, $mbxCount, $status)
        DBG ('Target mailbox: {0} of {1} = {2} MB of {3} MB' -f $mbxStat.ItemCount, $oneMbx.ItemCount, $mbxStat.TotalItemSize.Value.ToMB(), $oneMbx.TotalItemSize.Value.ToMB())

        if (($status -ne 'InProgress') -and ($status -ne 'Queued')) {

          DBG ('Final status reached, breaking: {0:N1} min' -f ((Get-Date) - $startTime).TotalMinutes)
          break
        }

        Start-Sleep 7
      }

      
      $tempPST = Join-Path $pstTempPath (Canonicalize-FileName ('{0}-{1}.pst' -f $oneMbx.DisplayName, $oneMbx.MailboxGuid))
      DBG ('Going to export the mailbox to a PST: {0}' -f $tempPST)
      DBGIF $MyInvocation.MyCommand.Name { Test-Path $tempPST }

      if (Test-Path $tempPST) {

        DBG ('Temp PST exists, deleting')
        Remove-Item $tempPST -Force
      }

      #
      # In order to have this cmdlet available at all, one must be assigned the following
      #   New-ManagementRoleAssignment ?Role ?Mailbox Import Export? ?User "DOMAIN\USER"
      #   New-ManagementRoleAssignment ?Role ?Mailbox Import Export? ?SecurityGroup "DOMAIN\GROUP"
      #

      [void] (New-MailboxExportRequest -Mailbox $tempMailbox -FilePath $tempPST -AcceptLargeDataLoss:$true -BadItemLimit 1000)

      while ($true) {

        $status = Get-MailboxExportRequest | Select -Expand Status
        $mbxStat = Get-MailboxStatistics $tempMailbox | select DisplayName, ItemCount, MailboxGuid, TotalItemSize, Identity, MapiIdentity 
        
        DBG ('PST export status: {0} of {1} | {2}' -f $i, $mbxCount, $status)
        DBGSTART # migth be the file does not exist yet
        DBG ('Data exported: mbx = {0} MB | pst = {1:N1} MB' -f $mbxStat.TotalItemSize.Value.ToMB(), ((Get-Item $tempPST -EA SilentlyContinue).Length / 1MB))
        DBGEND

        if (($status -ne 'InProgress') -and ($status -ne 'Queued')) {

          DBG ('Final PST export status reached, breaking: {0:N1} min' -f ((Get-Date) - $startTime).TotalMinutes)
          break
        }

        Start-Sleep 7
      }


      DBG ('Move the temp PST to the output PST folder: {0} | {1}' -f $tempPST, $pstTargetPath)
      Move-Item -Path $tempPST -Destination $pstTargetPath -Force


      DBG 'Delete the completed restore request'
      Get-MailboxRestoreRequest | Remove-MailboxRestoreRequest -Confirm:$false

      DBG 'Delete the completed PST export request'
      Get-MailboxExportRequest | Remove-MailboxExportRequest -Confirm:$false


      DBG 'Going to remove the temp mailbox'
      Remove-Mailbox $tempMailbox -Confirm:$false

      DBG 'Going to purge the deleted mailbox'
      Remove-StoreMailbox -Database $tempMbxDB -Identity $tempMailbox -MailboxState Disabled -Confirm:$false

      $i ++
    }
  }

  catch [System.Exception] { DBGER $MyInvocation.MyCommand.Name $error
  
  }

  finally { $errorActionPreference = 'Continue'

    DBG 'Delete all outstanding restore requests'
    Get-MailboxRestoreRequest | Remove-MailboxRestoreRequest -Confirm:$false

    DBG 'Delete all outstanding PST export requests'
    Get-MailboxExportRequest | Remove-MailboxExportRequest -Confirm:$false

    DBG 'Going to remove the temp mailbox in case it was not removed yet'
    Get-Mailbox $tempMailbox -EA SilentlyContinue | Remove-Mailbox -Confirm:$false

    DBG 'Going to remove the temp database'
    Get-MailboxDatabase $tempMbxDB -EA SilentlyContinue | Dismount-Database -Confirm:$false
    Get-MailboxDatabase $tempMbxDB -EA SilentlyContinue | Remove-MailboxDatabase -Confirm:$false

    DBG ('Restart MSExchangeIS')
    Restart-Service MSExchangeIS -Force

    if ($tempDbPathCreated) {

      DBG ('Delete the temp database folder: {0}' -f $tempDbPath)
      Remove-Item $tempDbPath -Force -Recurse
    }

    DBG 'Going to remove the restore database'
    Get-MailboxDatabase $restoreDB -EA SilentlyContinue | Dismount-Database -Confirm:$false
    Get-MailboxDatabase $restoreDB -EA SilentlyContinue | Remove-MailboxDatabase -Confirm:$false

    DBG ('Restart MSExchangeIS')
    Restart-Service MSExchangeIS -Force

    if ($restoreDbLogPathCreated) {

      DBG ('Delete the restore DB log folder: {0}' -f $restoreDbLogPath)
      Remove-Item $restoreDbLogPath -Force -Recurse
    }

    if ($restoreDbPathCreated) {

      DBG ('Delete the restore DB folder: {0}' -f $restoreDbPath)
      Remove-Item $restoreDbPath -Force -Recurse
    }

    if ($pstTempPathCreated) {

      DBG ('Delete the temp PST folder: {0}' -f $pstTempPath)
      Remove-Item $pstTempPath -Force -Recurse
    }


    DBGIF ('Error during processing. Finished: {0} of {1}' -f ($i - 1), $mbxCount) { ($mbxCount -lt 1) -or ($i -ne ($mbxCount + 1)) }

  } DBGSTART; DBGEND }
}


function global:Get-AllSPFolders ([object] $folderOrWeb)
{
  [System.Collections.ArrayList] $outFolders = @($folderOrWeb)

  if (($folderOrWeb -ne $null) -and (($folderOrWeb -is [Microsoft.SharePoint.SPFolder]) -or ($folderOrWeb -is [Microsoft.SharePoint.SPWeb]))) {

    if ($folderOrWeb -is [Microsoft.SharePoint.SPWeb]) {

      DBG ('Get subfolders for web site: {0}' -f $folderOrWeb.Url)
      DBGSTART
      $folderOrWeb.Folders | % { [void] $outFolders.Add($_) }
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }

    $i = 0;
    while ($i -lt $outFolders.Count) {

      DBGSTART
      $subFoldersCount = $outFolders[$i].SubFolders.Count
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
      DBG ('Get subfolders for folder: {0} | # = {1}' -f $outFolders[$i].Url, $subFoldersCount)

      if ($subFoldersCount -gt 0) {

        DBGSTART
        $outFolders[$i].SubFolders | % { [void] $outFolders.Add($_) }
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
      }

      $i ++
    }
  }

  DBG ('Folders found: {0}' -f $outFolders.Count)
  DBGIF $MyInvocation.MyCommand.Name { $outFolders.Count -lt 1 }

  return ,$outFolders
}


function global:Get-AllSPFiles (
  [bool] $includeCA,
  [string[]] $includedExts = @(
    '.aspx', '.html', '.htm', # We rather do not touch '.svc' files because they mostly return 404 anyway and also produce errors into EventLog at central administration web site
    '.master', '.preview', '.css', 
    '.bmp', '.jpg', '.jpeg', '.png', '.gif', '.ico', '.ttf',
    '.js', '.json', '.xml', '.xsl', '.xsd', '.xaml', '.xap', '.spcolor', '.spfont', '.webpart', '.dwp', '.cnf', '.wsdl',
    '.txt', '.csv'
    ),
  [string[]] $explicitWebApps
  )
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBG ('Load SharePoint PowerShell snap-in')
  Assert-SnapInSafe Microsoft.SharePoint.PowerShell

  [System.Collections.ArrayList] $urls = @()
  $results = New-Object PSCustomObject

  Add-Member -Input $results -MemberType NoteProperty -Name enumStartTime -Value (Get-Date)
  Add-Member -Input $results -MemberType NoteProperty -Name statsPerSiteCol -Value (New-Object System.Collections.ArrayList)
  Add-Member -Input $results -MemberType NoteProperty -Name physFilesPerWebApp -Value (New-Object System.Collections.ArrayList)
  Add-Member -Input $results -MemberType NoteProperty -Name successFiles -Value 0
  Add-Member -Input $results -MemberType NoteProperty -Name errorFiles -Value 0

  [Collections.ArrayList] $webApps = @()

  if (Is-Null $explicitWebApps) {

    DBG ('Get all web applications, including CA: {0}' -f $includeCA)
    DBGSTART
    Get-SPWebApplication -IncludeCentralAdministration:$includeCA | % { [void] $webApps.Add($_) }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
  
  } else {

    DBG ('Get the explicitly named web applications (CA excluding): {0} | {1}' -f (Get-CountSafe $explicitWebApps), ($explicitWebApps -join ','))
    DBGSTART
    $explicitWebApps | % { Get-SPWebApplication $_ } | % { [void] $webApps.Add($_) }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    if ($includeCA) {

      DBG ('Add the CA into the list of the explicitly named web applications: includeCA = {0}' -f $includeCA)
      DBGSTART
      Get-SPWebApplication -IncludeCentralAdministration:$includeCA | ? { $_.IsAdministrationWebApplication } | % { [void] $webApps.Add($_) }
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }
  }

  DBG ('Web applications found: {0} | {1}' -f (Get-CountSafe $webApps), (($webApps | Select -Expand Url) -join ', '))
  DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $webApps) -lt 1 }

  [int] $allFiles = 0
  [int] $allFsFiles = 0
  foreach ($oneApp in $webApps) {

    DBG ('Web application: {0}' -f $oneApp.Url)

    [System.Collections.ArrayList] $sites = @()
    DBGSTART
    Get-SPSite -WebApplication $oneApp -Limit All | % { [void] $sites.Add($_) }
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBG ('Sites found: {0}' -f $sites.Count)
    
    foreach ($oneSite in $sites) {

      DBG ('Site collection: {0}' -f $oneSite.Url)

      [System.Collections.ArrayList] $webs = @()
      DBGSTART
      [void] $webs.Add($oneSite.RootWeb)
      $oneSite.RootWeb.GetSubwebsForCurrentUser() | % { [void] $webs.Add($_) }
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND

      DBG ('Web sites: # = {0}' -f (Get-CountSafe $webs))

      $siteColAllFiles = 0
      $siteColOutFiles = 0
   
      foreach ($oneWeb in $webs) {

        DBG ('Web site: {0}' -f $oneWeb.Url)

        $folders = Get-AllSPFolders $oneWeb

        foreach ($oneFolder in $folders) {

          [System.Collections.ArrayList] $oneFolderFiles = @()
          DBGSTART
          $oneFolder.Files | % { [void] $oneFolderFiles.Add($_) }
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND

          if ($oneFolderFiles.Count -gt 0) {

            foreach ($oneFile in $oneFolderFiles) {

              $allFiles ++
              $siteColAllFiles ++

              if (Contains-Safe $includedExts ([System.IO.Path]::GetExtension($oneFile.Url))) {

                if ($oneFile.Url -match '\Ahttp(?:s|)://.+') {

                  $oneFileUrl = $oneFile.Url
 
                } else {

                  $oneFileUrl = '{0}/{1}' -f $oneWeb.Url, $oneFile.Url
                }

                DBG ('File: {0}' -f $oneFileUrl)
                DBGIF $MyInvocation.MyCommand.Name { $oneFileUrl -notmatch $global:rxUrl }
                [void] $urls.Add($oneFileUrl)
  
                $siteColOutFiles ++
              }
            }

          }
        }
      }

      $siteColStat = New-Object PSCustomObject
      Add-Member -Input $siteColStat -MemberType NoteProperty -Name webApp -Value $oneApp.Url
      Add-Member -Input $siteColStat -MemberType NoteProperty -Name siteCol -Value $oneSite.Url
      Add-Member -Input $siteColStat -MemberType NoteProperty -Name allFiles -Value $siteColAllFiles
      Add-Member -Input $siteColStat -MemberType NoteProperty -Name outFiles -Value $siteColOutFiles

      [void] $results.statsPerSiteCol.Add($siteColStat)
    }

    #
    # Note: enum local disk files
    #

    # Note: on SP 2010 we cannot index into the IisSettings directly with ['Default']
    #       instead, we must use the .Item() method
    DBG ('Get IIS settings for the default extension: {0} | {1}' -f $oneApp.Url, $oneApp.Status)

    DBGSTART
    $siteInfo = $oneApp.IisSettings.Item('Default')
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { Is-Null $siteInfo }

    $siteId = $siteInfo.PreferredInstanceId # 2113459420
    $siteName = $siteInfo.ServerComment     # intranet
    $sitePath = $siteInfo.Path              # G:\SP-intranet-HTTP

    DBG ('IIS settings for the web application: {0} | {1} | {2} | {3}' -f $oneApp.Url, $siteId, $siteName, $sitePath)

    $siteXml = Get-IISGenericXml ('site', ('/site.id:"{0}"' -f $siteId))
    DBGIF $MyInvocation.MyCommand.Name { $siteXml.appcmd.site.'site.name' -ne $siteName }

    $siteXml = Get-IISGenericXml ('site', ('/site.id:"{0}"' -f $siteId))
    $iisAppXml = Get-IISGenericXml ('app', ('/site.name:"{0}"' -f $siteName))


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

    DBG ('Found IIS applications: {0}' -f (Get-CountSafe $iisAppXml.appcmd.app))
    DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $iisAppXml.appcmd.app) -lt 1 }

    if ((Get-CountSafe $iisAppXml.appcmd.app) -gt 0) {

      foreach ($oneIisApp in $iisAppXml.appcmd.app) {

        $oneIisAppName = $oneIisApp.'app.name'
        $oneIisAppUrl = ('{0}/{1}' -f $oneApp.Url.Trim('/'), $oneIisApp.path.Trim('/')).Trim('/')

        DBG ('Processing one IIS application: {0} | {1}' -f $oneIisAppName, $oneIisAppUrl)
        $vdirXml = Get-IISGenericXml ('vdir', '/app.name:"{0}"' -f $oneIisAppName)

        DBG ('Found IIS vdirs for application: {0} | {1}' -f $oneIisAppName, (Get-CountSafe $vdirXml.appcmd.vdir))
        DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $vdirXml.appcmd.vdir) -lt 1 }

        foreach ($oneVdir in $vdirXml.appcmd.vdir) {

          $oneVdirUrl = '{0}/{1}' -f $oneIisAppUrl.Trim('/'), $oneVdir.path.Trim('/')
          $oneVdirNtfs = $oneVdir.physicalPath

          DBG ('Output one Vdir path: {0} | {1}' -f $oneVdirUrl, $oneVdirNtfs)
          DBGIF $MyInvocation.MyCommand.Name { $oneVdirUrl -notmatch $global:rxURL }
          DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $oneVdirNtfs) }
          DBGIF $MyInvocation.MyCommand.Name { ($oneVdirNtfs -ne $sitePath) -and ($oneVdirNtfs -notlike (Join-Path $sitePath '*')) -and ($oneVdirNtfs -notlike (Join-Path $env:ProgramFiles 'Common Files\Microsoft Shared\Web Server Extensions\*')) }

          $oneVdirDescriptor = New-Object PSCustomObject
          Add-Member -Input $oneVdirDescriptor -MemberType NoteProperty -Name url -Value $oneVdirUrl
          Add-Member -Input $oneVdirDescriptor -MemberType NoteProperty -Name path -Value $oneVdirNtfs

          [void] $diskUrls.Add($oneVdirDescriptor)
        }
      }
    }

    DBG ('Found base disk URLs and files: {0}' -f $diskUrls.Count)
    DBGIF $MyInvocation.MyCommand.Name { $diskUrls.Count -lt 1 }

    [Collections.ArrayList] $diskUrlsAllFiles = @()
    #[Collections.ArrayList] $diskUrlsOutFiles = @()
    [int] $diskUrlsOutFileCount = 0

    foreach ($oneDiskUrl in $diskUrls) {

      [string[]] $diskFiles = Get-ChildItem $oneDiskUrl.path -Recurse | ? { -not $_.PSIsContainer } | Select -Expand FullName
      DBG ('Found files in the physical base vdir: {0} | {1}' -f $oneDiskUrl.path, $diskFiles.Count)

      foreach ($oneDiskFile in $diskFiles) {

        $oneDiskFileRelativePath = $oneDiskFile.SubString($oneDiskUrl.path.Length).Replace('\', '/')
        $oneDiskFileUrl = '{0}/{1}' -f $oneDiskUrl.url.Trim('/'), $oneDiskFileRelativePath.Trim('/')

        DBG ('One physical disk file: {0}' -f $oneDiskFileUrl)
        [void] $diskUrlsAllFiles.Add($oneDiskFileUrl)

        if (Contains-Safe $includedExts ([System.IO.Path]::GetExtension($oneDiskFile))) {

          #[void] $diskUrlsOutFiles.Add($oneDiskFileUrl)
          [void] $urls.Add($oneDiskFileUrl)
          $diskUrlsOutFileCount ++
        }
      }
    }

    $webAppPhysFileStats = New-Object PSCustomObject
    Add-Member -Input $webAppPhysFileStats -MemberType NoteProperty -Name webApp -Value $oneApp.Url
    Add-Member -Input $webAppPhysFileStats -MemberType NoteProperty -Name baseVdirs -Value $diskUrls
    Add-Member -Input $webAppPhysFileStats -MemberType NoteProperty -Name allFiles -Value $diskUrlsAllFiles
    Add-Member -Input $webAppPhysFileStats -MemberType NoteProperty -Name outFilesCount -Value $diskUrlsOutFileCount
    
    $allFsFiles += $diskUrlsAllFiles.Count
    [void] $results.physFilesPerWebApp.Add($webAppPhysFileStats)
  }


  Add-Member -Input $results -MemberType NoteProperty -Name enumEndTime -Value (Get-Date)
  Add-Member -Input $results -MemberType NoteProperty -Name enumMinutes -Value ([Math]::Round(($results.enumEndTime - $results.enumStartTime).TotalMinutes, 1))
  Add-Member -Input $results -MemberType NoteProperty -Name allDbFiles -Value $allFiles
  Add-Member -Input $results -MemberType NoteProperty -Name allFsFiles -Value $allFsFiles
  Add-Member -Input $results -MemberType NoteProperty -Name outFiles -Value $urls.Count
  Add-Member -Input $results -MemberType NoteProperty -Name urls -Value $urls

  DBGIF $MyInvocation.MyCommand.Name { $results.allDbFiles -lt 1 }
  DBGIF $MyInvocation.MyCommand.Name { $results.allFsFiles -lt 1 }
  DBGIF $MyInvocation.MyCommand.Name { $results.outFiles -lt 1 }

  return $results
}


function global:Touch-AllSPFiles (
    [int] $tooBigDownload = 10MB,
    [int] $tooSlowDownload = 7,
    [bool] $includeCA,
    [string[]] $explicitWebApps,
    [string] $importURLs,
    [bool] $randomize = $true,
    [int] $delayMillisecond = 170
    )
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  #DBG ('Load SharePoint PowerShell snap-in')
  #Assert-SnapInSafe Microsoft.SharePoint.PowerShell

  DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $importURLs) -and ($includeCA -or (Is-NonNull $explicitWebApps)) }

  
  if (Is-EmptyString $importURLs) {

    $stats = Get-AllSPFiles -includeCA $includeCA -explicitWebApps $explicitWebApps

  } else {

    DBG ('Will use the supplied URL list: exist = {0} | {1}' -f (Test-Path $importURLs), $importURLs)
    DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $importURLs) }
    DBGSTART
    [object[]] $urls = Get-Content $importURLs
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND
    DBGIF $MyInvocation.MyCommand.Name { $urls.Count -lt 1 }

    $stats = New-Object PSCustomObject
    Add-Member -Input $stats -MemberType NoteProperty -Name enumStartTime -Value (Get-Date)
    Add-Member -Input $stats -MemberType NoteProperty -Name statsPerSiteCol -Value $null
    Add-Member -Input $stats -MemberType NoteProperty -Name physFilesPerWebApp -Value 0
    Add-Member -Input $stats -MemberType NoteProperty -Name errorFiles -Value 0
    Add-Member -Input $stats -MemberType NoteProperty -Name successFiles -Value 0
    Add-Member -Input $stats -MemberType NoteProperty -Name enumEndTime -Value (Get-Date)
    Add-Member -Input $stats -MemberType NoteProperty -Name enumMinutes -Value 0
    Add-Member -Input $stats -MemberType NoteProperty -Name allDbFiles -Value 0
    Add-Member -Input $stats -MemberType NoteProperty -Name allFsFiles -Value 0
    Add-Member -Input $stats -MemberType NoteProperty -Name outFiles -Value $urls.Count
    Add-Member -Input $stats -MemberType NoteProperty -Name urls -Value $urls
  }


  Define-CookieAwareWebClient

  DBG ('Start touching URLs: # = {0}' -f (Get-CountSafe $stats.urls))

  [System.Collections.ArrayList] $urlStats = @()
  [DateTime] $startTime = Get-Date

  [System.Collections.ArrayList] $urlsToTouch = $null
  if ($randomize) {

    [System.Collections.ArrayList] $urlsToTouch = Get-Random -Input $stats.urls -Count $stats.urls.Count
  
  } else {

    [System.Collections.ArrayList] $urlsToTouch = $stats.urls
  }


  $i = 1
  foreach ($oneUrl in $urlsToTouch) {
  
    #DBG ('Instantiate new CookieAwareWebClient')
    DBGSTART
    $webClient = New-Object Sevecek.CookieAwareWebClient
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    $webClient.Headers.Add('User-Agent', 'Sevecek-SharePoint-Keep-Alive')
    $webClient.Credentials = [System.Net.CredentialCache]::DefaultCredentials

    if (($i % 500) -eq 0) {

      DBG ('Progress at url: {0}' -f $i)
    }

    [string] $httpOutput = ''

    [DateTime] $oneUrlStartTime = Get-Date

    $httpError = $false
    DBGSTART
    $httpOutput = $webClient.DownloadString($oneUrl)
    if ($error.Count -gt 0) {

        $httpError = $true        
    }
    DBGER "Download error for: #$i = $oneUrl" $error
    DBGEND

    DBGSTART
    $webClient.Dispose()
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    [DateTime] $oneUrlEndTime = Get-Date

    $urlStat = New-Object PSCustomObject
    Add-Member -Input $urlStat -MemberType NoteProperty -Name url -Value $oneUrl
    Add-Member -Input $urlStat -MemberType NoteProperty -Name error -Value $httpError
    Add-Member -Input $urlStat -MemberType NoteProperty -Name seconds -Value ([Math]::Round(($oneUrlEndTime - $oneUrlStartTime).TotalSeconds, 2))
    Add-Member -Input $urlStat -MemberType NoteProperty -Name size -Value $httpOutput.Length
    [void] $urlStats.Add($urlStat)

    if ($httpError) {
      
      $stats.errorFiles = $stats.errorFiles + 1
    
    } else {

      $stats.successFiles = $stats.successFiles + 1
    }

    DBGIF ('Quite a big result: #{0} | {1}MB | {2}' -f $i, ([int] ($urlStat.size / 1MB)), $urlStat.url) { $urlStat.size -gt $tooBigDownload }
    DBGIF ('Quite a slow result: #{0} | {1}sec | {2}' -f $i, $urlStat.seconds, $urlStat.url) { $urlStat.seconds -gt $tooSlowDownload }

    if ($delayMillisecond -gt 0) {

      Start-Sleep -Milliseconds $delayMillisecond
    }

    $i ++
  }

  [DateTime] $endTime = Get-Date

  DBG ('Enum time taken: {0}min' -f $stats.enumMinutes)
  DBG ('Enumed files: allDB = {0} | allFS = {1} | toBeTouched = {2} | successTouched = {3} | errors = {4}' -f $stats.allDbFiles, $stats.allFsFiles, $stats.outFiles, $stats.successFiles, $stats.errorFiles)
  DBG ('Site collection enum stats: {0}' -f ($stats.statsPerSiteCol | ft * -Auto | Out-String))

  DBG ('Download time taken: {0:N1}min' -f ($endTime - $startTime).TotalMinutes)

  return ,$urlStats
}


function global:Retweet-SPItems ([string] $webUrl, [string] $list, [string] $markColumn, [string] $columnToPublish, [string] $abstractColumn, [bool] $includeLink, [bool] $linkOnlyIfLonger, [string] $publicWebAppUrl, [DateTime] $oldestItem = (Get-Date).AddDays(-2))
# Retweet-SPItems -webUrl 'http://intranet/blog' -list Posts -markColumn TweetedAlready -columnToPublish Title -abstractColumn TwitterAbstract -includeLink $true -publicWebAppUrl 'http://intranet/'
# Retweet-SPItems -webUrl 'http://intranet' -list Hlasky -markColumn TweetedAlready -columnToPublish Body -includeLink $true -publicWebAppUrl 'http://intranet/'
<#
   $webUrl = 'http://intranet/blog'
   $list = 'Posts'
   $markColumn = 'TweetedAlready'
   $columnToPublish = 'Title'
   $abstractColumn = 'TwitterAbstract'
   $includeLink = $true
   $linkOnlyIfLonger = $false
   $publicWebAppUrl = 'http://intranet/'
   $oldestItem = (Get-Date).AddDays(-2)
#>
{
  DBG ('Load SharePoint PowerShell snap-in')
  Assert-SnapInSafe Microsoft.SharePoint.PowerShell

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $webUrl }
  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $list }
  DBGIF $MyInvocation.MyCommand.Name { $includeLink -and (Is-EmptyString $publicWebAppUrl) }

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

  if ((Is-ValidString $webUrl) -and (Is-ValidString $list)) {

    DBG ('Going to open the list: {0} | {1}' -f $webUrl, $list)

    DBG ('Get the containing web')
    DBGSTART
    $web = $null
    $web = Get-SPWeb $webUrl -EA SilentlyContinue
    DBGER $MyInvocation.MyCommand.Name $Error
    DBGEND

    DBGIF $MyInvocation.MyCommand.Name { Is-Null $web }
    DBG ('Web opened: {0} | lists # = {1}' -f $web.Title, $web.Lists.Count)
    DBG ('Web contains lists: {0}' -f (($web.Lists | select -Expand Title) -join ','))

    DBG ('Get the requested list: {0}' -f $list)
    DBGSTART
    $spList = $null
    $spList = $web.Lists[$list]
    DBGER $MyInvocation.MyCommand.Name $Error
    DBGEND

    DBGIF $MyInvocation.MyCommand.Name { Is-Null $spList }
    DBG ('List opened: {0} | last modified = {1:s} | id = {2} | items # = {3}' -f $spList.Title, $spList.LastItemModifiedDate, $spList.Id, $spList.Items.Count)

    if (Is-NonNull $spList) {

      DBG ('Build SP query to obtain non-tweeted items')
      # Note: the type here must be specified explicitly
      #       because the .GetItems() would not be able to
      #       determine the correct overload to call
      #       without the exact typing.
      [Microsoft.SharePoint.SPQuery] $spQuery = New-Object Microsoft.SharePoint.SPQuery
    
      #
      # Note: the column names must be InternalName: $list.Fields | Select Title, InternalName
      #
      #       Value types: Text, DateTime, Boolean
      #       Operators:   Eq, Leq, Geq, Contains, IsNotNull
      #

      $spQuery.Query = '' +
                         '' +
                           '' +
                             '' +
                               ('' -f $markColumn) +
                               'False' +
                             '' +
                             '' +
                               ('' -f $markColumn) +
                             '' +
                           '' +
                           '' +
                             '' +
                             ('{0:s}' -f $oldestItem) +
                           '' +
                         '' +
                       '' +
                       '' +
                         '' +
                       ''

      if (Is-ValidString $abstractColumn) {
      
        $spQuery.ViewFields = ('' -f $columnToPublish) +
                              ('' -f $abstractColumn) +
                              ('' -f $markColumn) +
                               ''
      } else {
    
        $spQuery.ViewFields = ('' -f $columnToPublish) +
                              ('' -f $markColumn) +
                               ''
      }

      # This must be set to $false in order to be able to update the fetched list items later
      # otherwise I get the following errors:
      # Value does not fall within the expected range.
      $spQuery.ViewFieldsOnly = $false
  
      $foundItems = $spList.GetItems($spQuery)
      DBG ('Found items: {0}' -f (Get-CountSafe $foundItems))
      
      if ((Get-CountSafe $foundItems) -gt 0) {
      
        foreach ($oneFoundItem in $foundItems) {
        
          [string] $tweetStatus = Extract-PureTextFromHtml $oneFoundItem[$columnToPublish] $true $true
          DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $tweetStatus }
          # Note: trim non-alphanumeric characters from around the string
          $tweetStatus = [regex]::Match($tweetStatus, '(\A\W*)(.+?)(\W*\Z)').Groups[2].Value
          DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $tweetStatus }
          
          if (Is-ValidString $tweetStatus) {
          
            if ((Is-ValidString $abstractColumn) -and ($oneFoundItem[$abstractColumn])) {
          
              DBGIF $MyInvocation.MyCommand.Name { $tweetStatus.Length -gt 110 }  # just a matter of letting some abstract to prevail
              $tweetStatus = '{0} - {1}' -f $tweetStatus, $oneFoundItem[$abstractColumn]
            }

            DBG ('Tweet status: {0} | {1}' -f $tweetStatus.Length, $tweetStatus)

            if (
                ($includeLink -and (-not $linkOnlyIfLonger)) -or 
                ($includeLink -and $linkOnlyIfLonger -and ($tweetStatus.Length -gt 140))
               )
            {
              $itemURI = '{0}/{1}?ID={2}' -f $publicWebAppUrl.TrimEnd('/'), $spList.DefaultDisplayFormUrl.TrimStart('/'), $oneFoundItem.ID
              DBG ('Tweet item URI: {0}' -f $itemURI)
            
              if ($tweetStatus.Length -gt 118) {
            
                $tweetStatus = '{0}..' -f $tweetStatus.SubString(0, 115)
              }
              
              $tweetStatus = '{0} {1}' -f $tweetStatus, $itemURI
            }
          
            DBG ('Final tweet status: {0}' -f $tweetStatus)
            [void] $tweets.Add($tweetStatus)
          }
          
          <#$fld = $oneFoundItem.Fields[$columnToPublish]
          $fld.ParseAndSetValue($oneFoundItem, 'ahoj')
          $fld.Update()#>
          $oneFoundItem[$markColumn] = $true
          $oneFoundItem.Update()          
        }
      }
    }
  }
  
  DBG ('Tweets: #{0} | {1}' -f (Get-CountSafe $tweets), ($tweets -join ' == '))

  return ,$tweets
}



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

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $pptFile }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $pptFile) }

  [string] $outFile = ''

  if ((Is-ValidString $pptFile) -and (Test-Path $pptFile)) {

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

      DBG ('Open PowerPoint application')
      DBGSTART
      $ppt = New-Object -ComObject PowerPoint.Application 
      DBGER $MyInvocation.MyCommand.Name $Error
      DBGEND
      [void] $comList.Add($ppt)

      # FileName, ReadOnly, Untitled, WithWindow
      $withWindow = [Microsoft.Office.Core.MsoTriState]::msoFalse
      DBG ('Get the slide deck: {0}' -f $pptFile)
      DBGSTART
      $slideDeck = $ppt.Presentations.Open($pptFile, [Microsoft.Office.Core.MsoTriState]::msoTrue, [Microsoft.Office.Core.MsoTriState]::msoTrue, $withWindow)
      DBGER $MyInvocation.MyCommand.Name $Error
      DBGEND
      [void] $comList.Add($slideDeck)

      DBG ('Presentation has slides: {0}' -f $slideDeck.Slides.Count)


      Add-Type -AssemblyName Microsoft.Office.Interop.PowerPoint
      $saveOptions = [Microsoft.Office.Interop.PowerPoint.PpSaveAsFileType]::"ppSaveAs$saveAs"

      $outFile = [System.IO.Path]::ChangeExtension($pptFile, $saveAs.ToLower())
      if (Test-Path $outFile) { Remove-Item $outFile -Force }
      DBG ('Save the file: {0}' -f $outFile)
      DBGSTART
      $slideDeck.SaveAs($outFile, $saveOptions)
      DBGER $MyInvocation.MyCommand.Name $Error
      DBGEND


      DBG ('Close the slide deck')
      DBGSTART
      $slideDeck.Close()
      #[void] [System.Runtime.Interopservices.Marshal]::ReleaseComObject($slideDeck)
      $slideDeck = $null
      DBGER $MyInvocation.MyCommand.Name $Error
      DBGEND

      DBG ('Quit the PowerPoint application')
      DBGSTART
      $ppt.Quit()
      #[void] [System.Runtime.Interopservices.Marshal]::ReleaseComObject($ppt)
      $ppt = $null
      DBGER $MyInvocation.MyCommand.Name $Error
      DBGEND


      Release-ComList ([ref] $comList)
  }

  return $outFile
}


function global:Print-PptAsXPSorPDF ([string] $pptFile, [string] $printAs, [string] $printHow, [switch] $asPDF)
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $pptFile }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $pptFile) }

  [string] $outFile = ''

  if ((Is-ValidString $pptFile) -and (Test-Path $pptFile)) {

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

      DBG ('Open PowerPoint application. Give it some 5 seconds to calm down.')
      # Note: nobody knows, but there were some problems such as sometimes the COM object
      #       didn't open due to some weird error such as "the COM interface does not exist" etc.
      #       I suspect that the problem was due to the previous POWERPNT process still running/terminating
      #       just as we were trying to start a new one. This delay seems to solve the problem.
      Start-Sleep 5

      DBGSTART
      $ppt = $null
      $ppt = New-Object -ComObject PowerPoint.Application 
      DBGER $MyInvocation.MyCommand.Name $Error
      DBGEND
      DBGIF $MyInvocation.MyCommand.Name { Is-Null $ppt }

      if (Is-NonNull $ppt) {

        [void] $comList.Add($ppt)

        # FileName, ReadOnly, Untitled, WithWindow
        $withWindow = [Microsoft.Office.Core.MsoTriState]::msoFalse
        DBG ('Get the slide deck: {0}' -f $pptFile)
        DBGSTART
        $slideDeck = $null
        $slideDeck = $ppt.Presentations.Open($pptFile, [Microsoft.Office.Core.MsoTriState]::msoTrue, [Microsoft.Office.Core.MsoTriState]::msoTrue, $withWindow)
        DBGER $MyInvocation.MyCommand.Name $error
        DBGEND
        DBGIF $MyInvocation.MyCommand.Name { Is-Null $slideDeck }
        
        if (Is-NonNull $slideDeck) {

          [void] $comList.Add($slideDeck)
          
          DBG ('Presentation has slides: {0}' -f $slideDeck.Slides.Count)

      
          if (-not $asPDF) {

            $outFile = [System.IO.Path]::ChangeExtension($pptFile, 'xps')

          } else {

            $outFile = [System.IO.Path]::ChangeExtension($pptFile, 'pdf')
          }

          if (Test-Path $outFile) { Remove-Item $outFile -Force }

          [System.Threading.Thread]::CurrentThread.CurrentCulture = [System.Globalization.CultureInfo] 'en-US'


          Add-Type -AssemblyName Microsoft.Office.Interop.PowerPoint
          Add-Type -AssemblyName Office

          DBG ('Initialize printing parameters')
          DBGSTART
          $slideDeck.PrintOptions.OutputType = [Microsoft.Office.Interop.PowerPoint.PpPrintOutputType]::"ppPrintOutput$printAs"
      
          # Note: not that we would always want a handout output, yet we still can configure this as default
          $slideDeck.PrintOptions.HandoutOrder = [Microsoft.Office.Interop.PowerPoint.PpPrintHandoutOrder]::ppPrintHandoutHorizontalFirst
   
          $slideDeck.PrintOptions.FitToPage = [Microsoft.Office.Core.MsoTriState]::msoTrue
  
          $slideDeck.PrintOptions.HighQuality = [Microsoft.Office.Core.MsoTriState]::msoTrue
          $slideDeck.PrintOptions.NumberOfCopies = 1
          [void] $slideDeck.PrintOptions.Ranges.Add(1, $slideDeck.Slides.Count)
          $slideDeck.PrintOptions.RangeType = [Microsoft.Office.Interop.PowerPoint.PpPrintRangeType]::ppPrintAll
  
          # we must do it synchronously in order to wait before .Close() and .Quit()
          $slideDeck.PrintOptions.PrintInBackground = [Microsoft.Office.Core.MsoTriState]::msoFalse
          $slideDeck.PrintOptions.FrameSlides = [Microsoft.Office.Core.MsoTriState]::msoTrue
          $slideDeck.PrintOptions.PrintColorType = [Microsoft.Office.Interop.PowerPoint.PpPrintColorType]::"ppPrint$printHow"
          $slideDeck.PrintOptions.PrintHiddenSlides = [Microsoft.Office.Core.MsoTriState]::msoFalse

          $slideDeck.PrintOptions.ActivePrinter = "Microsoft XPS Document Writer";
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND


          if (-not $asPDF) {

            [string[]] $paramNames = @('PrintToFile', 'Collate')
            [object[]] $paramValues = @($outFile, [Microsoft.Office.Core.MsoTriState]::msoTrue)

            DBG ('Print out the document: {0}' -f $outFile)
            DBGSTART
            [void] $slideDeck.psobject.BaseObject.GetType().InvokeMember(
              'PrintOut', 
              [Reflection.BindingFlags]::InvokeMethod, 
              $null, 
              $slideDeck.psobject.BaseObject, 
              $paramValues, 
              $null, # ParameterModifiers
              ([System.Globalization.CultureInfo] 'en-US'), 
              $paramNames
              )
            DBGER $MyInvocation.MyCommand.Name $Error
            DBGEND
      
          } else {

            # Note: more parameters are probably not supported??
            #       I have just tested it with the following signature
            [string[]] $paramNames = @('Path', 'FixedFormatType', 'Intent', 'FrameSlides', 'HandoutOrder', 'OutputType', 'PrintHiddenSlides', 'PrintRange', 'RangeType', 'SlideShowName', 'IncludeDocProperties', 'KeepIRMSettings', 'DocStructureTags', 'BitmapMissingFonts') #, 'UseISO19005_1', 'ExternalExporter')
            [object[]] $paramValues = @(
              $outFile, 
              [Microsoft.Office.Interop.PowerPoint.PpFixedFormatType]::ppFixedFormatTypePDF, 
              [Microsoft.Office.Interop.PowerPoint.PpFixedFormatIntent]::ppFixedFormatIntentScreen, 
              $slideDeck.PrintOptions.FrameSlides,
              $slideDeck.PrintOptions.HandoutOrder,
              $slideDeck.PrintOptions.OutputType,
              $slideDeck.PrintOptions.PrintHiddenSlides,
              $slideDeck.PrintOptions.Ranges.Item(1),
              $slideDeck.PrintOptions.RangeType,
              '',
              $false,
              $false,
              $false,
              $false
              )

            DBG ('Export the document: {0}' -f $outFile)
            DBGSTART
            [void] $slideDeck.psobject.BaseObject.GetType().InvokeMember(
              'ExportAsFixedFormat', 
              [Reflection.BindingFlags]::InvokeMethod, 
              $null, 
              $slideDeck.psobject.BaseObject, 
              $paramValues, 
              $null, # ParameterModifiers
              ([System.Globalization.CultureInfo] 'en-US'), 
              $paramNames
              )
            DBGER $MyInvocation.MyCommand.Name $Error
            DBGEND
          }


          DBG ('Close the slide deck')
          DBGSTART
          $slideDeck.Close()
          #[void] [System.Runtime.Interopservices.Marshal]::ReleaseComObject($slideDeck)
          $slideDeck = $null
          DBGER $MyInvocation.MyCommand.Name $Error
          DBGEND
        }

        DBG ('Quit the PowerPoint application')
        DBGSTART
        $ppt.Quit()
        #[void] [System.Runtime.Interopservices.Marshal]::ReleaseComObject($ppt)
        $ppt = $null
        DBGER $MyInvocation.MyCommand.Name $Error
        DBGEND
      }

      Release-ComList ([ref] $comList)
  }

  DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $outFile }
  DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path $outFile) }
  return $outFile
}


function global:Try-LdapPasswordsFast (
    [string] $dc, 
    [string] $login, 
    [string] $domain, 
    [switch] $authBasic, 
    [int] $tryLength = 5,
    [string] $charSet = 'abcdefgh' #'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890'
    )
{
  if (([AppDomain]::CurrentDomain.GetAssemblies() | % { $_.Evidence.Name }) -notcontains 'System.DirectoryServices.Protocols') {

    [void] ([System.Reflection.Assembly]::LoadWithPartialName('System.DirectoryServices.Protocols'))
  }

  [System.DirectoryServices.Protocols.LdapConnection] $conn = $null
  [System.Management.Automation.ActionPreference] $errorActionBackup = $global:errorActionPreference
  $global:errorActionPreference = [System.Management.Automation.ActionPreference]::Stop

  try {

    if ($authBasic -and ($dc -notlike '?*:?*')) {

      $dc = $dc + ':636'
    }

    $conn = New-Object System.DirectoryServices.Protocols.LdapConnection $dc

    if ($authBasic) {

      $conn.SessionOptions.ProtocolVersion = 3
      $conn.SessionOptions.Signing = $false
      $conn.SessionOptions.Sealing = $false
      $conn.SessionOptions.SecureSocketLayer = $true
      $conn.AuthType = [DirectoryServices.Protocols.AuthType]::Basic

    } else {

      $conn.SessionOptions.ProtocolVersion = 3
      $conn.SessionOptions.Signing = $true
      $conn.SessionOptions.Sealing = $false
      $conn.SessionOptions.SecureSocketLayer = $false
      $conn.AuthType = [DirectoryServices.Protocols.AuthType]::Ntlm
    }

    [double] $pwdCount = 1;
    for ($i = 0; $i -lt $tryLength; $i++) { $pwdCount = $pwdCount * $charSet.Length }
    Write-Host ('Will try passwords: len = {0} | charset = {1} | pwds = {2}' -f $tryLength, $charSet.Length, $pwdCount)


    [byte[]] $charMatrix = New-Object byte[] $tryLength
    for ($i = 0; $i -lt $charMatrix.Length; $i ++) { $charMatrix[$i] = 0 }

    [byte] $positionMax = $charSet.Length - 1
    [int] $iteration = 0
    [string] $foundPwd = [string]::Empty
    [System.Text.StringBuilder] $pwdMachine = New-Object System.Text.StringBuilder $charMatrix.Length
    for ($i = 0; $i -lt $charMatrix.Length; $i ++) { [void] $pwdMachine.Append('.') }

    $error.Clear()
    $dtStart = [DateTime]::Now

    do {

      $iteration ++

      [byte] $i = 0
      while ($i -lt $charMatrix.Length) {

        if ($charMatrix[$i] -eq $positionMax) {

          $charMatrix[$i] = 0
          $i ++
          continue
          
        } else {

          $charMatrix[$i] = $charMatrix[$i] + 1
          break
        }
      }

      for ($k = 0; $k -lt $charMatrix.Length; $k ++)
      {
        $pwdMachine[$k] = $charSet[$charMatrix[$k]]
      }

      $onePwd = $pwdMachine.ToString()
    
      $cred = New-Object System.Net.NetworkCredential $login, $onePwd, $domain
      [bool] $check = $true

      try {

        $conn.Bind($cred)

      } catch {

        #Write-Host ('Error on password: {0} | {1}' -f $onePwd, $_.Exception.Message)
        $check = $false
      }

      if ($check) {

        $foundPwd = $onePwd
        break
      }

      if (($iteration % 27000) -eq 0) {

        $dtProcessDiff = [DateTime]::Now - $dtStart
        Write-Host ('Progress at: {0,8:D} | {1} | {2,7:N1} min | pwds/sec = {3:N0}' -f $iteration, $onePwd, $dtProcessDiff.TotalMinutes, (([double] $iteration) / $dtProcessDiff.TotalSeconds))
      }

    } while ($i -lt $charMatrix.Length)

    $dtEnd = [DateTime]::Now

    Write-Host ('Time stats: iterations = {0} | start = {1} | end = {2} | took = {3:N1} min' -f $iteration, $dtStart.ToString('yyyy-MM-dd HH:mm:ss'), $dtEnd.ToString('yyyy-MM-dd HH:mm:ss'), ($dtEnd - $dtStart).TotalMinutes)
    
    if ([string]::IsNullOrEmpty($foundPwd)) {

      Write-Host ('Didnt find the password')

    } else {

      Write-Host ('Found password: {0}' -f $foundPwd)
    }
  
  } catch {

    Write-Host ('Error: {0}' -f $_.Exception.Message)

  } finally {

    if (-not ([object]::Equals($null, $conn))) {

      $conn.Dispose()
    }

    $global:errorActionPreference = $errorActionBackup
  }
}


function global:Try-LdapPassword ([string] $path, [string] $login, [string] $pwd, [string] $security = 'Secure,Signing')
{
<#
.DESCRIPTION
  security: AuthenticationTypes enumeration = None (simple bind), Secure+Singing, Secure+Sealing, SecureSocketsLayer
            must NOT combine Sealing+Signing because then only the Signing takes effect while the communication is not encrypted
#>

  $ErrorActionPreference = 'SilentlyContinue'
  $error.Clear()

  [bool] $worked = $false

  try {
   
    $theObject = $null
    $theObject = New-Object DirectoryServices.DirectoryEntry (Normalize-AdsiPath $path), $login, $pwd, $security

    if (Is-NonNull $theObject) {

      [void] $theObject.RefreshCache('name')
      [void] $theObject.Close()
      [void] $theObject.Dispose()
      $theObject = $null
    }
  
    $worked = $error.Count -eq 0
  }

  catch {

  }

  $ErrorActionPreference = 'Continue'

  return $worked
}


function global:Try-LdapAllPasswords (
  [string] $path, 
  [string] $login, 
  [int] $pwdChars, 
  [string] $security = 'Secure,Signing', 
  [byte[]] $charSet = ((48..57) + (65..90) + (97..122)))
{
<#
.DESCRIPTION
  security: AuthenticationTypes enumeration = None (simple bind), Secure+Singing, Secure+Sealing, SecureSocketsLayer

  charSet: (48..57) + (65..90) + (97..122)) = 0-9, A-Z, a-z
           (32..126)                        =  !"# ... xyz{|}~
#>

  [byte[]] $charIndexes = New-Object byte[] $pwdChars
  [byte[]] $pwdSample = New-Object byte[] $pwdChars

  $charSetMaxIdx = $charSet.Count - 1

  $lastSample = [DateTime]::Now
  [double] $totalPwds = [Math]::Pow($charSet.Count, $pwdChars)
  Write-Host ('Passwords: {0}' -f $totalPwds)
  Write-Host ('Start: {0:s}' -f $lastSample)

  $hit = $false
  $i = 1
  $j = 0
  $countSet = 1000
  while ((-not $hit) -and ($j -lt $pwdChars)) {

    for ($k = 0; $k -lt $pwdChars; $k ++) {

      $pwdSample[$k] = $charSet[$charIndexes[$k]]
    }

    $onePwd = [Text.Encoding]::ASCII.GetString($pwdSample)
    
    $hit = Try-LdapPassword $path $login $onePwd $security

    if (($i % $countSet) -eq 0) { 
  
      $newSample = [DateTime]::Now
      $tookSeconds = ($newSample - $lastSample).TotalSeconds
      Write-Host ('{0,6}: {1:s}, delta = {2:N1} sec, overall = {3:N1} years' -f $i, $newSample, $tookSeconds, (($totalPwds / $countSet) * $tookSeconds / 60 / 24 / 365))
      $lastSample = $newSample
    }

    $i ++

    $j = 0
    while ($j -lt $pwdChars) {

      if ($charIndexes[$j] -eq $charSetMaxIdx) { 

        $charIndexes[$j] = 0
        $j ++
 
      } else {

        $charIndexes[$j] = $charIndexes[$j] + 1 
        break
      }
    }
  }

  Write-Host ('End: {0:s}' -f [DateTime]::Now)
}


function global:Download-McpTranscript (
  [string] $trainerName,
  [string] $transcriptId,
  [string] $accessCode,
  [string] $transcriptOutputName,
  [string] $verifyMcpId,
  [string] $testDifferentUrl,
  [string] $transcriptValidationUrl = 'https://mcp.microsoft.com/Anonymous/Transcript/Validate'
  )
{
  DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator))


  $outTranscript = New-Object PSCustomObject
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name trainer -Value $trainerName
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name outputName -Value $transcriptOutputName
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name transcriptId -Value $transcriptId
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name accessCode -Value $accessCode
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name mcpIdShouldBe -Value $verifyMcpId
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name downloaded -Value $false
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name outputFile -Value $null
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name validTranscript -Value $false
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name mcpId -Value $null
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name lastActivity -Value $null
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name mctValidity -Value $null
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name isMCT -Value $false
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name courseCount -Value 0
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name courseList -Value $null
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name examCount -Value 0
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name examList -Value $null
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name certCount -Value 0
  Add-Member -InputObject $outTranscript -MemberType NoteProperty -Name certList -Value $null


  if (Is-ValidString $testDifferentUrl) {

    DBG ('Will go for a custom test url: {0}' -f $testDifferentUrl)
    $url = $testDifferentUrl
  
  } else {

    DBG ('Will go for the default download url')

    # old one:                           'https://mcp.microsoft.com/authenticate/validatemcp.aspx'
    # user starting page:                'https://mcp.microsoft.com/Anonymous//Transcript/Validate'
    # landing page with the transcript:  'https://mcp.microsoft.com/Anonymous/Transcript/Share'
    $url = $transcriptValidationUrl
  }

<# Old version

  $postRes = Download-WebPage $url -returnByteArray $true -postParams @{
    '__EVENTTARGET' = $null ;
    '__EVENTARGUMENT' = $null ;
    '__VIEWSTATE' = '/wEPDwUJLTc1OTI2MjA0ZGTmfTEgdWV3s7bUFtzORGCoGZeRjJ+olhmfqjmmgNSqCg==' ;
    '__EVENTVALIDATION' = '/wEWBAKGy/bnDAKHqt+ACQK6kInoAQLii9zeA6mmkrsn8cIWJMCYWxHV5HnxD8vWGmy/yk5WeXpHnjH2' ;
    'TxtTranscriptAccessUserId' = $transcriptId ;
    'TxtMCPValidationCode' = $accessCode ;
    'BtnSubmit' = 'Submit'
    }
#>

  $postRes = Download-WebPage $url -returnByteArray $true -postParams @{
    'transcriptId' = $transcriptId ;
    'accessCode' = $accessCode
    }


  if (Is-NonNull $postRes) {
     
    $outTranscript.downloaded = $true

    DBG ('Obtained response: {0} bytes | {1:N1} kB' -f $postRes.Count, ($postRes.Count / 1KB))
    
    $transcriptHTMLFile = Get-DataFileApp $transcriptOutputName $null '.htm'
    DBG ('Saving repsonse to: {0}' -f $transcriptHTMLFile)
    
    #DBGIF ('The output file exists: {0}' -f $transcriptHTMLFile) { Test-Path $transcriptHTMLFile }
    if (Test-Path $transcriptHTMLFile) {

      DBGSTART
      Remove-Item $transcriptHTMLFile -Force
      DBGER $MyInvocation.MyCommand.Name $error
      DBGEND
    }

    DBGSTART
    #$postRes | Set-Content $transcriptHTMLFile -Encoding Byte -EA SilentlyContinue -EV er
    [System.IO.File]::WriteAllBytes($transcriptHTMLFile, $postRes)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    if (Test-Path $transcriptHTMLFile) {

      $outTranscript.outputFile = $transcriptHTMLFile
    }
  }



  if (Is-ValidString $outTranscript.outputFile) {

    DBG ('Transcript HTML exists, going to open it with HTML Agility Pack')

    DBGSTART
    Add-Type -Path "$baseDir\HtmlAgilityPack.Dll"
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    DBGSTART
    $html = New-Object HtmlAgilityPack.HtmlDocument
    $html.Load($outTranscript.outputFile)
    DBGER $MyInvocation.MyCommand.Name $error
    DBGEND

    if (Is-NonNull $html) {

      DBG ('HTML contents to validate: //title = {0}' -f (Trim-SafeWhitespace $html.DocumentNode.SelectSingleNode('//title').InnerText))
      DBG ('HTML contents to validate: //author = {0}' -f (Trim-SafeWhitespace $html.DocumentNode.SelectSingleNode('//meta[@name="Author" and @content="Microsoft Corporation"]').OuterHtml))
      
      $lastActivity = (Trim-SafeWhitespace $html.DocumentNode.SelectSingleNode('//td[@id="snLastActivities"]').InnerText)

      DBG ('HTML contents to validate: //snLastActivities = {0}' -f $lastActivity)

      if (($true) -and
          ($html.DocumentNode.SelectSingleNode('//title').InnerText -eq 'Microsoft Certified Professional') -and
          (Is-NonNull $html.DocumentNode.SelectSingleNode('//meta[@name="Author" and @content="Microsoft Corporation"]')) -and       
          ($lastActivity -like '*Last Activity Recorded : * Microsoft Certification ID : *') -and       
         ($true)) {

        DBG ('Transcript determined as valid. Proceed with its data')
        $outTranscript.validTranscript = $true

      } else {

        DBGIF ('Invalid transcript format. No data to be parsed: {0}' -f $trainerName) { $true }
        $outTranscript.validTranscript = $false
      }

    
      if ($outTranscript.validTranscript) {

        DBG ('Determine last activity date')
        $mcpActivityMatch = [RegEx]::Match($lastActivity, 'Last Activity Recorded \: (.*, \d\d\d\d)\s')
        DBGIF $MyInvocation.MyCommand.Name { -not $mcpActivityMatch.Success }

        if ($mcpActivityMatch.Success) {

          DBG ('Last activity date matched: {0}' -f $mcpActivityMatch.Groups[1].Value)
          DBGSTART
          $outTranscript.lastActivity = [DateTime]::Parse($mcpActivityMatch.Groups[1].Value, (New-Object System.Globalization.CultureInfo 'en-us'))
          DBGER $MyInvocation.MyCommand.Name $error
          DBGEND

          DBG ('Last activity date parsed: {0:d}' -f $outTranscript.lastActivity)
        }

        DBG ('Determine MCP ID')
        $mcpIDMatch = [RegEx]::Match($lastActivity, 'Microsoft Certification ID \: (\d+)\Z')
        DBGIF $MyInvocation.MyCommand.Name { -not $mcpIDMatch.Success }

        if ($mcpIDMatch.Success) {

          $outTranscript.mcpId = $mcpIDMatch.Groups[1].Value
          DBG ('MCP ID found: {0}' -f $outTranscript.mcpId)
        }
      }


      if (Is-ValidString $outTranscript.mcpId) {

        $mctHistoryText = (Trim-SafeWhitespace $html.DocumentNode.SelectSingleNode('//div[@id="divMCTHistory"]').InnerText)
        DBG ('Going to determine MCT status: {0}' -f $mctHistoryText)

        $mctStatusMatch = [RegEx]::Match($mctHistoryText, 'MICROSOFT CERTIFIED TRAINER CERTIFICATION HISTORY')
        #DBGIF $MyInvocation.MyCommand.Name { -not $mctStatusMatch.Success }

        if ($mctStatusMatch.Success) {

          DBG ('Going to check MCT status to be Current')

          if (Is-NonNull $html.DocumentNode.SelectSingleNode('//div[@id="divMCTHistory"]/table/tr/td/*[. = "Current"]')) {

            $outTranscript.isMCT = $true
          
          } else {

            DBG ('There is not Current marker, we must go for expiration dates')

            $lastestMctExpirationDate = [DateTime]::Parse('1990-01-01')
            foreach ($oneMctExpirationMatch in [regex]::Matches($mctHistoryText, '(\d\d/\d\d/\d\d\d\d)+')) { 
            
              DBG ('One MCT expiration date: {0}' -f $oneMctExpirationMatch.Groups[1].Value)
              DBGSTART
              $oneMctExpirationDate = [DateTime]::Parse($oneMctExpirationMatch.Groups[1].Value, (New-Object System.Globalization.CultureInfo 'en-us'))
              DBGER $MyInvocation.MyCommand.Name $error
              DBGEND
              DBG ('One MCT expiration date parsed: {0:d}' -f $oneMctExpirationDate)
              
              if ($oneMctExpirationDate -gt $lastestMctExpirationDate) {

                $lastestMctExpirationDate = $oneMctExpirationDate
              }
            }

            DBG ('Latest MCT expiration date: {0:d}' -f $lastestMctExpirationDate)

            $outTranscript.mctValidity = $lastestMctExpirationDate

            if ($lastestMctExpirationDate -gt (Get-Date)) {

              $outTranscript.isMCT = $true
            }
          }

          DBG ('Is MCT current: {0}' -f $outTranscript.isMCT)
        }
      }


      $examsListMatch = [RegEx]::Match($html.DocumentNode.SelectSingleNode('//div[@id="divExamsCompleted"]').InnerText, 'MICROSOFT CERTIFICATION EXAMS COMPLETED SUCCESSFULLY')
      DBG ('Exam list valid: {0}' -f $examsListMatch.Success) 

      if ($examsListMatch.Success) {

        DBG ('Going to process the exam list')

        [System.Collections.ArrayList] $examList = @()
        
        $i = 1
        foreach ($oneExamDef in ($html.DocumentNode.SelectNodes('//div[@id="divExamsCompleted"]/table/tr/td/span[@class="transcriptContent"]') | Select-Object -Expand InnerText)) {

          if ($oneExamDef -like '[0-9][0-9][0-9]') {

            DBGIF ('Weird exam number possition: {0}' -f $i) { ($i % 3) -ne 1 }
            DBG ('Exam ID: {0}' -f $oneExamDef)

            Add-ListUnique ([ref] $examList) $oneExamDef
          
          } elseif (($i % 3) -eq 2) {

            $one = Replace-HtmlSymbolEntitiesFromHtml $oneExamDef

            DBGIF ('Weird exam name: {0}' -f $oneExamDef)  { ($one -like "*[~!@$%^*_={}`"|;``[``]\<>?]*") -or ($one -like '*``*') }
            DBGIF ('Weird exam name: {0}' -f $one) { $one -notlike '*[a-z]*' }
            $oneNoSpecChar = [regex]::Replace($one, '[^a-zA-Z0-9]', ' ')
            $oneNoWhiteSpace = Trim-SafeWhitespace $oneNoSpecChar

            DBGIF ('Weird exam name: {0}' -f $oneNoWhiteSpace) { $oneNoWhiteSpace.Length -le 10 }
            DBG ('Exam name: {0}' -f $oneNoWhiteSpace)
          
          } elseif (($i % 3) -eq 0) {

            DBGSTART
            [DateTime] $examDate = $null
            $examDate = [DateTime]::Parse($oneExamDef, (New-Object System.Globalization.CultureInfo 'en-us'))
            DBGEND

            DBGIF ('Weird exam date: {0}' -f $oneExamDef) { Is-Null $examDate }
            DBG ('Exam date: {0:d}' -f $examDate)
          }

          $i ++
        }

        DBG ('Exams found: {0}' -f $examList.Count)
        DBGIF $MyInvocation.MyCommand.Name { $examList.Count -lt 1 }

        $examList.Sort()
        $outTranscript.examCount = $examList.Count
        $outTranscript.examList = Format-MultiValue $examList
      }



      # divActiveCertifications, divRetiredCertifications, divLegacyCertifications
      $certListMatch = [RegEx]::Match($html.DocumentNode.SelectSingleNode('//div[(@id="divActiveCertifications") or (@id="divRetiredCertifications") or (@id="divLegacyCertifications")]').InnerText, 'MICROSOFT CERTIFICATIONS')
      DBG ('Certification list valid: {0}' -f $certListMatch.Success)

      if ($certListMatch.Success) {

        DBG ('Going to process certifications')

        [System.Collections.ArrayList] $certList = @()
        
        $certCategory = ''

        foreach ($oneCertDef in ($html.DocumentNode.SelectNodes('//div[@id="divActiveCertifications" or @id="divRetiredCertifications" or @id="divLegacyCertifications"]/table/tr'))) {

          #   
          #     
          #       
          #         Microsoft? Certified IT Professional
          #         
# # # # # # # #
# # # Enterprise Desktop Administrator on Windows 7 $oneCertCategory = Trim-SafeWhiteSpace ([regex]::Replace((Replace-HtmlSymbolEntitiesFromHtml $oneCertDef.SelectSingleNode('.//span[@class="transcriptBold"]').InnerText), '[^a-zA-Z0-9]', ' ')) $oneCertSpec = Trim-SafeWhiteSpace ([regex]::Replace((Replace-HtmlSymbolEntitiesFromHtml $oneCertDef.SelectSingleNode('./td/table/tr[2]/td[3]').InnerText), '[^a-zA-Z0-9]', ' ')) DBG ('Certificate category: {0}' -f $oneCertCategory) DBG ('Certificate spec: {0}' -f $oneCertSpec) if ((Is-ValidString $oneCertCategory) -and ($oneCertCategory -ne 'Trainer')) { DBGIF 'Invalid certificate spec' { Is-ValidString $oneCertSpec } DBGIF 'Invalid certificate spec' { ($oneCertCategory.Length -le 15) } $certCategory = $oneCertCategory } if ((Is-ValidString $oneCertSpec) -and ($oneCertSpec -ne 'MCT Enrollment')) { DBGIF 'Invalid certificate spec' { $oneCertSpec.Length -le 8 } DBGIF 'Invalid certificate category' { Is-EmptyString $certCategory } if ($certCategory -ne $oneCertSpec) { $certSpecification = '{0}: {1}' -f $certCategory, $oneCertSpec } else { $certSpecification = '{0}' -f $certCategory } Add-ListUnique ([ref] $certList) $certSpecification DBG ('Full certification name: {0}' -f $certSpecification) } } $certList.Sort() DBG ('Certification count: {0}' -f $certList.Count) $outTranscript.certCount = $certList.Count $outTranscript.certList = Format-MultiValue $certList } $courseListMatch = [RegEx]::Match($html.DocumentNode.SelectSingleNode('//div[@id="divCoursesEligibleToTeach"]').InnerText, 'MICROSOFT COURSES CERTIFIED TO TEACH') DBG ('Course list valid: {0}' -f $courseListMatch.Success) if ($courseListMatch.Success) { DBG ('Going to process the course list') [System.Collections.ArrayList] $courseList = @() $i = 1 foreach ($oneCourseDef in ($html.DocumentNode.SelectNodes('//div[@id="divCoursesEligibleToTeach"]/table/tr/td/span[@class="transcriptContent"]') | Select-Object -Expand InnerText)) { $oneCourseDef = (Trim-Safe $oneCourseDef) if ($oneCourseDef -like '[0-9][0-9][0-9]*') { $courseIDs = $oneCourseDef.Split(',') | % { $oneCourseId = $_.Trim().Trim('()').Trim() DBGIF ('Weird course ID: {0}' -f $_) { Is-EmptyString $oneCourseId } if (Is-ValidString $oneCourseId) { DBGIF ('Weird course ID: {0}' -f $oneCourseId) { $oneCourseId -match '[^0-9]' } DBGIF ('Weird course ID: {0}' -f $oneCourseId) { $oneCourseId.Length -gt 5 } $oneCourseIdPure = [regex]::Replace($oneCourseId, '[^a-zA-Z0-9]', '').Trim() DBGSTART $oneCourseIdInt = 0 $oneCourseIdInt = [int]::Parse($oneCourseIdPure) DBGEND DBGIF ('Weird course ID: {0}' -f $oneCourseId) { $oneCourseId -eq 0 } $oneCourseIdPure } } DBGIF ('Weird course ID possition: {0}' -f $i) { ($i % 2) -ne 1 } DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $courseIDs) -lt 1 } DBG ('Course IDs: {0}' -f ($courseIDs -join '|')) if ((Get-CountSafe $courseIDs) -gt 1) { [void] $courseList.AddRange($courseIDs) } else { Add-ListUnique ([ref] $courseList) $courseIDs } } else { $one = Replace-HtmlSymbolEntitiesFromHtml $oneCourseDef # note: backtick cannot be matched in -like operator with [] # backtick cannot be matched in -like operator with "*``*" # backtick can be matched in -like operator with '*``*' DBGIF ('Weird course name: {0}' -f $one) { ($one -like "*[~!@$%^*_={}`"|;``[``]\<>?]*") -or ($one -like '*``*') } # allowed: #+():,./' # C# # COM+ DBGIF ('Weird course name: {0}' -f $one) { $one -notlike '*[a-z]*' } DBGIF ('Weird course name possition: {0}' -f $i) { ($i % 2) -ne 0 } #$oneNoSpecChar = [regex]::Replace($oneNoWhiteSpace, '[^a-zA-Z0-9]', '') $oneNoSpecChar = [regex]::Replace($one, '[^a-zA-Z0-9]', ' ') $oneNoWhiteSpace = Trim-SafeWhitespace $oneNoSpecChar DBGIF ('Weird course name: {0}' -f $oneNoWhiteSpace) { $oneNoWhiteSpace.Length -le 15 } DBG ('Course name: {0}' -f $oneNoWhiteSpace) } $i ++ <# if ($one -like '(*') { $one = $one.SubString(5) } if ($one -like '*)') { $one = $one.SubString(0, $one.Length - 5) } $one #> } $courseList.Sort() DBGIF $MyInvocation.MyCommand.Name { $courseList.Count -lt 1 } #DBGIF $MyInvocation.MyCommand.Name { $courseList[0] -ne 'Course #' } DBG ('Courses found: {0}' -f $courseList.Count) $outTranscript.courseCount = $courseList.Count $outTranscript.courseList = Format-MultiValue $courseList } } } DBGIF $MyInvocation.MyCommand.Name { $outTranscript.isMCT -and (Is-EmptyString $outTranscript.courseList) } DBGIF $MyInvocation.MyCommand.Name { (-not $outTranscript.isMCT) -and (Is-ValidString $outTranscript.courseList) } DBGIF $MyInvocation.MyCommand.Name { (Is-ValidString $outTranscript.mcpIdShouldBe) -and (Is-ValidString $outTranscript.mcpId) -and ($outTranscript.mcpId -ne $outTranscript.mcpIdShouldBe) } return $outTranscript } function global:Sign-ISE ([string] $signingCertSubjectMail = 'ondrej@sevecek.com') { [System.Security.Cryptography.X509Certificates.X509Certificate2] $signingCert = $null $signingCert = dir Cert:\CurrentUser\My -CodeSigningCert | ? { $_.Subject -like "*E=$signingCertSubjectMail*" | Sort NotAfter | Select -Last 1 } if ($signingCert -ne $null) { $openFiles = $psISE.CurrentPowerShellTab | % { $_.Files } foreach ($oneOpenFile in $openFiles) { $oneFilePath = $oneOpenFile.FullPath $oneFileLine = $oneOpenFile.Editor.CaretLine $oneFileColumn = $oneOpenFile.Editor.CaretColumn if (-not $oneOpenFile.IsSaved) { [void] $oneOpenFile.Save() } if (([System.IO.Path]::GetExtension($oneFilePath) -eq '.ps1') -and ((Get-AuthenticodeSignature $oneFilePath).Status -ne 'Valid')) { [void] $psISE.CurrentPowerShellTab.Files.Remove($oneOpenFile) [void] (Set-AuthenticodeSignature $oneFilePath -Certificate $signingCert -TimestampServer http://timestamp.verisign.com/scripts/timstamp.dll) $reloadedFile = $psISE.CurrentPowerShellTab.Files.Add($oneFilePath) $reloadedFile.Editor.SetCaretPosition($oneFileLine, $oneFileColumn) } } } else { throw 'Error: no signing certificate found' } } function global:Encode-BytesToBerMultibyte ([byte[]] $originalBytes, [switch] $trimZeros) { # Note: the function encodes byte array into a BER multibyte encoding # which splits the original eight bit bytes into seven bit bytes and marks the continuation # with a leading one bit [byte] $len = $originalBytes.Length #while ($originalBytes[$len - 1] -eq 0) { $len-- } #DBGX ('Len: {0}' -f $len) DBGIF $MyInvocation.MyCommand.Name { $len -lt 1 } [byte] $newLen = [Math]::Floor((($len * 8) / 7)) # Note: $newLen might be several bytes longer than the $len, not just by one longer if ((($len * 8) % 7) -gt 0) { $newLen++ } #DBGX ('Newlen: {0}' -f $newLen) [byte[]] $newBytes = New-Object byte[] $newLen [byte] $oneEncodedByte = 0 [byte] $oneEncodedByteOverflow = 0x00 # Note: the first (last) byte does NOT have the most significant bit set [byte] $baseIdx = 0 [byte] $overflowsInserted = 0 #DBGX ('len = {0}' -f $len) do { for ($i = 0; $i -lt ($len - $baseIdx); $i ++) { #DBGX ('base = {0}, i = {1}' -f $baseIdx, $i) #$oneEncodedByte = ((($originalBytes[($baseIdx + $i)] -shl ($i + 1)) -band 0xFE) -shr 1) -bor $oneEncodedByteOverflow $oneEncodedByte = [Sevecek.PowerShellUtils.Bitwise]::Shr(([Sevecek.PowerShellUtils.Bitwise]::Shl($originalBytes[($baseIdx + $i)] , ($i + 1)) -band 0xFE) , 1) -bor $oneEncodedByteOverflow #DBGX ('encoded = {0}' -f $oneEncodedByte) $newBytes[$newBytes.Length - ($baseIdx + $i) - 1 - $overflowsInserted] = $oneEncodedByte #DBGX ('newBytes = {0}' -f ([BitConverter]::ToString($newBytes))) #$oneEncodedByteOverflow = ($originalBytes[($baseIdx + $i)] -shr (7 - $i)) -bor 0x80 $oneEncodedByteOverflow = [Sevecek.PowerShellUtils.Bitwise]::Shr($originalBytes[($baseIdx + $i)] , (7 - $i)) -bor 0x80 #DBGX ('overflow = {0}' -f $oneEncodedByteOverflow) # Note: $i == 6 if ((7 - $i) -eq 1) { #DBGX ('overlap') # Note: we have reached the point where we shifted the original to the right only by 1 # which means that all its 7 leftmost bits 8-7-6-5-4-3-2 went to the overflow # and thus we would not be able to fill the overflow with anything else on the next round # So we just save the overflow as it is and restart with overflow 10000000 $newBytes[$newBytes.Length - ($baseIdx + $i) - 2 - $overflowsInserted] = $oneEncodedByteOverflow #DBGX ('newBytes = {0}' -f ([BitConverter]::ToString($newBytes))) $oneEncodedByteOverflow = 0x80 # Note: any non-last byte DOES have the most significant bit set $overflowsInserted++ $i++ break } } $baseIdx += $i } while ($baseIdx -lt $len) #DBGX ('fin') #if ($oneEncodedByteOverflow -ne 0x80) { #DBGX ('remaining overflow: base = {0} | length = {1} | insertions = {2}' -f $baseIdx, $newBytes.Length, $overflowsInserted) if (($newBytes.Length - $baseIdx - $overflowsInserted - 1) -ge 0) { $newBytes[$newBytes.Length - $baseIdx - $overflowsInserted - 1] = $oneEncodedByteOverflow #DBGX ('newBytes = {0}' -f ([BitConverter]::ToString($newBytes))) } #} DBGIF $MyInvocation.MyCommand.Name { $newBytes[0] -eq 0 } if ($trimZeros) { [byte] $nonZeroIdx = 0 while ($newBytes[$nonZeroIdx] -eq 0x80) { $nonZeroIdx++ } $newBytes = $newBytes[$nonZeroIdx..($newBytes.Length - 1)] } return ,$newBytes } function global:Decode-BytesFromBerMultibyte ([byte[]] $bytes, [switch] $possiblyTrimmedZeros) { [byte] $bytesLen = $bytes.Length [byte] $resultSize = 0 if (-not $possiblyTrimmedZeros) { [int] $maxOrigBits = 7 * ([int] $bytesLen) [int] $minOrigBits = 7 * (([int] $bytesLen) - 1) + 1 $maxOrigBytes = [Math]::Floor($maxOrigBits / 8) #if (($maxOrigBits % 8) -gt 0) { $maxOrigBytes++ } $minOrigBytes = [Math]::Floor($minOrigBits / 8) if (($minOrigBits % 8) -gt 0) { $minOrigBytes++ } #DBGX ('to decode: {0} | maxOrigBytes = {1} | minOrigBytes = {2}' -f $bytesLen, $maxOrigBytes, $minOrigBytes) # Note: if the encoded data contain all their original zeros (such as original of 35-82-00-00-00-00-00-00) would encode as 80-80-80-80-80-80-80-82-84-35 # then this assumption is correct and from result we will always be able to determine the orginal buffer size DBGIF ('Weird number of max/min bytes in the original data: len = {0} | max = {1} | min = {2}' -f $bytesLen, $maxOrigBytes, $minOrigBytes) { ($maxOrigBytes -ne $minOrigBytes) } $resultSize = [Math]::Max($maxOrigBytes, $minOrigBytes) } else { #DBGX ('to decode: {0} | {1}' -f ([BitConverter]::ToString($bytes)), $bytesLen) # Note: in case the encoded data were trimmed from left of the 0x80 zeros # then the assumptions are not necessary guaranteed and the higher one applies just be sure to have # buffer big enough, although not always necessary # Note: if the zero trimming occured yet before encoding, this would not be necessary # but for example when encoding OIDs, the 0x80s are trimmed from the left only after encoding each individual OID arc #DBGIF ('Weird number of max/min bytes in the original data: len = {0} | max = {1} | min = {2}' -f $bytesLen, $maxOrigBytes, $minOrigBytes) { ($maxOrigBytes -lt ($minOrigBytes - 1)) -or ($maxOrigBytes -gt $minOrigBytes) } [int] $assumedOrigBits = 7 * ([int] $bytesLen) $assumedOrigBytes = [Math]::Floor($assumedOrigBits / 8) #DBGX ('assumedOrigBytes: {0}' -f $assumedOrigBytes) if (($assumedOrigBits % 8) -ne 0) { # Note: we might be missing 0x80 on the left with some original zero bits #[byte] $remainingZeros = (0xFF -shr (7 - ($assumedOrigBits % 8))) -shl (7 - ($assumedOrigBits % 8)) [byte] $remainingZeros = [Sevecek.PowerShellUtils.Bitwise]::Shl( ([Sevecek.PowerShellUtils.Bitwise]::Shr(0xFF, (7 - ($assumedOrigBits % 8)))) , (7 - ($assumedOrigBits % 8)) ) #DBGX ('remaining zeros: {0} | {1} | {2}' -f ($assumedOrigBits % 8), [Convert]::ToString($remainingZeros, 2), [Convert]::ToString($bytes[0], 2)) if (($bytes[0] -band $remainingZeros) -ne 0x80) { $resultSize = [Math]::Floor($assumedOrigBits / 8) + 1 #DBGX ('must increment: resultSize = {0}' -f $resultSize) } else { $resultSize = $assumedOrigBytes #DBGX ('length ok: resultSize = {0}' -f $resultSize) } } else { # Note: we are ok with enough length on the left $resultSize = $assumedOrigBytes #DBGX ('length ok: resultSize = {0}' -f $resultSize) } } #DBGX ('bytes: {0}' -f $bytesLen) #DBGX ('result size: {0}' -f $resultSize) [byte[]] $resultBytes = New-Object byte[] $resultSize [byte] $skippedOverlaps = 0 for ($i = $bytesLen - 1; $i -ge 0; $i--) { [byte] $iter = ($bytesLen - $i - 1) % 8 #DBGX ('i: {0}' -f $i) #DBGX ('iter: {0}' -f $iter) DBGIF $MyInvocation.MyCommand.Name { ($bytes[$i] -lt 0x80) -and ($iter -gt 0) } #[byte] $oneSepthtet = ($bytes[$i] -band 0x7F) -shr $iter [byte] $oneSepthtet = [Sevecek.PowerShellUtils.Bitwise]::Shr(($bytes[$i] -band 0x7F), $iter) [byte] $bitsFromNextSepthtet = 0 if ($i -gt 0) { DBGIF $MyInvocation.MyCommand.Name { $bytes[$i - 1] -lt 0x80 } #$bitsFromNextSepthtet = ($bytes[$i - 1] -shl (7 - $iter)) -band 0xFF $bitsFromNextSepthtet = ([Sevecek.PowerShellUtils.Bitwise]::Shl(($bytes[$i - 1]), (7 - $iter))) -band 0xFF } [byte] $resultIdx = $bytesLen - $i - 1 - $skippedOverlaps [byte] $resultByte = $oneSepthtet -bor $bitsFromNextSepthtet #DBGX ('one: {0}, next {1}' -f $oneSepthtet, $bitsFromNextSepthtet) #DBGX ('result idx: {0}' -f $resultIdx) #DBGX ('new resultByte: {0}' -f $resultByte) DBGIF $MyInvocation.MyCommand.Name { $resultIdx -gt $resultSize } DBGIF $MyInvocation.MyCommand.Name { ($resultIdx -ge $resultSize) -and ($resultByte -ne 0) } # Note: the $bytes[0] might contain only remaining zeros which do not # go into the $resultBytes as determined above with $assumedOrigBytes # in such a case we go one additional round here which we cannot store # into the $resultBytes though if ($resultIdx -lt $resultSize) { $resultBytes[$resultIdx] = $resultByte } # Note: $iter == 6 if ((7 - $iter) -eq 1) { # Note: at this point we have taken the all bits from the next field # and applied them here. So we have to skip the next input byte # because all its bits have been exhausted $skippedOverlaps++ $i -- } } return ,$resultBytes } function global:Encode-UInt64ToBerMultibyte ([UInt64] $uint, [switch] $trimZeros) { # Note: the function encodes a number (8 byte unsigned int) into a BER multibyte encoding # which splits the original eight bit bytes into seven bit bytes and markes the continuation # with a leading one #DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) # Note: this comes in least-significant-byte-first form [byte[]] $originalBytes = [BitConverter]::GetBytes($uint) [byte[]] $encodedBytes = Encode-BytesToBerMultibyte $originalBytes -trimZeros:$trimZeros return ,$encodedBytes } function global:Decode-UInt64FromBerMultibyte ([byte[]] $bytes, [switch] $possiblyTrimmedZeros) { # Note: this function decodes eight-byte array from input byte array encoded into BER format # Note: BER encodes the individual numbers into 7bit so it might be up to 10 bytes long DBGIF $MyInvocation.MyCommand.Name { $bytes.Length -gt 10 } [byte[]] $resultBytes = Decode-BytesFromBerMultibyte $bytes -possiblyTrimmedZeros:$possiblyTrimmedZeros DBGIF $MyInvocation.MyCommand.Name { $resultBytes.Length -gt 8 } [byte[]] $eightBytes = New-Object byte[] 8 [Array]::Copy($resultBytes, 0, $eightBytes, 0, [Math]::Min(8, $resultBytes.Length)) $resultBytes = $eightBytes [UInt64] $result = [BitConverter]::ToUInt64($resultBytes, 0) return $result } function global:Encode-BerOID ([string] $stringOID) { # Note: the method encodes a string OID (object identifier) representation into BER format # which treats the first two OID numbers differently from the rest of the numbers # x.y.a.b.c.d.e.f... (x*40+y).a.b.c.d.e.f.... # OID root arch (top-level arc) is limited to 0 or 1 or 2 only # the first two arcs are encoded together and every following arch is encoded individually # into BER format # maximum individual OID arc value is 2^64-1 in Windows which means we must go for UInt64 DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) Define-SevecekUtils DBGIF $MyInvocation.MyCommand.Name { Is-EmptyString $stringOID } DBGIF $MyInvocation.MyCommand.Name { $stringOID -notmatch '\A\d+\.\d+\.(\d+\.)*\d+\Z' } [string[]] $nodes = $stringOID.Split('.') DBGIF $MyInvocation.MyCommand.Name { $nodes.Length -lt 3 } [Collections.ArrayList] $bytes = @() [UInt64] $arcTopLevel = [UInt64]::Parse($nodes[0]) [UInt64] $arcSecond = [UInt64]::Parse($nodes[1]) # Note: OID top-level arcs are defined as only 0 or 1 or 2 DBGIF $MyInvocation.MyCommand.Name { $arcTopLevel -gt 2 } # Note: for OID top-level arcs 0 and 1, the second level arcs are limited to 0-39 # for OID top-level arc 2, the second level arc is not limited DBGIF $MyInvocation.MyCommand.Name { ($arcTopLevel -lt 2) -and ($arcSecond -gt 39) } [byte[]] $oneNumberBytes = Encode-UInt64ToBerMultibyte (($arcTopLevel * 40 + $arcSecond)) -trimZeros [void] $bytes.Add( $oneNumberBytes ) for ($i = 2; $i -lt $nodes.Length; $i ++) { [UInt64] $oneNumber = [UInt64]::Parse($nodes[$i]) $oneNumberBytes = Encode-UInt64ToBerMultibyte $oneNumber -trimZeros #DBGX ('One bytes: {0}' -f ([BitConverter]::ToString($oneNumberBytes))) [void] $bytes.Add( $oneNumberBytes ) } #DBGX ('List size: {0}' -f $bytes.Count) [byte[]] $resultBytes = @() foreach ($oneBytes in $bytes) { $resultBytes += $oneBytes } DBG ('Encoded BER OID: {0}' -f ([BitConverter]::ToString($resultBytes))) return $resultBytes } function global:Decode-BerOID ([byte[]] $berOID) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) # Note: OID top-level arcs are defined as only 0 or 1 or 2 # Note: for OID top-level arcs 0 and 1, the second level arcs are limited to 0-39 # for OID top-level arc 2, the second level arc is not limited # thus if the first BER encoded UInt64 values decode as the following: # 0-39 = 0.0-39... # 40-79 = 1.0-39... # 80-.. = 2.0-..... #DBGX ('bytes: {0}' -f ([BitConverter]::ToString($berOID))) Define-SevecekUtils DBG ('Encoded BER OID: {0}' -f ([BitConverter]::ToString($berOID))) $i = 0 [bool] $topLevelArc = $true [Text.StringBuilder] $oid = New-Object Text.StringBuilder while ($i -lt $berOID.Length) { $startBerUInt64 = $i while (($i -lt $berOID.Length) -and ($berOID[$i] -ge 0x80)) { $i++ } #DBGX ('found: start = {0} | end = {1}' -f $startBerUInt64, $i) # Note: each arc is limited to UInt64 meaning up to 10 bytes in BER encoding DBGIF $MyInvocation.MyCommand.Name { ($i - $startBerUInt64 + 1) -gt 10 } [byte[]] $oneBerArc = $berOID[$startBerUInt64..$i]; #DBGX ('oneBerArc: {0}' -f ([BitConverter]::ToString($oneBerArc))) [UInt64] $oneArc = Decode-UInt64FromBerMultibyte $oneBerArc -possiblyTrimmedZeros #DBGX ('oneArc: {0}' -f $oneArc) if ($topLevelArc) { if ($oneArc -le 39) { [void] $oid.Append(('0.{0}' -f $oneArc)) } elseif ($oneArc -le 79) { [void] $oid.Append(('1.{0}' -f ($oneArc - 40))) } else { [void] $oid.Append(('2.{0}' -f ($oneArc - 80))) } $topLevelArc = $false } else { [void] $oid.Append(('.{0}' -f $oneArc)) } #DBGX ('growing: {0}' -f $oid.ToString()) $i ++ } DBG ('Decoded OID: {0}' -f $oid.ToString()) return $oid.ToString() } function global:Clip-FileInHtml ([string] $fileName) { [string[]] $out = Get-Item $fileName | % { Encode-HtmlFile $_.FullName } DBG ('Sending lines in clipboard: {0}' -f $out.Length) # Note: the clip program if used as: $out | clip # actually looses unicode characters and replaces them with ? #$out | clip [string] $tempFile = [IO.Path]::ChangeExtension(([IO.Path]::GetTempFileName()), '.tmp.txt') DBG ('Saving the contents into a temporary file as well: {0}' -f $tempFile) $out | Set-Content -Path $tempFile -Encoding UTF8 $out | clip } function global:Escape-Xml ([string] $text) { [hashtable] $xmlEscapes = @{ '"' = '"'; '''' = '''; '<' = '<'; '>' = '>'; '&' = '&'; } foreach ($oneXmlEscape in $xmlEscapes.Keys) { $text = $text.Replace($oneXmlEscape, $xmlEscapes[$oneXmlEscape]) } return $text } function global:Backup-SpWebApplication ([string] $webApplicationUrl, [string] $backupRootFolder) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) DBG ('Load the Microsoft.SharePoint.PowerShell snap-in') Assert-SnapInSafe Microsoft.SharePoint.PowerShell if (Is-EmptyString $webApplicationUrl) { DBG ('Empty web application URL, ask for one') DBGSTART [string[]] $webApplications = Get-SPWebApplication | select -Expand Url DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Found web applications: #{0} | {1}' -f (Get-CountSafe $webApplications), ($webApplications -join ', ')) DBGIF $MyInvocation.MyCommand.Name { (Get-CountSafe $webApplications) -lt 1 } if ((Get-CountSafe $webApplications) -gt 0) { [Collections.ArrayList] $webAppsOffered = @() foreach ($oneWebApplication in $webApplications) { Add-AskerChoice -choices $webAppsOffered -name $oneWebApplication -id ($webAppsOffered.Count + 1) } $webApplicationUrl = Ask-UserForValueWithChoices -query 'Select one web application for backup' -choices $webAppsOffered -defaultChoiceId $webAppsOffered.Count } } if (Is-EmptyString $backupRootFolder) { $backupRootFolder = Ask-UserForValue -query 'Specify the backup folder root path' -mustBeSpecified $true } DBG ('Web application to backup: {0} | {1}' -f $webApplicationUrl, $backupRootFolder) DBGSTART DBGER $MyInvocation.MyCommand.Name $error DBGEND } function global:Export-SharePoint ([switch] $structureOnly) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) # # function Normalize-Uri ([string] $uri) { return $uri.Replace('\', '/').Trim().Trim('/').Trim('\').Trim() } function Get-UriParent ([string] $uri) { $uri = Normalize-Uri $uri [int] $lastIdx = $uri.LastIndexOf('/') if ($lastIdx -ge 0) { return (Normalize-Uri $uri.SubString(0, $lastIdx)) } else { return [string]::Empty } } function Get-UriLeaf ([string] $uri) { $uri = Normalize-Uri $uri [int] $lastIdx = $uri.LastIndexOf('/') if ($lastIdx -ge 0) { return (Normalize-Uri $uri.SubString(($lastIdx + 1), $uri.Length - $lastIdx - 1)) } else { return $uri } } function Encode-NowToNTFS () { return [DateTime]::Now.ToString('yyyy-MM-dd-HH-mm-ss') } function Encode-SiteUrlToNTFS ([string] $url) { # Note: site URL or managed path will never contain # or ~ $url = Normalize-Uri $url if (-not ([string]::IsNullOrEmpty($url))) { return $url.Replace('/', '#').Replace(':', '~') } else { return '#' } } function Get-SubPath ([string] $path, [string] $parent) { DBGIF $MyInvocation.MyCommand.Name { [string]::IsNullOrEmpty($path) } DBGIF $MyInvocation.MyCommand.Name { [string]::IsNullOrEmpty($parent) } DBGIF $MyInvocation.MyCommand.Name { $path -notlike "$($parent)\?*" } if ((-not ([string]::IsNullOrEmpty($path))) -and (-not ([string]::IsNullOrEmpty($parent))) -and ($path -like "$($parent)\?*")) { return $path.SubString(($parent.Length + 1)) } else { return $path } } # # Assert-SnapInSafe Microsoft.SharePoint.PowerShell # # DBG ('Going to load web application list') DBGSTART [string[]] $webAppsAll = $null $webAppsAll = Get-SPWebApplication | Select -Expand Url DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Loaded web applications: {0} | {1}' -f $webAppsAll.Length, ($webAppsAll -join ',')) DBGIF $MyInvocation.MyCommand.Name { $webAppsAll.Length -lt 1 } [Collections.ArrayList] $structure = @() if ($webAppsAll.Length -gt 0) { # # foreach ($oneWebApp in $webAppsAll) { DBG ('One web application: {0}' -f $oneWebApp) [PSCustomObject] $newWebApp = New-Object PSCustomObject Add-Member -Input $newWebApp -MemberType NoteProperty -Name url -Value (Normalize-Uri $oneWebApp) # # DBG ('Get the list of farm hostheader managed paths') [hashtable] $farmHostheaderMgtPaths = @{} DBGSTART [object[]] $farmManagedPaths = $null $farmManagedPaths = Get-SPManagedPath -HostHeader DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Found farm managed paths: {0}' -f $farmManagedPaths.Length, (($farmManagedPaths | Select -Expand Name) -join ',')) if ($farmManagedPaths.Length -gt 0) { foreach ($oneFarmManagedPath in $farmManagedPaths) { DBG ('One farm host header managed path: {0} | {1}' -f $oneFarmManagedPath.Name, $oneFarmManagedPath.Type) [PSCustomObject] $newMgtPath = New-Object PSCustomObject Add-Member -Input $newMgtPath -MemberType NoteProperty -Name siteColls -Value (New-Object System.Collections.ArrayList) switch ($oneFarmManagedPath.Type) { 'ExplicitInclusion' { Add-Member -Input $newMgtPath -MemberType NoteProperty -Name type -Value 'E' } 'WildcardInclusion' { Add-Member -Input $newMgtPath -MemberType NoteProperty -Name type -Value 'W' } default { DBGIF ('Weird farm hostheader managed path type: {0} | {1}' -f $oneFarmManagedPath.Type, $oneFarmManagedPath.Name) { $true } } } [void] $farmHostheaderMgtPaths.Add((Normalize-Uri $oneFarmManagedPath.Name), $newMgtPath) } } # # DBG ('Getting managed paths for the web app: {0}' -f $oneWebApp) [hashtable] $webappMgtPaths = @{} DBGSTART [object[]] $mgtPaths = $null $mgtPaths = Get-SPManagedPath -WebApplication $oneWebApp DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Found web application managed paths: {0} | {1}' -f $mgtPaths.Length, (($mgtPaths | Select -Expand Name) -join ',')) if ($mgtPaths.Length -gt 0) { foreach ($oneMgtPath in $mgtPaths) { DBG ('One managed path: {0} | {1}' -f $oneMgtPath.Name, $oneMgtPath.Type) [PSCustomObject] $newMgtPath = New-Object PSCustomObject Add-Member -Input $newMgtPath -MemberType NoteProperty -Name siteColls -Value (New-Object System.Collections.ArrayList) switch ($oneMgtPath.Type) { 'ExplicitInclusion' { Add-Member -Input $newMgtPath -MemberType NoteProperty -Name type -Value 'E' } 'WildcardInclusion' { Add-Member -Input $newMgtPath -MemberType NoteProperty -Name type -Value 'W' } default { DBGIF ('Weird managed path type: {0} | {1}' -f $oneFarmManagedPath.Type, $oneFarmManagedPath.Name) { $true } } } [void] $webappMgtPaths.Add((Normalize-Uri $oneMgtPath.Name), $newMgtPath) } } # # Add-Member -Input $newWebApp -MemberType NoteProperty -Name paths -Value $webappMgtPaths Add-Member -Input $newWebApp -MemberType NoteProperty -Name farmPaths -Value $farmHostheaderMgtPaths # # DBG ('Getting managed path site collections for the web app: {0}' -f $oneWebApp) DBGSTART [object[]] $mgtPathSiteColls = $null $mgtPathSiteColls = Get-SPSite -WebApplication $oneWebApp -Limit All | ? { -not $_.HostHeaderIsSiteName } DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Found managed path site collections: {0} | {1}' -f $mgtPathSiteColls.Length, (($mgtPathSiteColls | Select -Expand Url) -join ',')) DBG ('Getting host header site collections for the web app: {0}' -f $oneWebApp) DBGSTART [object[]] $hostHeaderSiteColls = $null $hostHeaderSiteColls = Get-SPSite -WebApplication $oneWebApp -Limit All | ? { $_.HostHeaderIsSiteName } DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Found host header site collections: {0} | {1}' -f $hostHeaderSiteColls.Length, (($hostHeaderSiteColls | Select -Expand Url) -join ',')) # # if ($mgtPathSiteColls.Length -gt 0) { foreach ($oneMgtPathSiteColl in $mgtPathSiteColls) { [string] $srvRelativeUrl = Normalize-Uri $oneMgtPathSiteColl.ServerRelativeUrl DBG ('Searching for the web application explicit managed path for site collection: url = {0} | relative = {1} | normalized = {2}' -f $oneMgtPathSiteColl.Url, $oneMgtPathSiteColl.ServerRelativeUrl, $srvRelativeUrl) if ($newWebApp.paths.Keys -contains $srvRelativeUrl) { [PSCustomObject] $newSiteCol = New-Object PSCustomObject Add-Member -Input $newSiteCol -MemberType NoteProperty -Name name -Value ([string]::Empty) Add-Member -Input $newSiteCol -MemberType NoteProperty -Name url -Value (Normalize-Uri $oneMgtPathSiteColl.Url) Add-Member -Input $newSiteCol -MemberType NoteProperty -Name db -Value $oneMgtPathSiteColl.ContentDatabase.Name DBGIF $MyInvocation.MyCommand.Name { $newWebApp.paths[$srvRelativeUrl].siteColls.Count -ne 0 } [void] $newWebApp.paths[$srvRelativeUrl].siteColls.Add($newSiteCol) } else { # Note: we must use our own routing for getting the parens because Split-Path changes forward slashes (/) into backslashes (\) [string] $srvParentUrl = Get-UriParent $srvRelativeUrl [string] $srvLeafUrl = Get-UriLeaf $srvRelativeUrl DBG ('Searching for the web application wildcard managed path for site collection: url = {0} | srvRelative = {1} | parent = {2} | leaf = {3}' -f $oneMgtPathSiteColl.Url, $srvRelativeUrl, $srvParentUrl, $srvLeafUrl) if ($newWebApp.paths.Keys -contains $srvParentUrl) { [PSCustomObject] $newSiteCol = New-Object PSCustomObject Add-Member -Input $newSiteCol -MemberType NoteProperty -Name name -Value $srvLeafUrl Add-Member -Input $newSiteCol -MemberType NoteProperty -Name url -Value (Normalize-Uri $oneMgtPathSiteColl.Url) Add-Member -Input $newSiteCol -MemberType NoteProperty -Name db -Value $oneMgtPathSiteColl.ContentDatabase.Name [void] $newWebApp.paths[$srvParentUrl].siteColls.Add($newSiteCol) } else { DBGIF ('Weird site collection without a managed path: {0}' -f $oneMgtPathSiteColl.ServerRelativeUrl) { $true } } } } } # # [Collections.ArrayList] $allAAMsList = @() # # if ($hostHeaderSiteColls.Length -gt 0) { foreach ($oneHostHeaderSiteColl in $hostHeaderSiteColls) { DBG ('One hostheader site collection: {0}' -f $oneHostHeaderSiteColl.Url) [PSCustomObject] $newSiteCol = New-Object PSCustomObject # # DBG ('Obtain the alternate URLs for the host header site collection: {0}' -f $oneHostHeaderSiteColl.Url) DBGSTART [Collections.ArrayList] $hhscAAM = @() DBGSTART [object[]] $hhscZoneUrls = $null $hhscZoneUrls = Get-SPSiteURL -Identity $oneHostHeaderSiteColl DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ("Obtained the following AAM for the hostheader site collection: {0} | -->`r`n{1}" -f $hhscZoneUrls.Length, ($hhscZoneUrls | Out-String)) DBGIF $MyInvocation.MyCommand.Name { $hhscZoneUrls.Length -lt 1 } if ($hhscZoneUrls.Length -gt 0) { foreach ($oneHHSCZoneUrl in $hhscZoneUrls) { [PSCustomObject] $newAAM = New-Object PSCustomObject Add-Member -Input $newAAM -MemberType NoteProperty -Name zone -Value $oneHHSCZoneUrl.Zone Add-Member -Input $newAAM -MemberType NoteProperty -Name public -Value (Normalize-Uri $oneHHSCZoneUrl.Url) [void] $hhscAAM.Add($newAAM) [void] $allAAMsList.Add($newAAM.public) } } Add-Member -Input $newSiteCol -MemberType NoteProperty -Name aam -Value $hhscAAM # # [string] $srvRelativeUrl = Normalize-Uri $oneHostHeaderSiteColl.ServerRelativeUrl DBG ('Searching for the web application explicit hostheader managed path for site collection: url = {0} | relative = {1} | normalized = {2}' -f $oneHostHeaderSiteColl.Url, $oneHostHeaderSiteColl.ServerRelativeUrl, $srvRelativeUrl) if ($newWebApp.farmPaths.Keys -contains $srvRelativeUrl) { Add-Member -Input $newSiteCol -MemberType NoteProperty -Name name -Value ([string]::Empty) Add-Member -Input $newSiteCol -MemberType NoteProperty -Name url -Value (Normalize-Uri $oneHostHeaderSiteColl.Url) Add-Member -Input $newSiteCol -MemberType NoteProperty -Name db -Value $oneHostHeaderSiteColl.ContentDatabase.Name [void] $newWebApp.farmPaths[$srvRelativeUrl].siteColls.Add($newSiteCol) } else { # Note: we must use our own routing for getting the parens because Split-Path changes forward slashes (/) into backslashes (\) [string] $srvParentUrl = Get-UriParent $srvRelativeUrl [string] $srvLeafUrl = Get-UriLeaf $srvRelativeUrl DBG ('Searching for the web application wildcard hostheader managed path for site collection: url = {0} | srvRelative = {1} | parent = {2} | leaf = {3}' -f $oneHostHeaderSiteColl.Url, $srvRelativeUrl, $srvParentUrl, $srvLeafUrl) if ($newWebApp.farmPaths.Keys -contains $srvParentUrl) { [PSCustomObject] $newSiteCol = New-Object PSCustomObject Add-Member -Input $newSiteCol -MemberType NoteProperty -Name name -Value $srvLeafUrl Add-Member -Input $newSiteCol -MemberType NoteProperty -Name url -Value (Normalize-Uri $oneHostHeaderSiteColl.Url) Add-Member -Input $newSiteCol -MemberType NoteProperty -Name db -Value $oneHostHeaderSiteColl.ContentDatabase.Name [void] $newWebApp.farmPaths[$srvParentUrl].siteColls.Add($newSiteCol) } else { DBGIF ('Weird site collection without a managed path: {0}' -f $oneHostHeaderSiteColl.ServerRelativeUrl) { $true } } } } } # # DBG ('Get the alternate URLs for the web application: {0}' -f $oneWebApp) [Collections.ArrayList] $aam = @() DBGSTART [object[]] $zoneUrls = $null $zoneUrls = Get-SPAlternateURL -WebApplication $oneWebApp DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ("Obtained the following AAM for the web application: {0} | -->`r`n{1}" -f $zoneUrls.Length, ($zoneUrls | Out-String)) DBGIF $MyInvocation.MyCommand.Name { $zoneUrls.Length -lt 1 } if ($zoneUrls.Length -gt 0) { foreach ($oneZoneUrl in $zoneUrls) { [PSCustomObject] $newAAM = New-Object PSCustomObject Add-Member -Input $newAAM -MemberType NoteProperty -Name zone -Value $oneZoneUrl.Zone Add-Member -Input $newAAM -MemberType NoteProperty -Name public -Value (Normalize-Uri $oneZoneUrl.PublicUrl) Add-Member -Input $newAAM -MemberType NoteProperty -Name internal -Value (Normalize-Uri $oneZoneUrl.IncomingUrl) [void] $aam.Add($newAAM) [void] $allAAMsList.Add($newAAM.public) [void] $allAAMsList.Add($newAAM.internal) } } Add-Member -Input $newWebApp -MemberType NoteProperty -Name aam -Value $aam [string[]] $allAAMsUnique = $allAAMsList | Select -Unique DBG ('The web application is accessible on the following AAMs: {0} | {1}' -f $allAAMsUnique.Length, ($allAAMsUnique -join ',')) Add-Member -Input $newWebApp -MemberType NoteProperty -Name aamAll -Value $allAAMsUnique # # [void] $structure.Add($newWebApp) } # # DBGIF $MyInvocation.MyCommand.Name { $structure.Count -lt 1 } DBG ('User requested returning structure only: {0}' -f $structureOnly) if ((-not $structureOnly) -and ($structure.Count -gt 0)) { Write-Host ('') Write-Host ('') Write-Host ('#############################################################') [Collections.ArrayList] $processWebApps = @() [bool] $askerAutoDefault = $false foreach ($oneWebApp in $structure) { [bool] $yesWebApp = Parse-BoolSafe (Ask-UserForBool -query ('Do you want to export the web application: {0}' -f $oneWebApp.url) -default 'Yes' -autodefault ([ref] $askerAutoDefault)) if ($yesWebApp) { [void] $processWebApps.Add($oneWebApp) } } [string] $outputPath = [string]::Empty if ($processWebApps.Count -gt 0) { $outputPath = Ask-UserForValue -query 'Specify an export root path' -defaultVal $global:libCommonParentDir -mustBeSpecified $true -validationCode { Test-Path -Literal $args[0] } } DBG ('User requested processing the following web applications: {0} | {1}' -f $processWebApps.Count, (($processWebApps | Select -Expand Url) -join ',')) DBGIF $MyInvocation.MyCommand.Name { $processWebApps.Count -lt 1 } DBGIF $MyInvocation.MyCommand.Name { ([string]::IsNullOrEmpty($outputPath)) -or (-not (Test-Path -Literal $outputPath)) } if (($processWebApps.Count -gt 0) -and (-not ([string]::IsNullOrEmpty($outputPath))) -and (Test-Path -Literal $outputPath)) { [string] $outPathDated = Join-Path $outputPath ('SevecekSPExport-{0}' -f (Encode-NowToNTFS)) DBG ('Saving the export to the following path: {0}' -f $outPathDated) DBGIF $MyInvocation.MyCommand.Name { Test-Path -Literal $outPathDated } if (-not (Test-Path -Literal $outPathDated)) { DBG ('Create the output folder') DBGSTART New-Item -Path $outPathDated -ItemType Directory -Force | Out-Null DBGER $MyInvocation.MyCommand.Name $error DBGEND DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $outPathDated) } if (Test-Path -Literal $outPathDated) { DBG ('Exporting the web applications') foreach ($oneExpWebApp in $structure) { [string] $ntfsWebApp = Join-Path $outPathDated (Encode-SiteUrlToNTFS $oneExpWebApp.Url) [string] $ntfsWebAppPaths = Join-Path $ntfsWebApp Paths [string] $ntfsWebAppFarmPaths = Join-Path $ntfsWebApp FarmPaths DBG ('One web app: {0} | {1}' -f $oneExpWebApp.Url, $ntfsWebApp) DBGIF $MyInvocation.MyCommand.Name { Test-Path -Literal $ntfsWebApp } DBGSTART New-Item -Path $ntfsWebApp -ItemType Directory -Force | Out-Null New-Item -Path $ntfsWebAppPaths -ItemType Directory -Force | Out-Null New-Item -Path $ntfsWebAppFarmPaths -ItemType Directory -Force | Out-Null DBGER $MyInvocation.MyCommand.Name $error DBGEND DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $ntfsWebApp) } DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $ntfsWebAppPaths) } DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $ntfsWebAppFarmPaths) } DBG ('Paths to export: {0}' -f $oneExpWebApp.paths.Keys.Count) foreach ($oneExpPath in $oneExpWebApp.paths.Keys) { [string] $ntfsPath = Join-Path $ntfsWebAppPaths (Encode-SiteUrlToNTFS $oneExpPath) DBG ('Exporting one path: {0} | {1}' -f $oneExpPath, $ntfsPath) DBGSTART New-Item -Path $ntfsPath -ItemType Directory -Force | Out-Null DBGER $MyInvocation.MyCommand.Name $error DBGEND DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $ntfsPath) } [Collections.ArrayList] $expSiteColls = $oneExpWebApp.paths[$oneExpPath].siteColls DBG ('The path contains site collections: {0}' -f $expSiteColls.Count) if ($expSiteColls.Count -gt 0) { foreach ($oneExpSiteColl in $expSiteColls) { [string] $ntfsSiteColl = Join-Path $ntfsPath ([IO.Path]::ChangeExtension((Encode-SiteUrlToNTFS $oneExpSiteColl.name), 'bak')) DBG ('Exporting one site collection: {0} | {1}' -f $oneExpSiteColl.url, $ntfsSiteColl) DBGIF $MyInvocation.MyCommand.Name { Test-Path -Literal $ntfsSiteColl } DBGSTART Backup-SPSite -Identity $oneExpSiteColl.url -Path $ntfsSiteColl -Force DBGER $MyInvocation.MyCommand.Name $error DBGEND DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $ntfsSiteColl) } Add-Member -Input $oneExpSiteColl -MemberType NoteProperty -Name bakPath -Value (Get-SubPath -path $ntfsSiteColl -parent $outPathDated) } } } DBG ('Farm paths to export: {0}' -f $oneExpWebApp.farmPaths.Keys.Count) foreach ($oneExpFarmPath in $oneExpWebApp.farmPaths.Keys) { [string] $ntfsFarmPath = Join-Path $ntfsWebAppFarmPaths (Encode-SiteUrlToNTFS $oneExpFarmPath) DBG ('Exporting one path: {0} | {1}' -f $oneExpFarmPath, $ntfsFarmPath) DBGSTART New-Item -Path $ntfsFarmPath -ItemType Directory -Force | Out-Null DBGER $MyInvocation.MyCommand.Name $error DBGEND DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $ntfsFarmPath) } [Collections.ArrayList] $expHHSiteColls = $oneExpWebApp.farmPaths[$oneExpFarmPath].siteColls DBG ('The farm path contains hostheader site collections: {0}' -f $expHHSiteColls.Count) if ($expHHSiteColls.Count -gt 0) { foreach ($oneExpHHSiteColl in $expHHSiteColls) { [string] $ntfsHHSiteColl = Join-Path $ntfsFarmPath ([IO.Path]::ChangeExtension((Encode-SiteUrlToNTFS $oneExpHHSiteColl.url), 'bak')) DBG ('Exporting one hostheader site collection: {0} | {1}' -f $oneExpHHSiteColl.url, $ntfsHHSiteColl) DBGIF $MyInvocation.MyCommand.Name { Test-Path -Literal $ntfsHHSiteColl } DBGSTART Backup-SPSite -Identity $oneExpHHSiteColl.url -Path $ntfsHHSiteColl -Force DBGER $MyInvocation.MyCommand.Name $error DBGEND DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $ntfsHHSiteColl) } Add-Member -Input $oneExpHHSiteColl -MemberType NoteProperty -Name bakPath -Value (Get-SubPath -path $ntfsHHSiteColl -parent $outPathDated) } } } } [string] $outXml = Join-Path $outPathDated 'export.xml' DBG ('Save the exported info into an XML file: {0}' -f $outXml) DBGIF $MyInvocation.MyCommand.Name { Test-Path -Literal $outXml } DBGSTART $structure | Export-Clixml -Path $outXml -Force -Encoding UTF8 DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('') DBG ('==========================================') DBG ('We exported the following site collections') [Collections.ArrayList] $overallOutput = @() foreach ($oneWebApp in $structure) { foreach ($onePath in $oneWebApp.paths.Keys) { foreach ($oneSiteCol in $oneWebApp.paths[$onePath].siteColls) { [void] $overallOutput.Add($oneSiteCol) } } foreach ($onePath in $oneWebApp.farmPaths.Keys) { foreach ($oneSiteCol in $oneWebApp.farmPaths[$onePath].siteColls) { [void] $overallOutput.Add($oneSiteCol) } } } DBG ("`r`n{0}" -f ($overallOutput | Select url, db, bakPath | ft -auto | Out-String)) } } } } } DBGSTART ; DBGEND # just grab all remaining unhandled error messages DBGIF ('Some ERROR or WARNING occured during processing') { $global:assertOrErrorTriggered } return $structure } function global:Restore-SharePoint () { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) # # function Decode-SiteUrlFromNTFS ([string] $urlInNtfs) { # Note: site URL or managed path will never contain # or ~ if ($urlInNtfs -ne '#') { return $urlInNtfs.Replace('#', '/').Replace('~', ':') } else { return [string]::Empty } } function Has-ListIntersection ([string[]] $listA, [string[]] $listB) { for ($a = 0; $a -lt $listA.Length; $a ++) { for ($b = 0; $b -lt $listB.Length; $b ++) { if ($listA[$a] -eq $listB[$b]) { return $true } } } return $false } # # [Collections.ArrayList] $existingStructure = Export-SharePoint -structureOnly DBG ('Found existing web applications: {0}' -f $existingStructure.Count) DBGIF $MyInvocation.MyCommand.Name { $existingStructure.Count -lt 1 } if ((-not $global:assertOrErrorTriggered) -and ($existingStructure.Count -gt 0)) { Write-Host ('') Write-Host ('') Write-Host ('#############################################################') [string] $restorePath = Ask-UserForValue -query 'Specify a restore root path' -defaultVal $global:libCommonParentDir -mustBeSpecified $true -validationCode { Test-Path -Literal $args[0] } [string] $restoreManifest = Join-Path $restorePath 'export.xml' DBG ('User specified path contains the export.xml manifest: {0} | {1}' -f $restorePath, (Test-Path -Literal $restoreManifest)) if (-not (Test-Path -Literal $restoreManifest)) { $restoreManifest = [string]::Empty DBG ('The path specified is not an immediate export root, so search for the newest export manifest: {0}' -f $restorePath) DBGSTART [string[]] $restoreManifests = $null $restoreManifests = Get-ChildItem $restorePath -Include 'export.xml' -Recurse | Sort LastWriteTime -Descending | Select -Expand FullName DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Found possible restore manifests: {0} | {1}' -f $restoreManifests.Length, ($restoreManifests -join ',')) DBGIF $MyInvocation.MyCommand.Name { $restoreManifests.Length -lt 1 } if ($restoreManifests.Length -gt 0) { DBG ('Going to parse the manifests to see if any is usefull') foreach ($oneRestoreManifest in $restoreManifests) { DBG ('One newest restore manifest: {0}' -f $oneRestoreManifest) DBGSTART [Collections.ArrayList] $possibleStructure = $null $possibleStructure = Import-Clixml $oneRestoreManifest DBGER $MyInvocation.MyCommand.Name $error DBGEND DBGIF $MyInvocation.MyCommand.Name { $possibleStructure.Count -lt 1 } if ($possibleStructure.Count -gt 0) { DBG ('Valid manifest found: {0}' -f $oneRestoreManifest) $restoreManifest = $oneRestoreManifest break } } } } DBGIF $MyInvocation.MyCommand.Name { ([string]::IsNullOrEmpty($restoreManifest)) -or (-not (Test-Path -Literal $restoreManifest)) } if ((-not ([string]::IsNullOrEmpty($restoreManifest))) -and (Test-Path -Literal $restoreManifest)) { Write-Host ('') Write-Host ('') Write-Host ('#############################################################') [bool] $useTheManifest = Parse-BoolSafe (Ask-UserForBool -query ('Do you want to use the manifest: {0}' -f $restoreManifest) -default 'Yes') if ($useTheManifest) { [string] $restoreManifestParent = Split-Path -Parent $restoreManifest DBG ('Loading the restore manifest: {0} | {1}' -f $restoreManifest, $restoreManifestParent) DBGSTART [Collections.ArrayList] $backupStructure = $null $backupStructure = Import-Clixml $restoreManifest DBGER $MyInvocation.MyCommand.Name $error DBGEND DBGIF $MyInvocation.MyCommand.Name { $backupStructure.Count -lt 1 } if ($backupStructure.Count -gt 0) { DBG ('Parsing the web applications from the manifest') foreach ($oneWebApp in $backupStructure) { [string] $sourceWebApp = $oneWebApp.url DBG ('One web application urls: {0} | {1}' -f $sourceWebApp, ($oneWebApp.aamAll -join ',')) [string] $targetWebApp = [string]::Empty foreach ($oneExistingWebApp in $existingStructure) { DBG ('One existing web application urls: {0} | {1}' -f $oneExistingWebApp.url, ($oneExistingWebApp.aamAll -join ',')) if (Has-ListIntersection $oneWebApp.aamAll $oneExistingWebApp.aamAll) { DBG ('The web applications match by at least a single URL: {0}' -f $oneExistingWebApp.url) $targetWebApp = $oneExistingWebApp.url break } } Write-Host ('') Write-Host ('') Write-Host ('#############################################################') $targetWebApp = Ask-UserForValue -query ('Where to restore: {0} --> ' -f $sourceWebApp) -defaultVal $targetWebApp -mustBeSpecified $true -validationCode { -not ([object]::Equals((Get-SPWebApplication -Identity $args[0]), $null)) } DBG ('User wants to restore into the following web application: from = {0} | to = {1}' -f $sourceWebApp, $targetWebApp) DBGSTART [object] $spTargetWebApp = $null $spTargetWebApp = Get-SPWebApplication -Identity $targetWebApp DBGER $MyInvocation.MyCommand.Name $error DBGEND DBGIF $MyInvocation.MyCommand.Name { [object]::Equals($spTargetWebApp, $null) } if (-not ([object]::Equals($spTargetWebApp, $null))) { DBG ('Target web application exists: {0} | {1}' -f $spTargetWebApp.Url, $spTargetWebApp.Id) DBGSTART [object[]] $spTargetDatabases = $null $spTargetDatabases = $spTargetWebApp.ContentDatabases [string[]] $spTargetDatabaseNames = $null $spTargetDatabaseNames = $spTargetDatabases | Select -Expand Name DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('The target web application contains content databases: #{0} | {1}' -f $spTargetDatabaseNames.Length, ($spTargetDatabaseNames -join ', ')) DBGIF ('There are no existing content databases for target web application: {0}' -f $targetWebApp) { (Get-CountSafe $spTargetDatabaseNames) -lt 1 } if ((Get-CountSafe $spTargetDatabaseNames) -gt 0) { foreach ($oneSourcePath in $oneWebApp.paths.Keys) { DBG ('Ensure the web application managed path exists: {0} | {1}' -f $oneSourcePath, $oneWebApp.paths[$oneSourcePath].type) DBGSTART [object] $existingMgtPath = $null $existingMgtPath = Get-SPManagedPath -WebApplication $targetWebApp -Identity $oneSourcePath -EA SilentlyContinue DBGEND DBG ('The web application managed path exists: {0} | {1}' -f (-not ([object]::Equals($existingMgtPath, $null))), $existingMgtPath.Type) if ([object]::Equals($existingMgtPath, $null)) { DBG ('Must create the web application managed path first: {0} | {1} | {2}' -f $targetWebApp, $oneSourcePath, $oneWebApp.paths[$oneSourcePath].type) DBGSTART [object] $newMgtPath = $null $newMgtPath = New-SPManagedPath -WebApplication $targetWebApp -RelativeUrl $oneSourcePath -Explicit:($oneWebApp.paths[$oneSourcePath].type -eq 'E') DBGER $MyInvocation.MyCommand.Name $error DBGEND DBGIF $MyInvocation.MyCommand.Name { [object]::Equals($newMgtPath, $null) } } else { DBGIF $MyInvocation.MyCommand.Name { $existingMgtPath.Type -notlike ('{0}?*' -f $oneWebApp.paths[$oneSourcePath].type) } } } foreach ($oneSourceFarmPath in $oneWebApp.farmPaths.Keys) { DBG ('Ensure the hostheader managed path exists: {0} | {1}' -f $oneSourceFarmPath, $oneWebApp.farmPaths[$oneSourceFarmPath].type) DBGSTART [object] $existingHHMgtPath = $null $existingHHMgtPath = Get-SPManagedPath -Hostheader -Identity $oneSourceFarmPath -EA SilentlyContinue DBGEND DBG ('The hostheader managed path exists: {0} | {1}' -f (-not ([object]::Equals($existingHHMgtPath, $null))), $existingHHMgtPath.Type) if ([object]::Equals($existingHHMgtPath, $null)) { DBG ('Must create the hostheader managed path first: {0} | {1} | {2}' -f $targetWebApp, $oneSourceFarmPath, $oneWebApp.farmPaths[$oneSourceFarmPath].type) DBGSTART [object] $newMgtPath = $null $newMgtPath = New-SPManagedPath -Hostheader -RelativeUrl $oneSourceFarmPath -Explicit:($oneWebApp.farmPaths[$oneSourceFarmPath].type -eq 'E') DBGER $MyInvocation.MyCommand.Name $error DBGEND DBGIF $MyInvocation.MyCommand.Name { [object]::Equals($newMgtPath, $null) } } else { DBGIF $MyInvocation.MyCommand.Name { $existingHHMgtPath.Type -notlike ('{0}?*' -f $oneWebApp.farmPaths[$oneSourceFarmPath].type) } } } # # if (-not $global:assertOrErrorTriggered) { DBG ('Restore the web application site collections') foreach ($oneMgtPath in $oneWebApp.paths.Keys) { DBG ('Check if the web application managed path contains any site collections to restore: {0} | {1}' -f $oneMgtPath, $oneWebApp.paths[$oneMgtPath].siteColls.Count) if ($oneWebApp.paths[$oneMgtPath].siteColls.Count -gt 0) { foreach ($oneWASiteColl in $oneWebApp.paths[$oneMgtPath].siteColls) { [string] $bakFile = Join-Path $restoreManifestParent $oneWASiteColl.bakPath [string] $targetSiteColl = '{0}/{1}' -f $targetWebApp, $oneMgtPath if (-not ([string]::IsNullOrEmpty($oneWASiteColl.name))) { $targetSiteColl = '{0}/{1}' -f $targetSiteColl, $oneWASiteColl.name } DBG ('Going to restore the site collection: source = {0} | {1} | {2} | {3}' -f $oneWASiteColl.name, $oneWASiteColl.url, $oneWASiteColl.bakPath, $bakFile) DBG ('Going to restore the site collection: target = {0}' -f $targetSiteColl) DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $bakFile) } if (-not (Contains-Safe $spTargetDatabaseNames $oneWASiteColl.db)) { DBG ('The site collection to be imported was exported from a content database which does not exist in current environment. We will import the into the most free content database: sourceUrl = {0} | targetUrl = {1} | sourceDb = {2}' -f $oneWASiteColl.url, $targetSiteColl, $oneWASiteColl.db) DBGSTART Restore-SPSite -Identity $targetSiteColl -Path $bakFile -Force -Confirm:$false DBGER $MyInvocation.MyCommand.Name $error DBGEND } else { [Microsoft.SharePoint.Administration.SPContentDatabase] $targetContentDb = $null $targetContentDb = $spTargetDatabases | ? { $_.Name -eq $oneWASiteColl.db } DBGIF $MyInvocation.MyCommand.Name { Is-Null $targetContentDb } DBGSTART Restore-SPSite -Identity $targetSiteColl -Path $bakFile -ContentDatabase $targetContentDb -Force -Confirm:$false DBGER $MyInvocation.MyCommand.Name $error DBGEND } if ($global:assertOrErrorTriggered) { break } } } if ($global:assertOrErrorTriggered) { break } } } # # if (-not $global:assertOrErrorTriggered) { DBG ('Restore the hostheader site collections') # Note: we must have the keys sorted first, because if there is a hostheader managed path site collection # it must be imported only after there is the root hostheader managed path site collection. So the # root host header site collection, if any, must go first to be imported. foreach ($oneFarmPath in ($oneWebApp.farmPaths.Keys | Sort)) { DBG ('Check if the hostheader managed path contains any site collections to restore: {0} | {1}' -f $oneFarmPath, $oneWebApp.farmPaths[$oneFarmPath].siteColls.Count) if ($oneWebApp.farmPaths[$oneFarmPath].siteColls.Count -gt 0) { foreach ($oneHHSiteColl in $oneWebApp.farmPaths[$oneFarmPath].siteColls) { [string] $bakFile = Join-Path $restoreManifestParent $oneHHSiteColl.bakPath [string] $targetSiteColl = $oneHHSiteColl.url DBG ('Going to restore the site collection: source = {0} | {1} | {2} | {3}' -f $oneHHSiteColl.name, $oneHHSiteColl.url, $oneHHSiteColl.bakPath, $bakFile) DBG ('Going to restore the site collection: target = {0} | {1}' -f $targetSiteColl, $targetWebApp) DBGIF $MyInvocation.MyCommand.Name { -not (Test-Path -Literal $bakFile) } if (-not (Contains-Safe $spTargetDatabaseNames $oneHHSiteColl.db)) { DBG ('The site collection to be imported was exported from a content database which does not exist in current environment. We will import the into the most free content database: sourceUrl = {0} | targetUrl = {1} | sourceDb = {2}' -f $oneHHSiteColl.url, $targetSiteColl, $oneHHSiteColl.db) DBGSTART Restore-SPSite -Identity $targetSiteColl -Path $bakFile -HostheaderWebApplication $targetWebApp -Force -Confirm:$false DBGER $MyInvocation.MyCommand.Name $error DBGEND } else { [Microsoft.SharePoint.Administration.SPContentDatabase] $targetContentDb = $null $targetContentDb = $spTargetDatabases | ? { $_.Name -eq $oneHHSiteColl.db } DBGIF $MyInvocation.MyCommand.Name { Is-Null $targetContentDb } DBGSTART Restore-SPSite -Identity $targetSiteColl -Path $bakFile -HostheaderWebApplication $targetWebApp -ContentDatabase $targetContentDb -Force -Confirm:$false DBGER $MyInvocation.MyCommand.Name $error DBGEND } if ($global:assertOrErrorTriggered) { break } } } if ($global:assertOrErrorTriggered) { break } } } } } if ($global:assertOrErrorTriggered) { break } } } } } } DBGSTART ; DBGEND # just grab all remaining unhandled error messages DBGIF ('Some ERROR or WARNING occured during processing') { $global:assertOrErrorTriggered } } function global:Open-XmlFromXls ([string] $xlsFile = "C:\ONDRA\TRAINING\GOC-BPZ\bpz-test-base-v4.xlsx", [string] $columnWithLastValue = 4, [string] $rootElement = 'root', [switch] $saveXml) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) [XML] $xml = $null [System.Collections.ArrayList] $xlsData = Load-Xls $xlsFile -keyColumn $columnWithLastValue if ((Get-CountSafe $xlsData) -gt 0) { $xml = New-Object System.Xml.XmlDataDocument [void] $xml.InsertBefore($xml.CreateXmlDeclaration('1.0', 'UTF-8', $null), $xml.DocumentElement) [void] $xml.AppendChild($xml.CreateElement('', $rootElement, '')) [System.Xml.XmlElement] $currentElement = $xml.DocumentElement [string[]] $xlsElementColumns = Get-Member -Input $xlsData[0] -MemberType NoteProperty | Select -Expand Name | ? { $_ -notlike 'a:?*' } [string[]] $xlsAttributeColumns = Get-Member -Input $xlsData[0] -MemberType NoteProperty | Select -Expand Name | ? { $_ -like 'a:?*' } foreach ($oneXlsItem in $xlsData) { foreach ($oneXlsColumn in $xlsElementColumns) { if (Is-ValidString $oneXlsItem.$oneXlsColumn) { [System.Xml.XmlElement] $newElement = $xml.CreateElement($oneXlsColumn) [void] $newElement.SetAttribute('value', $oneXlsItem.$oneXlsColumn) foreach ($oneXlsAttributeColumn in $xlsAttributeColumns) { if (Is-ValidString $oneXlsItem.$oneXlsAttributeColumn) { [void] $newElement.SetAttribute($oneXlsAttributeColumn.Substring(2), $oneXlsItem.$oneXlsAttributeColumn) } } [Xml.XmlElement] $elementToCheck = $currentElement while ($true) { if ($newElement.Name -eq $elementToCheck.Name) { $currentElement = $elementToCheck.ParentNode break } if ($elementToCheck.ParentNode -isnot [System.Xml.XmlDataDocument]) { $elementToCheck = $elementToCheck.ParentNode } else { break } } [void] $currentElement.AppendChild($newElement) $currentElement = $newElement break } } } } if ((Is-NonNull $xml) -and ($saveXml)) { $xml.Save(([System.IO.Path]::ChangeExtension($xlsFile, 'xml'))) } return $xml } function global:Generate-RandomSerieUseMemory ([int] $items, [int] $count) { [Collections.ArrayList] $sourceIdx = @(0..($items - 1)) [Collections.ArrayList] $selectedIdx = @() for ($i = 0; $i -lt $count; $i ++) { $rndIdx = Get-Random -Minimum 0 -Maximum $sourceIdx.Count [void] $selectedIdx.Add($sourceIdx[$rndIdx]) [void] $sourceIdx.RemoveAt($rndIdx) } return $selectedIdx } function global:Generate-QuestionSet ([string] $questionsXmlFile = "C:\ONDRA\TRAINING\GOC-BPZ\bpz-test-base-v4.xml", [int] $number = 30, [string] $categoryNodes = 'kategorie', [string] $questionNodes = 'otazka', [string] $answerNodes = 'odpoved') { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) [hashtable] $questions = @{} DBG ('Loading the XML: {0}' -f $questionsXmlFile) DBGSTART [XML] $xml = [XML] (cat $questionsXmlFile) DBGER $MyInvocation.MyCommand.Name $error DBGEND if (Is-NonNull $xml) { DBG ('Obtain the test details') DBGSTART [System.Xml.XmlNodeList] $allCategories = $xml.SelectNodes(('//{0}' -f $categoryNodes)) [System.Xml.XmlNodeList] $allQuestions = $xml.SelectNodes(('//{0}' -f $questionNodes)) DBGER $MyInvocation.MyCommand.Name $error DBGEND DBG ('Loaded: categories = {0} | questions = {1}' -f $allCategories.Count, $allQuestions.Count) [int] $selected = 0 if (($allCategories.Count -gt 0) -and ($allQuestions.Count -gt 0)) { for ($i = 0; $i -lt $allCategories.Count; $i++) { [System.Xml.XmlNodeList] $oneCatQuestions = $allCategories[$i].SelectNodes(('./{0}' -f $questionNodes)) [int] $oneCatSelectCount = [Math]::Round(((([double] $oneCatQuestions.Count) / ([double] $allQuestions.Count)) * ([double] $number)), 0) if (($selected + $oneCatSelectCount) -gt $number) { $oneCatSelectCount = $number - $selected } if ($oneCatSelectCount -lt 1) { $oneCatSelectCount = 1 } $selected += $oneCatSelectCount DBG ('One category: questions = {0,3} of {1,3} | {2}' -f $oneCatQuestions.Count, $oneCatSelectCount, $allCategories[$i].value) [int[]] $rndIndexes = Generate-RandomSerieUseMemory -items $oneCatQuestions.Count -count $oneCatSelectCount [Collections.ArrayList] $testItems = @() foreach ($oneRndIndex in $rndIndexes) { [void] $testItems.Add($oneCatQuestions[$oneRndIndex]) } $oneQuestions = New-Object PSObject Add-Member -Input $oneQuestions -MemberType NoteProperty -Name count -Value $oneCatQuestions.Count Add-Member -Input $oneQuestions -MemberType NoteProperty -Name selected -Value $oneCatSelectCount Add-Member -Input $oneQuestions -MemberType NoteProperty -Name allItems -Value $oneCatQuestions Add-Member -Input $oneQuestions -MemberType NoteProperty -Name testItems -Value $testItems [void] $questions.Add($allCategories[$i].value, $oneQuestions) } } } return $questions } function global:Generate-QuestionTextFile ([hashtable] $questions = (Generate-QuestionSet), [string] $outFile, [switch] $compressSpaces) { DBG ('{0}: Parameters: {1}' -f $MyInvocation.MyCommand.Name, (($PSBoundParameters.Keys | ? { $_ -ne 'object' } | % { '{0}={1}' -f $_, $PSBoundParameters[$_]}) -join $multivalueSeparator)) [Text.StringBuilder] $outText = New-Object Text.StringBuilder foreach ($oneQuestionCat in $questions.Keys) { DBG ('One category: {0}' -f $oneQuestionCat) foreach ($oneQuestion in $questions[$oneQuestionCat].testItems) { [int] $answerCount = Get-CountSafe $oneQuestion.odpoved DBG (' One question: {0} | {1}' -f $oneQuestion.value, $answerCount) [void] $outText.AppendLine($oneQuestion.value) [int[]] $answerMix = Generate-RandomSerieUseMemory -items $answerCount -count $answerCount for ($i = 0; $i -lt $answerMix.Length; $i ++) { DBG (' One answer: {0}' -f $oneQuestion.odpoved[$answerMix[$i]].value) if (-not $compressSpaces) { [void] $outText.AppendLine() } [void] $outText.AppendLine(("`t[ ] {0}" -f $oneQuestion.odpoved[$answerMix[$i]].value)) } [void] $outText.AppendLine() [void] $outText.AppendLine() if (-not $compressSpaces) { [void] $outText.AppendLine() } } } DBG ('Saving the output file: {0}' -f $outFile) Set-Content -Path $outFile -Value $outText.ToString() -Encoding UTF8 -Force } # SIG # Begin signature block # MIIc/QYJKoZIhvcNAQcCoIIc7jCCHOoCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBbOB9ObESyBnhE # vd40g3wdpUAImxdY8D6qJ7zWJOPA2aCCGAQwggTlMIIDzaADAgECAhA5vUKe0oFu # utW8yQO0umXnMA0GCSqGSIb3DQEBCwUAMHUxCzAJBgNVBAYTAklMMRYwFAYDVQQK # Ew1TdGFydENvbSBMdGQuMSkwJwYDVQQLEyBTdGFydENvbSBDZXJ0aWZpY2F0aW9u # IEF1dGhvcml0eTEjMCEGA1UEAxMaU3RhcnRDb20gQ2xhc3MgMiBPYmplY3QgQ0Ew # HhcNMTYxMjAxMTU1MTEzWhcNMTgxMjAxMTU1MTEzWjBRMQswCQYDVQQGEwJDWjEa # MBgGA1UECAwRSmlob21vcmF2c2t5IEtyYWoxDTALBgNVBAcMBEJybm8xFzAVBgNV # BAMMDk9uZHJlaiBTZXZlY2VrMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC # AQEAr9E9hNj06bash9JX97kpsqK9Z/ciOBC6trI4nvlW9CPwhKBTb5wArhxLYZBG # 9jWPWrdy1nL/cm5qMqBb/mogYwMwvEYWMvsIOOVn6HD9lVhNAovD6PHz0ziBBKIs # zXTjyUPQaoIlIELovz967m78HJdUZJGxqhluAsS9o9/fEzA7XXUhUuqRKsetuZV/ # Asfh5sOveeoRsbeW4daTWvtz3TJuULL0w43LNVYJkd6LL8cegvLPVZUe1N7skvid # EvntdlowQsJlqFdrH3SGKIPKA6ObcY8SZWkEQSbVBF8Kum1UT+jN0gm+84FwOg5W # qKx+VvTK2ljVWnPrCD0Zzu2oIQIDAQABo4IBkzCCAY8wDgYDVR0PAQH/BAQDAgeA # MBMGA1UdJQQMMAoGCCsGAQUFBwMDMAkGA1UdEwQCMAAwHQYDVR0OBBYEFG2vSo3N # hQWILeUs0oN9XzHTejcfMB8GA1UdIwQYMBaAFD5ik5rXxxnuPo9JEIVVFSDjlIQc # MG0GCCsGAQUFBwEBBGEwXzAkBggrBgEFBQcwAYYYaHR0cDovL29jc3Auc3RhcnRz # c2wuY29tMDcGCCsGAQUFBzAChitodHRwOi8vYWlhLnN0YXJ0c3NsLmNvbS9jZXJ0 # cy9zY2EuY29kZTIuY3J0MDYGA1UdHwQvMC0wK6ApoCeGJWh0dHA6Ly9jcmwuc3Rh # cnRzc2wuY29tL3NjYS1jb2RlMi5jcmwwIwYDVR0SBBwwGoYYaHR0cDovL3d3dy5z # dGFydHNzbC5jb20vMFEGA1UdIARKMEgwCAYGZ4EMAQQBMDwGCysGAQQBgbU3AQIF # MC0wKwYIKwYBBQUHAgEWH2h0dHBzOi8vd3d3LnN0YXJ0c3NsLmNvbS9wb2xpY3kw # DQYJKoZIhvcNAQELBQADggEBAJuRiEvHtIYSpsmMkPhTz4QOOShN3p5KWdf8vm71 # A33CR9fds10d8D2B2aE+vjmHJ69GY0bbfg5oZY2Lsq2euL7Da5/hS8+6T3MEtD4h # njfHV7mxmoSfFuy/KDipoV6uwhI+ksqchXYdUH+5uCQO0MOO8ITjAgzUQsnZ4UIB # HBGeP+e+3ljxSYSXWdPIrgxdR971P/HhWSVfKNlmBgEKMQM5Jy0aAd4jxSl/AzdY # t0+6pliFJ1peGhdFni2Fm8fu5oN68aTIrNtc5WY7Lzgf+sRTVeWORWS37+1zAD0m # jzd8gyfBLxRuaRSfjYxny0rLXelAwfiA3ze2DU2Bfg9/rfcwggXYMIIDwKADAgEC # AhBsO9J+3TyUnpWOKKmzx1egMA0GCSqGSIb3DQEBCwUAMH0xCzAJBgNVBAYTAklM # MRYwFAYDVQQKEw1TdGFydENvbSBMdGQuMSswKQYDVQQLEyJTZWN1cmUgRGlnaXRh # bCBDZXJ0aWZpY2F0ZSBTaWduaW5nMSkwJwYDVQQDEyBTdGFydENvbSBDZXJ0aWZp # Y2F0aW9uIEF1dGhvcml0eTAeFw0xNTEyMTYwMTAwMDVaFw0zMDEyMTYwMTAwMDVa # MHUxCzAJBgNVBAYTAklMMRYwFAYDVQQKEw1TdGFydENvbSBMdGQuMSkwJwYDVQQL # EyBTdGFydENvbSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEjMCEGA1UEAxMaU3Rh # cnRDb20gQ2xhc3MgMiBPYmplY3QgQ0EwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAw # ggEKAoIBAQC5FARY97LFhiwIMmCtCCbAgXe5aBnZFSsdGGnk2hqWBZcuZHkaqT1R # M1rQd2r0ApNBw466cBur2Ht0b5jo17mpPmh2pImgIqwX1in4u7hhn9IH0GYOMEcg # K3ACHv5zCRxxNLXifqmsqKfxjjpABnaSyvd4bO9YBXN9f4NQ6aJVAuMArpanxsJk # e+P4WECVLk17v92CAN5JVaczI+baT/lgo5NVcTEkloCViSbIfU6ILeyhOSQZvpom # MYk8eJqI0nimOTJJfmXangNDsrX8np+3lXD0+6rCZisXRWIaeffyTMHZ31Qj1D50 # WYdRtX5yev4WgaXoKJQN3lkgXUcytvyHAgMBAAGjggFaMIIBVjAOBgNVHQ8BAf8E # BAMCAQYwEwYDVR0lBAwwCgYIKwYBBQUHAwMwEgYDVR0TAQH/BAgwBgEB/wIBADAy # BgNVHR8EKzApMCegJaAjhiFodHRwOi8vY3JsLnN0YXJ0c3NsLmNvbS9zZnNjYS5j # cmwwZgYIKwYBBQUHAQEEWjBYMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5zdGFy # dHNzbC5jb20wMAYIKwYBBQUHMAKGJGh0dHA6Ly9haWEuc3RhcnRzc2wuY29tL2Nl # cnRzL2NhLmNydDAdBgNVHQ4EFgQUPmKTmtfHGe4+j0kQhVUVIOOUhBwwHwYDVR0j # BBgwFoAUTgvvGqRAW6UXaYcwyjRoQ9BBrvIwPwYDVR0gBDgwNjA0BgRVHSAAMCww # KgYIKwYBBQUHAgEWHmh0dHA6Ly93d3cuc3RhcnRzc2wuY29tL3BvbGljeTANBgkq # hkiG9w0BAQsFAAOCAgEAY6U81bNtJyjY67pTrzAL6kpdEtX5mspw+kxjjNdNVH5G # 6lLnhaEkIxqdpvY/Wdw+UdNtExs+N8efKPSwh2m/BxXj2fSeLMwXcwHFookScEER # 8ez0quCNzioqNHac7LCXPEnQzbtG2FHlePKNDWh8eU6KxiAzNzIrIxPthinHGgLT # BOACHQM2YTlD8YoU5oN3dLmBOqtH0BDMZoLcjEIoEW1zC+TnVb3yU1G0xub6gnN7 # lP50vbAiHJYrnywQiXaloBV8B9YYfe6ZgvjqxwufwFcMVyE3UmCuDTsOpjqDEKpJ # 25s+FUdkie5VqCS1aaudLo31X+9UvP45pfgyRqzyfUnVEhH4ZXxlBWZMzj2Xov5+ # m/+H3kxYuFA5xdqdshj/Zx00S7PkCSF+8M1NCcvFgQwjIw61bZAjDBl3P3a8xNTX # sb2CjFdiNKbT3LD6IGeIf0b/EbPf0FXdvBrxm0ofMOhnngdPolPYCtoOGtZPAVe/ # xeu+/ZyKv6TSHlshaUO0iYfsmbXnZ51vvt/kkjwms9/qPFxSuE0fjEfF7aQazwRE # Df2hiVPR0pAhvShtM3oU4XreEFEUWEYHs25fYV4WMmxkUKSgmSmwRq45tvtGH4LT # b5+cd+iLqK8rBQL0E6xaUjjGfsYx7bueIvqTvCkrQvoxMbn/qDHCiypowDVq6TAw # ggZqMIIFUqADAgECAhADAZoCOv9YsWvW1ermF/BmMA0GCSqGSIb3DQEBBQUAMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IEFzc3VyZWQgSUQgQ0Et # MTAeFw0xNDEwMjIwMDAwMDBaFw0yNDEwMjIwMDAwMDBaMEcxCzAJBgNVBAYTAlVT # MREwDwYDVQQKEwhEaWdpQ2VydDElMCMGA1UEAxMcRGlnaUNlcnQgVGltZXN0YW1w # IFJlc3BvbmRlcjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAKNkXfx8 # s+CCNeDg9sYq5kl1O8xu4FOpnx9kWeZ8a39rjJ1V+JLjntVaY1sCSVDZg85vZu7d # y4XpX6X51Id0iEQ7Gcnl9ZGfxhQ5rCTqqEsskYnMXij0ZLZQt/USs3OWCmejvmGf # rvP9Enh1DqZbFP1FI46GRFV9GIYFjFWHeUhG98oOjafeTl/iqLYtWQJhiGFyGGi5 # uHzu5uc0LzF3gTAfuzYBje8n4/ea8EwxZI3j6/oZh6h+z+yMDDZbesF6uHjHyQYu # RhDIjegEYNu8c3T6Ttj+qkDxss5wRoPp2kChWTrZFQlXmVYwk/PJYczQCMxr7GJC # kawCwO+k8IkRj3cCAwEAAaOCAzUwggMxMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMB # Af8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMIIBvwYDVR0gBIIBtjCCAbIw # ggGhBglghkgBhv1sBwEwggGSMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdp # Y2VydC5jb20vQ1BTMIIBZAYIKwYBBQUHAgIwggFWHoIBUgBBAG4AeQAgAHUAcwBl # ACAAbwBmACAAdABoAGkAcwAgAEMAZQByAHQAaQBmAGkAYwBhAHQAZQAgAGMAbwBu # AHMAdABpAHQAdQB0AGUAcwAgAGEAYwBjAGUAcAB0AGEAbgBjAGUAIABvAGYAIAB0 # AGgAZQAgAEQAaQBnAGkAQwBlAHIAdAAgAEMAUAAvAEMAUABTACAAYQBuAGQAIAB0 # AGgAZQAgAFIAZQBsAHkAaQBuAGcAIABQAGEAcgB0AHkAIABBAGcAcgBlAGUAbQBl # AG4AdAAgAHcAaABpAGMAaAAgAGwAaQBtAGkAdAAgAGwAaQBhAGIAaQBsAGkAdAB5 # ACAAYQBuAGQAIABhAHIAZQAgAGkAbgBjAG8AcgBwAG8AcgBhAHQAZQBkACAAaABl # AHIAZQBpAG4AIABiAHkAIAByAGUAZgBlAHIAZQBuAGMAZQAuMAsGCWCGSAGG/WwD # FTAfBgNVHSMEGDAWgBQVABIrE5iymQftHt+ivlcNK2cCzTAdBgNVHQ4EFgQUYVpN # JLZJMp1KKnkag0v0HonByn0wfQYDVR0fBHYwdDA4oDagNIYyaHR0cDovL2NybDMu # ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEQ0EtMS5jcmwwOKA2oDSGMmh0 # dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRENBLTEuY3Js # MHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNl # cnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20v # RGlnaUNlcnRBc3N1cmVkSURDQS0xLmNydDANBgkqhkiG9w0BAQUFAAOCAQEAnSV+ # GzNNsiaBXJuGziMgD4CH5Yj//7HUaiwx7ToXGXEXzakbvFoWOQCd42yE5FpA+94G # AYw3+puxnSR+/iCkV61bt5qwYCbqaVchXTQvH3Gwg5QZBWs1kBCge5fH9j/n4hFB # pr1i2fAnPTgdKG86Ugnw7HBi02JLsOBzppLA044x2C/jbRcTBu7kA7YUq/OPQ6dx # nSHdFMoVXZJB2vkPgdGZdA0mxA5/G7X1oPHGdwYoFenYk+VVFvC7Cqsc21xIJ2bI # o4sKHOWV2q7ELlmgYd3a822iYemKC23sEhi991VUQAOSK2vCUcIKSK+w1G7g9BQK # Ohvjjz3Kr2qNe9zYRDCCBs0wggW1oAMCAQICEAb9+QOWA63qAArrPye7uhswDQYJ # KoZIhvcNAQEFBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IElu # YzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQg # QXNzdXJlZCBJRCBSb290IENBMB4XDTA2MTExMDAwMDAwMFoXDTIxMTExMDAwMDAw # MFowYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UE # CxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgQXNzdXJlZCBJ # RCBDQS0xMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA6IItmfnKwkKV # pYBzQHDSnlZUXKnE0kEGj8kz/E1FkVyBn+0snPgWWd+etSQVwpi5tHdJ3InECtqv # y15r7a2wcTHrzzpADEZNk+yLejYIA6sMNP4YSYL+x8cxSIB8HqIPkg5QycaH6zY/ # 2DDD/6b3+6LNb3Mj/qxWBZDwMiEWicZwiPkFl32jx0PdAug7Pe2xQaPtP77blUjE # 7h6z8rwMK5nQxl0SQoHhg26Ccz8mSxSQrllmCsSNvtLOBq6thG9IhJtPQLnxTPKv # mPv2zkBdXPao8S+v7Iki8msYZbHBc63X8djPHgp0XEK4aH631XcKJ1Z8D2KkPzIU # YJX9BwSiCQIDAQABo4IDejCCA3YwDgYDVR0PAQH/BAQDAgGGMDsGA1UdJQQ0MDIG # CCsGAQUFBwMBBggrBgEFBQcDAgYIKwYBBQUHAwMGCCsGAQUFBwMEBggrBgEFBQcD # CDCCAdIGA1UdIASCAckwggHFMIIBtAYKYIZIAYb9bAABBDCCAaQwOgYIKwYBBQUH # AgEWLmh0dHA6Ly93d3cuZGlnaWNlcnQuY29tL3NzbC1jcHMtcmVwb3NpdG9yeS5o # dG0wggFkBggrBgEFBQcCAjCCAVYeggFSAEEAbgB5ACAAdQBzAGUAIABvAGYAIAB0 # AGgAaQBzACAAQwBlAHIAdABpAGYAaQBjAGEAdABlACAAYwBvAG4AcwB0AGkAdAB1 # AHQAZQBzACAAYQBjAGMAZQBwAHQAYQBuAGMAZQAgAG8AZgAgAHQAaABlACAARABp # AGcAaQBDAGUAcgB0ACAAQwBQAC8AQwBQAFMAIABhAG4AZAAgAHQAaABlACAAUgBl # AGwAeQBpAG4AZwAgAFAAYQByAHQAeQAgAEEAZwByAGUAZQBtAGUAbgB0ACAAdwBo # AGkAYwBoACAAbABpAG0AaQB0ACAAbABpAGEAYgBpAGwAaQB0AHkAIABhAG4AZAAg # AGEAcgBlACAAaQBuAGMAbwByAHAAbwByAGEAdABlAGQAIABoAGUAcgBlAGkAbgAg # AGIAeQAgAHIAZQBmAGUAcgBlAG4AYwBlAC4wCwYJYIZIAYb9bAMVMBIGA1UdEwEB # /wQIMAYBAf8CAQAweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8v # b2NzcC5kaWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6 # MHgwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3Vy # ZWRJRFJvb3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9E # aWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwHQYDVR0OBBYEFBUAEisTmLKZB+0e # 36K+Vw0rZwLNMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqG # SIb3DQEBBQUAA4IBAQBGUD7Jtygkpzgdtlspr1LPUukxR6tWXHvVDQtBs+/sdR90 # OPKyXGGinJXDUOSCuSPRujqGcq04eKx1XRcXNHJHhZRW0eu7NoR3zCSl8wQZVann # 4+erYs37iy2QwsDStZS9Xk+xBdIOPRqpFFumhjFiqKgz5Js5p8T1zh14dpQlc+Qq # q8+cdkvtX8JLFuRLcEwAiR78xXm8TBJX/l/hHrwCXaj++wc4Tw3GXZG5D2dFzdaD # 7eeSDY2xaYxP+1ngIw/Sqq4AfO6cQg7PkdcntxbuD8O9fAqg7iwIVYUiuOsYGk38 # KiGtSTGDR5V3cdyxG0tLHBCcdxTBnU8vWpUIKRAmMYIETzCCBEsCAQEwgYkwdTEL # MAkGA1UEBhMCSUwxFjAUBgNVBAoTDVN0YXJ0Q29tIEx0ZC4xKTAnBgNVBAsTIFN0 # YXJ0Q29tIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MSMwIQYDVQQDExpTdGFydENv # bSBDbGFzcyAyIE9iamVjdCBDQQIQOb1CntKBbrrVvMkDtLpl5zANBglghkgBZQME # AgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEM # BgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqG # SIb3DQEJBDEiBCCRty3o5BSIwrWKs8IWXDRqc/bC7eLkZWUamL4TS2AdyzANBgkq # hkiG9w0BAQEFAASCAQBMTz+kdM26/KjGfK0GEPtX82HZdMAB450n6f/h7QJjxW7g # AttaDR4aQQxwP101kspH6xERaqyeaJpo8zYCwM72j6LmeNXxGk4gLumaKlMU3bMh # FJ8Rrm1edp/+2PF+j23KqB4mHC1kXF8uhEeR0c+LJY2aDjcOt5XxFsvUmOb+2DSI # 7YqjeyEnWD/GE7WDS+/ug6d3RYd0fFZuCdBoR8UTTJKOpKyAz9Xwm823Fh6JLtCe # So9GCOMslT8VDutdMSge6wRP2/U4mAYez4D/OUXj+AaVCCzrZuKt5J6Rf9U9Tbbg # xLD83xwL+Xjz4eASGJsUFlCJgdYZ4FawGdVq1BGxoYICDzCCAgsGCSqGSIb3DQEJ # BjGCAfwwggH4AgEBMHYwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0 # IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNl # cnQgQXNzdXJlZCBJRCBDQS0xAhADAZoCOv9YsWvW1ermF/BmMAkGBSsOAwIaBQCg # XTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xODEw # MTcyMTI3MDRaMCMGCSqGSIb3DQEJBDEWBBSUo4NoR4B0sFuEPl/wnRs/OjxUHzAN # BgkqhkiG9w0BAQEFAASCAQBrlW0SP/TOnawkGHfMkQl4pCcZqdWTUuxXRfoB8Wda # +yob0voMF+MfvFwFyYf7sFkcI4+U2fwXrVZmqXBcjmnoHUcHhavKxzQ4KXC5rINo # t1TNItlgyvUkKy1M+aJa+1e4DeaA6RWDjGzCa9PSLNc5IefE5QP3cspuM1tM0QlU # G3jt8yKrZZZK8EZv7TgAw886ct2WyEZ6+yLBAq5XvvWeKoMZWCszKdS3rme9hCZi # g+xvhoCvZ57RBR1yIEwC/CWJNlC6kF9r5peW5gth+hY0fcT1HbtDOK552uQKGRWz # UAIPknYktBQ9cEGv7U86VzYlNik/lcekmNHw+agF00IG # SIG # End signature block