(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