SRxCore.psm1

#Region '.\Enum\SRxLogLevel.ps1' 0
enum SRxLogLevel {
    SILENT 
    ERROR
    WARNING
    INFO
    VERBOSE
    DEBUG 
}
#EndRegion '.\Enum\SRxLogLevel.ps1' 9
#Region '.\Classes\CustomizationWriter.ps1' 0

class CustomizationWriter {

    hidden $_connection             = $null
    hidden $_onStatusCallback       = $null

    hidden [string] $_environment   = $null
    hidden [string] $_siteDesign    = $null
    hidden [string] $_termID        = $null
    hidden [int]$_scope             = -1

    #--------------------
    # Constructor
    #--------------------
    CustomizationWriter () {
        Write-Host " > constructor CustomizationWriter"
        Connect-SRxProvisioningDatabase_JSON
        $this._connection = Get-SRxConnection -siteUrl $global:SRxEnv.Tenancy.AdminUrl
    }

    hidden [pscustomobject] getProvisioningTerm( $hostingTerm, [string]$name ) {
        $customizationTerm = $null
        try {
            Get-PnPProperty -ClientObject $hostingTerm -Property Name, Id, Terms -Connection $this._connection | Out-Null

            $hostingTerm.Terms | ForEach-Object { 
                $nodeName = $($_.Name).trim()
                $n = $name.trim()
                $e = $nodeName.equals($n)
                if( $e ) {
                    $customizationTerm = $_
                }
            }
            if($customizationTerm) { Write-Host " > Found $name term."  }
        }
        catch {
            Write-Host ($_.Exception.Message) 
            Write-Host ($_.Exception) 
            return $null  
        }
        return $customizationTerm
    }
    hidden [boolean] setTermCustomProperties( $customizationTerm, [hashtable]$customProperties ) {
        try {
            $customProperties.Keys | ForEach-Object { 
                #Write-SRx WARNING " | - $_ = $($customProperties[$_])"
                $key = $_
                $value = $($customProperties[$_])
                Write-Host("value length = $($value.Length)")
                $chunks = @($value -split '(.{9000})' | Where-Object{$_})
                Write-Host("chunks count = $($chunks.Count )")
                if($chunks.Count -eq 1) { 
                    $customizationTerm.SetCustomProperty($key, $value) 
                }
                else { # key_0, key_1 ...
                    for( [int]$i = 0; $i -lt $chunks.Count; $i++) { 
                        $chunk_key = "$($key)_$($i)"
                        $chunk_val = $($chunks[$i])
                        #Write-Host ("$i = $($chunks[$i])")
                        $customizationTerm.SetCustomProperty($chunk_key, $chunk_val)
                    }
                }
            }
        }
        catch {
            Write-Host ($_.Exception.Message) 
            Write-Host ($_.Exception) 
            return $false  
        }
        return $true
    }
    hidden [pscustomobject] setProvisioningTerm( $hostingTerm, [string]$name, [hashtable]$customProperties, $isCustomizationTarget, $remove, [boolean]$disable) {
        try {
            $customizationTerm = $null
            Get-PnPProperty -ClientObject $hostingTerm -Property Id, Terms -Connection $this._connection | Out-Null

            $hostingTerm.Terms | ForEach-Object { 
                if( $($_.Name) -eq $name) {
                    $customizationTerm = $_
                }
            }

            if(-not $customizationTerm -and -not $remove ) {
                #Write-SRx VERBOSE " > Creating new term..."
                $customizationTerm = Add-PnPTermToTerm -ParentTerm $hostingTerm.Id -Name $name -CustomProperties $customProperties -Connection $this._connection
                #Write-SRx VERBOSE " > Added new term $name id=$($customizationTerm.Id)"
                Start-Sleep -Seconds 5 #added wait to avoid save conflict
                try {
                    if($disable) {
                        Write-Host " > Deprecating $name term...."
                        $customizationTerm.Deprecate($disable)
                        Invoke-PnPQuery -RetryCount 5  -Connection $this._connection
                    }
                    #Write-SRx VERBOSE " > Term $name id=$($customizationTerm.Id) created."
                } 
                catch { Write-Host " ? Failed to deprecate $name term." }  
                Write-Host " > Created $name term."    
            }
            else {
                if( $isCustomizationTarget ) {
                    if($remove) {
                        $id = $customizationTerm.Id
                        #Write-SRx VERBOSE " > Deleting term id = $id"
                        Remove-PnPTerm -Identity $id  -Connection $this._connection
                        Write-Host " > Deleted $name term"                     
                    }
                    else {
                        <#
                        $customProperties.Keys | ForEach-Object {
                            #Write-SRx WARNING " | - $_ = $($customProperties[$_])"
                            $customizationTerm.SetCustomProperty($_, $($customProperties[$_]))
                        }
                        #>

                        $this.setTermCustomProperties( $customizationTerm, $customProperties)
                        if($disable) { Write-Host " > Disabling $name term ..."  }
                        else         { Write-Host " > Enabling $name term ..."  }                         
                            $customizationTerm.Deprecate($disable)
                        #}
                        Invoke-PnPQuery -RetryCount 5  -Connection $this._connection
                        #Write-SRx VERBOSE " > Term $name id=$($customizationTerm.Id) updated."
                        Write-Host " > Updated $name term"                        
                    }                    
                } 
                else {
                    Write-Host " > Term $name found." 
                }         
            }
            return $customizationTerm
        }
        catch {
            Write-Host ($_.Exception.Message) 
            Write-Host ($_.Exception) 
            return $null  
        }
    } 
    hidden [pscustomobject] addProvisioningTerm($hostingTerm,[string]$name,[hashtable]$customProperties, $isCustomizationTarget, $remove, [boolean]$disable) {
        try {
            $customizationTerm = $null
            if( -not $isCustomizationTarget) {
                $customizationTerm = $this.getProvisioningTerm( $hostingTerm, $name)
            }
            if( $customizationTerm ) { return $customizationTerm  }
            else {
                if( $isCustomizationTarget ) {
                    if($remove) { $customizationTerm = $this.setProvisioningTerm( $hostingTerm, $name, $customProperties, $true, $true, $disable) }
                    else        { $customizationTerm = $this.setProvisioningTerm( $hostingTerm, $name, $customProperties, $true, $false, $disable) }
                    
                } else {
                    $customizationTerm = $this.setProvisioningTerm( $hostingTerm, $name, $customProperties, $false, $false, $disable)
                }
                return $customizationTerm
            }
        }
        catch {
            Write-Host ($_.Exception.Message) 
            Write-Host ($_.Exception) 
            return $null  
        }
    } 
    hidden [pscustomobject] newPnPXMLTermNode($hostingTerm, $termNode, $isTopNode, $isCustomizationTarget) {
            
        $disable = $false
        $customProperties = @{}
        $nodeName = $termNode.Name
        foreach ($attr in $termNode.Attributes.GetEnumerator() ) { 
            if( $attr.Name -eq "ts_KeyAttributeValue" ) { 
                $nodeName = $attr.Value
                $customProperties.add($attr.Name, $attr.Value) #add ts_KeyAttributeValue
            }
            elseif( $attr.Name -eq "ts_Deprecated" )      { $disable = [boolean]$($attr.Value) } #bypass ts_Deprecated
            elseif( $attr.Name -eq "ts_Scope" ) {} #bypass ts_Scope
            else { $customProperties.add($attr.Name, $attr.Value) }
        }
        #if($isCustomizationTarget) { $customProperties.add("ts_CustomizationTarget", $true) }
        #replace not allowed for use at term title symbols
        $nodeName = $nodeName.Replace('|','-')
        $nodeName = $nodeName.Replace(';','-')
        $nodeName = $nodeName.Replace('"','-')
        $nodeName = $nodeName.Replace('<','-')
        $nodeName = $nodeName.Replace('>','-')
        $nodeName = $nodeName.Replace('&','-')

        if( $isCustomizationTarget ) {
            if($termNode.Operation -eq "remove") { return $this.addProvisioningTerm( $hostingTerm, $nodeName, $customProperties, $true, $true,  $disable) }
            else                                 { return $this.addProvisioningTerm( $hostingTerm, $nodeName, $customProperties, $true, $false, $disable) }
        } 
        return $this.addProvisioningTerm( $hostingTerm, $nodeName, $customProperties, $false, $false, $disable)
    }
    hidden [pscustomobject] getSiteTerm() {
        $siteTerm = $null   
        if( $this._scope -eq 3)     { $siteTerm = Get-SRxHostingTerm -Site -siteID $this._termID -Connection $this._connection } #Site
        elseif( $this._scope -eq 2) { $siteTerm = Get-SRxHostingTerm -Design -siteDesign $this._siteDesign -siteEnvironment $this._environment -Connection $this._connection } #Design
        elseif( $this._scope -eq 1) { $siteTerm = Get-SRxHostingTerm -Environment -siteEnvironment $this._environment -Connection $this._connection } #Environment
        elseif( $this._scope -eq 0) { $siteTerm = Get-SRxHostingTerm -Profile -Connection $this._connection } #Profile
        return $siteTerm     
    }
    hidden [pscustomobject] setProvisioningBlock([int]$optype) {
        try {
            $termDesign = $null

            if( $optype -eq 0 )  { $siteTerm = Get-SRxHostingTerm -Sites -siteDesign $this._siteDesign -siteEnvironment $this._environment -Connection $this._connection } #sites
            else                 { $siteTerm = $this.getSiteTerm()  } #environment, design, site
            if($null -eq $siteTerm ) {
                Write-Host $(" > Hosting term for target customization scope not found")
                return $null
            }         
            # Provisioning template
            Write-Host $(" > Target customization scope: $($siteTerm.Name)") #-ForegroundColor Cyan
#return $null
            $tID = $siteTerm.CustomProperties.ts_ProvisioningTemplate
            #Write-SRx VERBOSE $(" > ts_ProvisioningTemplate = $tID")
            $provisioningTemplateTerm = $null
            if($tID -and ($this._scope -ne 2)) { #for environment & site, exdesign scope from Master only
                $provisioningTemplateTerm = Get-PnPTerm -Identity $([GUID]$tID) -ErrorAction SilentlyContinue -IncludeChildTerms -Connection $this._connection 
            }
            if( $null -eq $provisioningTemplateTerm) {
                #$decisionCreate = $Host.UI.PromptForChoice("", "Create provisionig block ?:", @('&Yes'; '&Cancel'), 1)

                #if( $decisionCreate -eq 1) {
                # return "cancelled"
                #}
                if( $this._scope -eq 0 ) { # Profile --> termSet
                    Write-Host $(" > Creating profile provisioning block ...")
                    $provisioningTemplateTerm = New-PnPTerm -TermSet $siteTerm.Name -TermGroup $global:SRxEnv.Tenancy.TermGroupName -Name "Customizations" -Connection $this._connection # -CustomProperties $customProperties
                }
                elseif($this._scope -eq 2) { #Design --> term

                    $termDesign = Get-SRxHostingTerm -Design -siteDesign $this._siteDesign -siteEnvironment "Master" -Connection $this._connection
                    if( -not $termDesign ) { 
                        $termDesign = Set-SRxTermDesign -siteDesign $this._siteDesign -siteEnvironment "Master" -Connection $this._connection 
                    }

                    $dID = $termDesign.CustomProperties.ts_ProvisioningTemplate
                    if($dID ) { $provisioningTemplateTerm = Get-PnPTerm -Identity $([GUID]$dID) -ErrorAction SilentlyContinue -IncludeChildTerms -Connection $this._connection  }
                    if( $null -eq $provisioningTemplateTerm) {
                        Write-Host $(" > Creating design provisioning block ...")
                        $provisioningTemplateTerm = Add-PnPTermToTerm -ParentTerm $termDesign.Id -Name "Customizations" -Connection $this._connection # -CustomProperties $customProperties
                    }
                }
                else { #Term --> term
                    Write-Host $(" > Creating provisioning block ...")
                    $provisioningTemplateTerm = Add-PnPTermToTerm -ParentTerm $siteTerm.Id -Name "Customizations" -Connection $this._connection # -CustomProperties $customProperties
                }

                if($null -ne $provisioningTemplateTerm) {
                    $s = [string]$($provisioningTemplateTerm.Id)
                    if($this._scope -eq 2)  { $termDesign.SetCustomProperty("ts_ProvisioningTemplate", $s) }
                    else                    { $siteTerm.SetCustomProperty("ts_ProvisioningTemplate", $s) }
                    Invoke-PnPQuery -RetryCount 5  -Connection $this._connection
                    #Write-SRx VERBOSE $(" > ts_ProvisioningTemplate = $tID")
                }
                else {
                    Write-Host $(" > Failed to create customization block")
                }
            }
            else {
                #Write-SRx INFO $(" > Target customization scope: $($siteTerm.Name)") -ForegroundColor Cyan
            }
            return $provisioningTemplateTerm
        }
        catch {
            Write-Host ($_.Exception.Message) 
            Write-Host ($_.Exception) 
            return $null  
        }
    }  
    [void] Start ([pscustomobject]$nodesPack) {
        #Write-Host "2: ------------ received nodePack -------------------"
        #Write-Host ( $nodesPack | FL | Out-String )
    
        [System.Collections.Stack]$nodesStack = $nodesPack.nodesStack
    
        $this._environment = $nodesPack.ts_Environment
        $this._siteDesign  = $nodesPack.ts_Design
        $this._termID      = $nodesPack.ts_TermID
    #Write-Host ("2: environment = $environment siteDesign = $siteDesign termID = $termID")
    #Write-Host "2: ------------ received nodeStack -------------------"
    #Write-Host ( $nodesStack | FT | Out-String )
    #Write-Host "2: ------------ termNode -------------------"
        $termNode = $nodesStack.Peek()
    #Write-Host ( $termNode | FT | Out-String )
    
        $this._scope        = $($termNode.Scope) #[int]$($termNode.Attributes["ts_Scope"])
        [string]$operation  = $($termNode.Operation)    
        [int]$optype        = $($termNode.optype) 
# Write-Host ("3: environment = $($this._environment) siteDesign = $($this._siteDesign) termID = $($this._termID)")
    
        $count = $nodesStack.Count
        $max = $count + 1
        $counter = 0
        #$Activity = "Creating customization filter on TermStore"
        $status = "Setting customization block ..."
        #Write-Progress -Activity $Activity -Status $status -PercentComplete $(($counter++ / $max) * 100)
        $counter++
        if($this._onStatusCallback) { $this._onStatusCallback.Invoke( $status, $counter-1, $max )}
        
        $hostingTerm = $this.setProvisioningBlock($optype)
        if( $null -eq $hostingTerm) {
            Write-Host " > Failure - no host ?"
            #return 'cancelled'
            return
        }    
#return
        
        $index = 1
        $parentTerm = $hostingTerm
       
        do {
            $termNode = $nodesStack.Peek()
            $status = "Adding $($termNode.Name) node ..."
            #Write-Progress -Activity $Activity -Status $status -PercentComplete $(($counter++ / $max) * 100)
            $counter++
            if($this._onStatusCallback) { $this._onStatusCallback.Invoke( $status, $counter-1, $max )}
            #Start-Sleep -Seconds 2
    
    
            if($index -eq $count) {
                if($index -eq 1) { $term = $this.newPnPXMLTermNode($parentTerm, $termNode, $true,  $true) }
                else             { $term = $this.newPnPXMLTermNode($parentTerm, $termNode, $false, $true) }
            }
            else                  { 
                if($index -eq 1) { $term =  $this.newPnPXMLTermNode($parentTerm, $termNode, $true,  $false) }
                else             { $term =  $this.newPnPXMLTermNode($parentTerm, $termNode, $false,  $false) }
            }
    
            if($null -eq $term) { 
                Write-Host " > Failure - ?"
                return #'cancelled'
            }
            else { 
                $parentTerm = $term 
            }
    
            $nodesStack.Pop()
            $index++        
        } while( $($nodesStack.Count) -gt 0 )
    
        Write-Host " > Completed termstore modifications"
    }  
    #------------------
    # Callbacks
    #------------------
    [void] OnStatusCallback( [System.Management.Automation.ScriptBlock]$onStatusCallback ) {
        $this._onStatusCallback = $onStatusCallback
    }

}
#EndRegion '.\Classes\CustomizationWriter.ps1' 348
#Region '.\Private\_Cache.ps1' 0
function _Cache { 
    $s = $global:SRxEnv.MasterCachePath
    $s = $s -replace "MasterCache",""
    if (!(Test-Path $s))
    {
        Write-SRx VERBOSE "Create the new folder $($s)"
        New-Item -itemType Directory -Path $s | Out-Null
    }
    Start-Process explorer $s 
}
$global:_Cache = { 
    $s = $global:SRxEnv.MasterCachePath
    $s = $s -replace "MasterCache",""
    if (!(Test-Path $s))
    {
        Write-SRx VERBOSE "Create the new folder $($s)"
        New-Item -itemType Directory -Path $s | Out-Null
    }
    Start-Process explorer $s 
}
#EndRegion '.\Private\_Cache.ps1' 21
#Region '.\Private\_Docs.ps1' 0
function _Docs {
    Start-Process ((Resolve-Path ".\..\Provisioning User Guide.pdf").Path)
}
$global:_Docs = {
    Start-Process ((Resolve-Path ".\..\Provisioning User Guide.pdf").Path)
}
#EndRegion '.\Private\_Docs.ps1' 7
#Region '.\Private\_EcoSystem.ps1' 0
function _EcoSystem { 
    $str = $global:SRxEnv.DefaultSettings.EcoSystem.ts_URL
    $str = $str.replace('{tenancy}', $global:SRxEnv.Tenancy.Name)
    $ecosystemURL = $str.replace('{ecosystem}', $global:SRxEnv.Tenancy.EcoSystem)
    Start-Process $ecosystemURL | Out-Null
}
$global:_EcoSystem = { 
    $str = $global:SRxEnv.DefaultSettings.EcoSystem.ts_URL
    $str = $str.replace('{tenancy}', $global:SRxEnv.Tenancy.Name)
    $ecosystemURL = $str.replace('{ecosystem}', $global:SRxEnv.Tenancy.EcoSystem)
    Start-Process $ecosystemURL | Out-Null
}
#EndRegion '.\Private\_EcoSystem.ps1' 13
#Region '.\Private\_Pipeline.ps1' 0
<#
function _Pipeline {
    $s = $global:SRxEnv.MasterCachePath
    $s = $s -replace "MasterCache",""
    $s = $s + "Scheduler\Pipeline.json"
    Start-Process notepad $s
}
#>

function _Pipeline { 
    $global:SRxEnv.Pipeline
}
#EndRegion '.\Private\_Pipeline.ps1' 12
#Region '.\Private\_Profile.ps1' 0
function _Profile { 
    Start-Process PowerShell -ArgumentList '-NoProfile -noexit -ExecutionPolicy Bypass -Command "".\shell.ps1 -Setup -Verbose""'
    #Start-Process PowerShell -ArgumentList '-NoProfile -noexit -ExecutionPolicy Bypass -Command "".\shell.ps1 -Setup""'
}
$global:_Profile = { 
    Start-Process PowerShell -ArgumentList '-NoProfile -noexit -ExecutionPolicy Bypass -Command "".\shell.ps1 -Setup -Verbose""'
    #Start-Process PowerShell -ArgumentList '-NoProfile -noexit -ExecutionPolicy Bypass -Command "".\shell.ps1 -Setup""'
}
#EndRegion '.\Private\_Profile.ps1' 9
#Region '.\Private\_Shell_Verbose.ps1' 0
function _Shell_Verbose { 
    Start-Process PowerShell -ArgumentList '-NoProfile -noexit -ExecutionPolicy Bypass -Command "".\shell.ps1 -Verbose""'
}
$global:_Shell_Verbose = {
    Start-Process PowerShell -ArgumentList '-NoProfile -noexit -ExecutionPolicy Bypass -Command "".\shell.ps1 -Verbose""'
}
#EndRegion '.\Private\_Shell_Verbose.ps1' 7
#Region '.\Private\_Shell.ps1' 0
function _Shell { 
    #Start-Process cmd.exe -ArgumentList "/c .\..\shell.bat"
    Start-Process PowerShell -ArgumentList '-NoProfile -noexit -ExecutionPolicy Bypass -File "".\shell.ps1""'
}
$global:_Shell = {
    Start-Process PowerShell -ArgumentList '-NoProfile -noexit -ExecutionPolicy Bypass -File "".\shell.ps1""'
}
#EndRegion '.\Private\_Shell.ps1' 8
#Region '.\Private\_Task.ps1' 0
function _Task { 
    #Start-Process cmd.exe -ArgumentList "/c .\..\task-verbose.bat"
    #Start-Process cmd.exe -ArgumentList "/c .\..\task.bat"
    Start-Process PowerShell -ArgumentList '-NoProfile -noexit -ExecutionPolicy Bypass -Command "".\shell.ps1 -Verbose -ControlFile Update-Pipeline""'
}
#EndRegion '.\Private\_Task.ps1' 6
#Region '.\Private\Add-Types.ps1' 0
Add-Type -assembly System
Add-Type -assembly System.Runtime.InteropServices
    $TypeDefinition = @'
    using System;
    using System.Runtime.InteropServices;
    namespace Win32Functions {
        public class Win32Windows {
            [DllImport("user32.dll")]
            public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
 
            [DllImport("user32.dll")]
            public static extern bool IsIconic(IntPtr hWnd);
        }
    }
'@
                   
Add-Type -TypeDefinition $TypeDefinition #-PassThru
#EndRegion '.\Private\Add-Types.ps1' 17
#Region '.\Private\ColorByLevel.ps1' 0
function ColorByLevel
{ 
<#
.SYNOPSIS
    synopsis
     
.DESCRIPTION
    description
         
.NOTES
    =========================================
    Project : Search Health Reports (SRx)
    -----------------------------------------
    File Name : Module-Name.psm1
 
    Requires :
        PowerShell Version 5.1, Search Health Reports (SRx), Microsoft.SharePoint.PowerShell,
        Patterns and Practices v15 PowerShell
 
    ========================================================================================
    This Sample Code is provided for the purpose of illustration only and is not intended to
    be used in a production environment.
     
        THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY
        OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
        WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
 
    We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to
    reproduce and distribute the object code form of the Sample Code, provided that You agree:
        (i) to not use Our name, logo, or trademarks to market Your software product in
            which the Sample Code is embedded;
        (ii) to include a valid copyright notice on Your software product in which the
             Sample Code is embedded;
        and
        (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against
              any claims or lawsuits, including attorneys' fees, that arise or result from
              the use or distribution of the Sample Code.
 
    ========================================================================================
     
.INPUTS
    input1
 
.EXAMPLE
    Module-Name
 
#>

[CmdletBinding()]
param ( 
        [parameter(Mandatory=$true,ValueFromPipeline=$true)]
        $line,
        [switch]$IncludeDetails
)
    BEGIN 
    {
        [System.ConsoleColor]$defaultColor = (Get-Host).UI.RawUI.ForegroundColor
        if($defaultColor -lt 0) {$defaultColor = [System.ConsoleColor]::White}
        
        $incomingResultRow = 0 
        $headlineOffset = 0
     }
    PROCESS
    {
        if([string]::IsNullOrEmpty($line)){return}
        $words = $line.Split()

        if ($incomingResultRow -eq 0) 
        {
            $headlineOffset = $line.indexOf("Headline")
            #Write-Host $("-" * $line.length) -foregroundColor Cyan
            #Write-Host $line -foregroundColor Cyan
            #Write-Host $("-" * $line.length) -foregroundColor Cyan
        }
        
        $count = 0
        foreach($word in $words[1..$words.Length])
        {
            if([string]::IsNullOrWhiteSpace($word))
            {
                $count++
                continue
            }

            $level = $word
            break
        }

        switch($level)
        {
            "INFO" {
                Write-Host $words[0..$count] -NoNewline
                Write-Host " Info " -NoNewline
                Write-Host $words[($count+2)..$words.Length] -ForegroundColor $defaultColor 
            }
            "Normal" {
                Write-Host $words[0..$count] -NoNewline
                Write-Host " $level " -ForegroundColor Green -NoNewline
                Write-Host $words[($count+2)..$words.Length] -ForegroundColor $defaultColor 
            }
            "Warning" {
                Write-Host $words[0..$count] -NoNewline
                Write-Host " $level " -ForegroundColor Yellow -NoNewline
                Write-Host $words[($count+2)..$words.Length] -ForegroundColor $defaultColor
            }
            "Error" {
                Write-Host $words[0..$count] -NoNewline
                Write-Host " $level " -ForegroundColor Red -NoNewline
                Write-Host $words[($count+2)..$words.Length] -ForegroundColor $defaultColor
            }
            "Exception" {
                Write-Host $words[0..$count] -NoNewline
                Write-Host " $level " -ForegroundColor Magenta -NoNewline
                Write-Host $words[($count+2)..$words.Length] -ForegroundColor $defaultColor
            }
            "Skipped" {
                Write-Host $words[0..$count] -NoNewline
                Write-Host " $level " -ForegroundColor DarkGray -NoNewline
                Write-Host $words[($count+2)..$words.Length] -ForegroundColor $defaultColor 
            }
            default {Write-Host $line}
        }

        if ($IncludeDetails -and ($incomingResultRow -gt 1) -and ($dataList[$incomingResultRow-2].Details -ne $null)) {
            if ($dataList[$incomingResultRow-2].Details -is [String]) {
                $details = $dataList[$incomingResultRow-2].Details.split("`n").trimEnd()
            } else {
                $detailStrings = $dataList[$incomingResultRow-2].Details | foreach { $_ | Out-String -stream }
                $details = $detailStrings | foreach { $_.trimEnd() }
            }

            $lineInDetails = 0
            foreach ($d in $details) {
                if ($d.length -gt $($Host.UI.RawUI.WindowSize.Width - $headlineOffset - 1)) {
                    $d = $d.Substring(0, $($Host.UI.RawUI.WindowSize.Width - $headlineOffset -3)) + "..."
                }
                if (($lineInDetails -eq 0) -or ($details[$lineInDetails-1] -ne $details[$lineInDetails] -ne "")) {
                    Write-Host $(" " * $headlineOffset) -NoNewline
                    Write-Host $d
                }
                $lineInDetails++
            }
        }
        $incomingResultRow++
    }
    END
    {}
}
#EndRegion '.\Private\ColorByLevel.ps1' 148
#Region '.\Private\Format-SRxFileShortcut.ps1' 0
function Format-SRxFileShortcut {
    param (
        $programs, 
        [string]$startMenu,
        [switch]$remove
    )
    begin {
        try {

            $programs.GetEnumerator() | ForEach-Object { 
                $shortcutPath = if(  $null -ne $($_.Value[1])) { "$startMenu\$($_.Value[1])\$($_.Key).lnk" } else { "$startMenu\$($_.Key).lnk" }
                $folderPath   = if(  $null -ne $($_.Value[1])) { "$startMenu\$($_.Value[1])" }               else { "$startMenu" }
                if (-not (Test-Path $shortcutPath) -and -not $remove ) {
                    if (-not (Test-Path $folderPath)) {
                        write-host ("Specified folder {0} doesn't exist for the {1} shortcut, creating now..." -f $_.Value[1], $_.Key)
                        New-Item -ItemType Directory -Path $folderPath -Force | Out-Null
                        write-host ("Creating shortcut for {0} with path {1} in folder {2}..." -f $_.Key, $_.Value[0], $_.Value[1])
                    }
                    else {
                        write-host ("Shortcut for {0} not found with path {1} in existing folder {2}, creating it now..." -f $_.Key, $_.Value[0], $_.Value[1])
                    }

                    $shortcut = $shortcutPath #"$startMenu\$($_.Value[1])\$($_.Key).lnk"
                    $target = $_.Value[0]
                    $windowStyle = $_.Value[2]
                    $arguments = $_.Value[3]

                    $description = $_.Key
                    $workingdirectory = $SRxEnv.Paths.SRxRoot

                    $WshShell = New-Object -ComObject WScript.Shell
                    $Shortcut = $WshShell.CreateShortcut($shortcut)
                    $Shortcut.TargetPath        = $target
                    $Shortcut.Description       = $description
                    $shortcut.WorkingDirectory  = $workingdirectory
                    $Shortcut.WindowStyle       = $windowStyle
                    $Shortcut.Arguments         = $arguments
                    $Shortcut.Save()
        
                }
                elseif($remove) {
                    Remove-Item $shortcutPath #$shortcutPath"$startMenu\$($_.Value[1])\$($_.Key).lnk"
                }
            }           

        }
        catch {

        }       
    }
}
#EndRegion '.\Private\Format-SRxFileShortcut.ps1' 52
#Region '.\Private\Format-SRxJson.ps1' 0
function Format-SRxJson {
    <#
    .SYNOPSIS
        Prettifies JSON output.
    .DESCRIPTION
        Reformats a JSON string so the output looks better than what ConvertTo-Json outputs.
    .PARAMETER Json
        Required: [string] The JSON text to prettify.
    .PARAMETER Minify
        Optional: Returns the json string compressed.
    .PARAMETER Indentation
        Optional: The number of spaces (1..1024) to use for indentation. Defaults to 4.
    .PARAMETER AsArray
        Optional: If set, the output will be in the form of a string array, otherwise a single string is output.
    .EXAMPLE
        $json | ConvertTo-Json | Format-SRxJson -Indentation 2
    #>

    [CmdletBinding(DefaultParameterSetName = 'Prettify')]
    Param(
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [string]$Json,

        [Parameter(ParameterSetName = 'Minify')]
        [switch]$Minify,

        [Parameter(ParameterSetName = 'Prettify')]
        [ValidateRange(1, 1024)]
        [int]$Indentation = 4,

        [Parameter(ParameterSetName = 'Prettify')]
        [switch]$AsArray
    )

    if ($PSCmdlet.ParameterSetName -eq 'Minify') {
        return ($Json | ConvertFrom-Json) | ConvertTo-Json -Depth 100 -Compress
    }

    # If the input JSON text has been created with ConvertTo-Json -Compress
    # then we first need to reconvert it without compression
    if ($Json -notmatch '\r?\n') {
        $Json = ($Json | ConvertFrom-Json) | ConvertTo-Json -Depth 100
    }

    $indent = 0
    $regexUnlessQuoted = '(?=([^"]*"[^"]*")*[^"]*$)'

    $result = $Json -split '\r?\n' |
        ForEach-Object {
            # If the line contains a ] or } character,
            # we need to decrement the indentation level unless it is inside quotes.
            if ($_ -match "[}\]]$regexUnlessQuoted") {
                $indent = [Math]::Max($indent - $Indentation, 0)
            }

            # Replace all colon-space combinations by ": " unless it is inside quotes.
            $line = (' ' * $indent) + ($_.TrimStart() -replace ":\s+$regexUnlessQuoted", ': ')

            # If the line contains a [ or { character,
            # we need to increment the indentation level unless it is inside quotes.
            if ($_ -match "[\{\[]$regexUnlessQuoted") {
                $indent += $Indentation
            }

            $line
        }

    if ($AsArray) { return $result }
    return $result -Join [Environment]::NewLine
}
#EndRegion '.\Private\Format-SRxJson.ps1' 70
#Region '.\Private\ListControls.ps1' 0
function ListControls 
{
    Write-Host
    Write-Host "Control files that evaluate the test $Test" -ForegroundColor Cyan
    $__ruleControlFiles | % {$filename=$_.FullName;Get-Content $filename} | ? {$_ -match $Test.Substring(0,$Test.Length-4)} | % {$filename}
}
#EndRegion '.\Private\ListControls.ps1' 7
#Region '.\Private\ListTests.ps1' 0
function ListTests 
{
    Write-Host
    Write-Host "Test evaluated by control file $ControlFile" -ForegroundColor Cyan
    foreach($rule in $__rulesControl)
    {
        Write-Host " $($rule.Rule)"
    }
}
#EndRegion '.\Private\ListTests.ps1' 10
#Region '.\Private\LoadModule.ps1' 0
$LoadModule = {
    param( $name, $version, $debug2)
    #param( $name )

    if( -not $global:SRxEnv.Exists ) {
        $coreConfigPath = $("$PSScriptRoot\Config\core.config.json")
        $coreConfigRaw = Get-Content $coreConfigPath -Raw -Encoding UTF8 -ErrorAction Stop
        $coreConfigJSON = ConvertFrom-Json $coreConfigRaw

        $version = $coreConfigJSON.Version
        $debug2  = $coreConfigJSON.Debug
    }

    $version     = if ( $null -eq $version ) { $global:SRxEnv.Version }     else { $version }
    $debug         = if ( $null -eq $debug2 )  { $global:SRxEnv.Debug }     else { $debug2 }
    $module     = $null

    if ($debug) {
        #write-host "Module $name debug $version imported."
        if ($debug2) { $module = $("$PSScriptRoot\Modules\$name\$version\$name.psm1") }
        else { $module = $(Join-Path $global:SRxEnv.paths.SRxRoot "Modules\$name\$($global:SRxEnv.Version)\$name.psm1") }
        if ( -not $(Test-Path -path $module -PathType Leaf )) { $module = $null }
    }

    # If module is imported say that and do nothing
    if (Get-Module -Name $name -Verbose:$false ) {
        #write-host "Module $name $version is already imported."
    } 
    else {
        if ($debug -and $module) {
            write-host "Module $name local source code version $version imported."
            Import-Module $module -DisableNameChecking -Verbose:$false -Force 
        }
        else {
            # If module is not imported, but available on disk then import
            if( $null -ne $(Get-Module -ListAvailable  -Name $name -Verbose:$false | Where-Object { $_.Name -eq $name -and $_.Version -eq [System.Version]$version })) {
                if ( (Import-Module -Name $name -RequiredVersion $version -PassThru -Verbose:$false -WarningAction silentlyContinue).Version -eq [System.Version]$version) {
                    #write-host "Module $name $version imported."
                }
                else {
                    write-host "Module $name $version not imported, exiting."
                    EXIT 1
                }
            }
            else {
                write-host "Module $name $version is not available on disk, trying to import and install..."
                # If module is not imported, not available on disk, but is in online gallery then install and import
                if (Find-Module -Name $name -RequiredVersion $version ) { #-MinimumVersion $version
                    write-host "Module $name $version found."
                    Install-Module -Name $name -RequiredVersion $version -Force -Verbose:$false -Scope CurrentUser
                    write-host "Module $name $version installed."
                    Import-Module $name -RequiredVersion $version -DisableNameChecking -Verbose:$false
                    write-host "Module $name $version imported."
                }
                else {
                    # If the module is not imported, not available and not in the online gallery then abort
                    write-host "Module $name $version not imported, not available and not in an online gallery, exiting."
                    EXIT 1
                }
            }
        }
    }
}
#EndRegion '.\Private\LoadModule.ps1' 64
#Region '.\Private\NewSRxEventObject.ps1' 0
function NewSRxEventObject 
{
    $SRxEvent = New-Object PSObject -Property @{ 
        "Name" = $( if ([string]::isNullOrEmpty($evaluatedRules.Name)) { $($rule.Rule) } else { $evaluatedRules.Name } );
        "Result" = $( if ($evaluatedRule.Success -is [bool]) { $evaluatedRule.Success } else { $false } );
        "ControlFile" = $ControlFile;
        "RunId" = $timestamp;
        "FarmId" = $global:SRxEnv.FarmId;
        "Source" = $( if ([string]::isNullOrEmpty($xSSA.Name)) { $ENV:ComputerName } else { $xSSA.Name } );
        "Category" = $( if ([string]::isNullOrEmpty($evaluatedRule.category)) { "Undefined" } else { $evaluatedRule.category } );
        "Timestamp" = $(Get-Date -Format u);
        "Dashboard" = $(($global:SRxEnv.Dashboard.Initialized) -and $($rule.WriteToDashboard -eq "true"));
        "Alert" = $( if ($alert -is [bool]) { $alert } else { $false } );
        "Level" = $( if ([string]::isNullOrEmpty($msg.Level)) { "Exception" } else { $msg.Level } );
        "Headline" = $(
            if ([string]::isNullOrEmpty($msg.headline)) {
                $msgHeadline = "-- The test `"" + $($rule.Rule) + "`" provided an empty message headline --"
            } elseif($msg.headline.Length -gt 250) {
                $msgHeadline = $msg.headline.Substring(0,247) + "..."
            } else {
                $msgHeadline = $msg.headline
            }
            $msgHeadline
        );
        "Details" = $( 
            if ($msg.details -is [String]) {
                $msgDetails = $msg.details.split("`n").trimEnd()
            } else {
                $detailStrings = $msg.details | foreach { $_ | Out-String -stream }
                $i = 0;
                $msgDetails = $detailStrings | foreach { if (($i -eq 0) -or ($_ -ne $detailStrings[$i-1] -ne "")) { $_.trimEnd() }; $i++ }
            }
            $msgDetails
        );
        "Data" = $msg.Data;
    }

    #return the event object...
    return $SRxEvent
}
#EndRegion '.\Private\NewSRxEventObject.ps1' 41
#Region '.\Private\ProcessRules.ps1' 0
function ProcessRules 
{
    $global:SRxEnv.SetCustomProperty("EnableProcessRules", $true)
    [int]$counter = 1
    [int]$maxcount = $__rulesControl.Count
    foreach($rule in $__rulesControl)
    {
        if( -not $global:SRxEnv.EnableProcessRules) {
            break
        }
        if([string]::IsNullOrWhiteSpace($rule.Rule)) {continue}
        $start = Get-Date

        $ruleTitle = $($rule.Rule).replace("Rule-","")
        Write-SRx INFO "Executing rule $ruleTitle..." -ForegroundColor Cyan #-NoNewline
        if( $null -ne $global:__onRuleCallback ) { 
# Write-Host ("global:__onRuleCallback = $($global:__onRuleCallback.getType().Name)")
            $global:__onRuleCallback.Invoke( $ruleTitle, $counter++, $maxcount ) 
        }

        $ruleFile = $__ruleDefinitionFiles | ? {$_.Name -eq "$($rule.Rule).ps1"}
        
        if(-not $ruleFile -or $ruleFile.Count -eq 0)
        {
            Write-SRx WARNING "Unable to find rule $($rule.Rule) to run."
            continue
        }
        if($ruleFile.Count -gt 1)
        {
            Write-SRx WARNING "Not running rule: $($rule.Rule)"
            Write-SRx WARNING "Found two or more rules with the same name, $($rule.Rule), in the .\lib\Rules\* folders."
            Write-SRx WARNING "All rules must have unique names."
            continue
        }
        # dot source the test script
        try
        {
            # $params = "-ThresholdsFile file.csv"
            if([string]::IsNullOrEmpty($Params)) 
            {
                $evaluatedRules = & $ruleFile.FullName
            }
            else
            {
                Write-SRx INFO "Params found. Invoking expression..."
                $evaluatedRules = Invoke-Expression "& '$($ruleFile.FullName)' $Params"
            }
            foreach($evaluatedRule in $evaluatedRules)
            {
                if ($evaluatedRule -is [string]) {
                    Write-SRx INFO "-skipped-> " -ForegroundColor DarkGray -NoNewline
                    Write-SRx INFO $evaluatedRule
                    continue
                } 

                # if no Dashboard, don't write to lists
                if(-not $global:SRxEnv.Dashboard.Initialized)
                {
                    $alert = $false
                }
                else 
                {
                    $alertLevels = @("exception", "error")
                    if ($rule.AlertOnErrorOnly -ne "true") { $alertLevels += "warning" }

                    #We only want to alert on warnings when...
                    $alert = $((-not $evaluatedRule.Success) -and                #the rule evaluation was un-successful and...
                                ($rule.AlertOnFailure -eq "true") -and           # "AlertOnFailure" is defined for this rule and...
                                ($alertLevels -contains ($evaluatedRule.Message.level))) # The alert levels contains the level of the rule
                }
                
                $msg = $evaluatedRule.Message
                NewSRxEventObject
            }
            
         }
        catch
        {
            $evaluatedRules = $null #to prevent bleed over from previous test...
            $msgExcp = "Caught exception while evaluating $($rule.Rule)"
            $l = if($msgExcp.Length -gt 80) {80} else {$msgExcp.Length}

            Write-SRx ERROR $('=' * $l)
            Write-SRx ERROR $msgExcp
            Write-SRx ERROR "Exception: $($_.Exception.Message)"
            Write-SRx ERROR $('-' * $l)
            Write-SRx ERROR $_.InvocationInfo.PositionMessage
            Write-SRx ERROR $_.Exception
            Write-SRx ERROR $('=' * $l)

            $alert = $($rule.AlertOnFailure -eq "true")
            $msg = New-Object PSObject @{ level = "Exception"; headline = $_.Exception.Message; details = $_.InvocationInfo.PositionMessage; }
            NewSRxEventObject
        }
        finally
        {
            $end = Get-Date
            $span = New-TimeSpan $start $end
            Write-SRx VERBOSE "Finished evaluating rule $($rule.Rule). Time: [$($span.Hours):$($span.Minutes):$($span.Seconds).$($span.Milliseconds)]"
        }
    }
}
#EndRegion '.\Private\ProcessRules.ps1' 103
#Region '.\Private\SRxEnv.ps1' 0
$SRxLogLevel = [SRxLogLevel]::VERBOSE
#======================================
#== Extensions to the $global:SRxEnv object ==
#======================================
if ($global:SRxEnv.Exists) {
    $global:SRxEnv | Set-SRxCustomProperty "h" $(New-Object PSObject)
    $global:SRxEnv.h | Set-SRxCustomProperty "description" "Utility Helper Methods"
    $global:SRxEnv.h | Add-Member -Force ScriptMethod -Name isNumeric -Value { 
        param ( $value ) 
        return $value -match "^[\d\.]+$"
    }
    $global:SRxEnv.h | Add-Member -Force ScriptMethod -Name isUnknownOrNull -Value { 
        param ( $value ) 
        return ( [string]::IsNullOrEmpty($value) -or ( ($value -is [string]) -and ($value -eq 'unknown') ) )
    }
    $global:SRxEnv.h | Add-Member -Force ScriptMethod -Name isUnknownOrZero -Value { 
        param ( $value ) 
        return (
            ([string]::IsNullOrEmpty($value)) -or ($value -eq 0) -or ( 
               ($value -is [string]) -and (($value -eq "0") -or ($value -eq 'unknown'))
            ))
    }
    $global:SRxEnv.h | Add-Member -Force ScriptMethod -Name isUnknown -Value { 
        param ( $value ) 
        return ( ($value -is [string]) -and ($value -eq 'unknown') )
    }
    $global:SRxEnv.h | Add-Member -Force ScriptMethod -Name GetDiscreteTime -Value { 
        param ( [datetime]$t, $interval = 15 )
        $midPoint =  [math]::Round($interval/2)
        
        #Rounds a datetime to a discrete point in time (e.g. useful for charting points in time)
        return $t.AddMinutes( 
                    $($x = $t.Minute % $interval; if ($x -lt $midPoint) {(-1)*$x } else {$interval-$x}) 
                ).AddSeconds( (-1)*($t.Second) )
    }
    $global:SRxEnv.h | Add-Member -Force ScriptMethod -Name BuildHandleMappings -Value { 
        param ( [bool]$invalidatePreviousMappings = $false )
        
        $map = @()
        foreach ($name in $global:___SRxCache.SSASummaryList.Name) {
            $handle = "" #the assumed value
            if (-not $invalidateSiblingMappings) {
                if (($name -eq $global:SRxEnv.SSA) -and ($global:SRxEnv.Dashboard.Handle -ne $null))  { 
                    $handle = $global:SRxEnv.Dashboard.Handle
                } else {
                    $mappedHandle = $($global:SRxEnv.HandleMap | Where {$_.Name -eq $name}).Handle
                    if ($mappedHandle.count -gt 1) {
                        #this path should not happen *(implies multiple SSAs with the same name)
                        Write-SRx WARNING $("[`$global:SRxEnv.h.BuildHandleMappings] Invalid HandleMap: Multiple SSAs map to '" + $SSA + "' ...ignoring map")
                        $mappedHandle = ""
                    }
                    if (-not [string]::isNullOrEmpty($mappedHandle)) {
                        $handle = $mappedHandle
                    }
                }
            }
            Write-SRx VERBOSE $(" --> Mapping SSA '" + $name + "', to Handle: '" + $handle + "'")
            $map += $( New-Object PSObject -Property @{ "Name" = $name; "Handle" = $handle } )
        }
        $global:SRxEnv.PersistCustomProperty("HandleMap", $map)
    }
}
#EndRegion '.\Private\SRxEnv.ps1' 63
#Region '.\Public\Connect-SRxProvisioningDatabase_JSON.ps1' 0
Function Connect-SRxProvisioningDatabase_JSON {
    [CmdletBinding()]
    PARAM (
    )
    PROCESS {
        try {
            $Reloaded = $false
            # Work folder
            $profilesFolderPath = $global:SRxEnv.CachePath + "\Profiles"
           
            if (!(Test-Path $profilesFolderPath))
            {
                Write-SRx VERBOSE " > Create the new folder $profilesFolderPath"
                New-Item -itemType Directory -Path $profilesFolderPath | Out-Null
                $Reloaded = $true
            }
           
            if( $global:SRxEnv.Tenancy.TermGroupID -ne "") {
                #ensure folder for term group
                $sgroupName = (Remove-SRxStringSpecialCharacter -String $global:SRxEnv.Tenancy.TermGroupName)
                $profileFolderPath = $profilesFolderPath + "\" + $sgroupName
                 
                if (!(Test-Path $profileFolderPath))
                {
                    Write-SRx VERBOSE " > Create the new folder $profileFolderPath"
                    New-Item -itemType Directory -Path $profileFolderPath | Out-Null
                    $Reloaded = $true
                }
                else {
                    $configDPath = $profileFolderPath + "\ProvisioningDatabase.min.json"
                    if (!(Test-Path $configDPath))
                    {
                        Save-SRxProfile -TermGroupName $global:SRxEnv.Tenancy.TermGroupName
                        #Write-SRx VERBOSE " ? Can't find ProvisioningDatabase.min.json"
                        #$Reloaded = $true
                    }
                    if ((Test-Path $configDPath)) 
                    {
# Write-SRx VERBOSE " > Loading provisioning schema $configDPath ..."
                        $fileStamp = (Get-ChildItem -path $configDPath).LastWriteTime
                        if( $global:SRxEnv.ProvisioningDatabaseTimeStamp ) {
                           if( $global:SRxEnv.ProvisioningDatabaseTimeStamp -ne $fileStamp) { $Reloaded = $true }
                        }

                        $configDB = Get-Content $configDPath -Raw -ErrorAction Stop
                        $global:SRxDB = ConvertFrom-Json $configDB #-Depth 1024


                        $propertyDBValue = $SRxDB.Templates.ProvisioningTemplate.TermGroups.TermGroup.TermSets.TermSet#.Terms
                        #Write-SRx VERBOSE " > Provisioning schema $($propertyDBValue.ID) loaded"
                        if($global:SRxEnv.ProvisioningDatabase  ) {
                            $global:SRxEnv.psobject.members.remove("ProvisioningDatabase")
                        }
                        $global:SRxEnv | Add-Member -Force -MemberType "NoteProperty" -Name "ProvisioningDatabase" -Value $propertyDBValue
                        $global:SRxEnv.SetCustomProperty( "ProvisioningDatabaseTimeStamp", $fileStamp)
# Write-SRx VERBOSE " > Provisioning schema on <$($SRxDB.Templates.ProvisioningTemplate.TermGroups.TermGroup.Name)> activated" #-ForegroundColor Cyan

                    }                       
                }
            }
            else {
                $Reloaded = $true
            }
            return $Reloaded
        }
        catch {
            return $false
        }
    }
}
#EndRegion '.\Public\Connect-SRxProvisioningDatabase_JSON.ps1' 71
#Region '.\Public\Connect-SRxSPOService.ps1' 0
function Connect-SRxSPOService {
    <#
    .SYNOPSIS
        Create a new stored credential stored on local machine
 
    .DESCRIPTION
        This function will save a new stored credential to a .cred file into current user's folder on local machine.
         
    .NOTES
        =========================================
        Project : The Source Shell (SRx)
        -----------------------------------------
        File Name : Connect-SRxSPOService.psm1
        Author : Nikolay Mukhin
        Requires :
            PowerShell Version 5.1, The Source Shell (SRx), Microsoft.SharePoint.PowerShell
        ========================================================================================
         
    .INPUTS
     
    .OUTPUTS
     
    .EXAMPLE
        Connect-SRxSPOService
    #>

    
        [CmdletBinding()]
        param (
        )    
        BEGIN 
        {
            Write-SRx DEBUG "BEGIN Connect-SRxSPOService"
        }
        PROCESS
        {
            Write-SRx DEBUG "PROCESS - Entered Connect-SRxSPOService process"
            try {            
                if( $global:SRxEnv.Tenancy.LoginAs) { #$global:SRxEnv.LoginAs) {
                    $cred = Get-SRxStoredCredential -UserName $global:SRxEnv.Tenancy.LoginAs #$global:SRxEnv.LoginAs
                }
                else {
                    $cred = Get-SRxStoredCredential #-Pa #currently logged-in user's pa-account
                }
            
                if( -not $cred) {
                    return $true #$Setup
                    #if( CredentialsPrompt -eq 1) {
                    # throw "[shell] Please use New-SRxStoredCredential cmdlet to store credentials for your account"
                    #}
                    #else {
                    # New-SRxStoredCredential
                    #}
                    #Write-SRx INFO $("[shell] Please restart shell with key -RebuildSRx to re-connected tenancy with updated credentials.") -ForegroundColor Red # -NoNewline
                } 
                else {
                    try {

                        $tenancyURL = Convert-SRxURL_JSON -URL $global:SRxEnv.Tenancy.AdminUrl
                        #$tenancyURL = $global:SRxEnv.Tenancy.AdminURL #"https://medline0-admin.sharepoint.com"
                        #if($tenancyURL -eq "") {
                        #
                        #}
                        $SRXEnv.Tenancy.LastConnectionSuccessful = $false
                        $SRXEnv.Tenancy.LastPnPConnectionSuccessful = $false

                        if( -not $global:SRxEnv.Tenancy.MFA) {
                            $global:SRxEnv.SetCustomProperty("PnPCredentials", $cred) 
                            Write-SRx DEBUG "Connect-SPOService on $tenancyURL"

                            Connect-SPOService -Url  $tenancyURL -Credential $cred
                            #$connection = Get-SRxConnection -siteUrl $global:SRxEnv.Tenancy.AdminUrl
                            #if( -not $connection) {
                            # $SRXEnv.Tenancy.LastPnPConnectionSuccessful = $false
                            #}
                        }
                        else {
                            Connect-SPOService -Url  $tenancyURL 
                            #$connection = Get-SRxConnection -siteUrl $global:SRxEnv.Tenancy.AdminUrl
                        }

                        $global:SRxEnv.PnPConnections['SPO'] = $true
                        $SRXEnv.Tenancy.LastConnectionSuccessful = $true
                        $SRXEnv.Tenancy.LastPnPConnectionSuccessful = $true

                        if( $global:SRxEnv.Log.Level -eq "VERBOSE") {
# Write-SRx VERBOSE $(" > Connected to tenancy (SPO Online): ") -NoNewline
# Write-SRx VERBOSE ($tenancyURL)
                        }
                        

                    }
                    catch {
                        #Write-SRx Warning $("[shell] Can't Connect-SPOService on tenancy: $($tenancyURL)")
                        Write-SRx Warning $("[shell] "+ $_.Exception.Message)
                        #Write-SRx Warning $("[shell] Please run New-SRxStoredCredential cmdlet to enter or update stored credentials for $($global:SRxEnv.PnPUserName)")
                        $SRXEnv.Tenancy.LastConnectionSuccessful = $false
                        $SRXEnv.Tenancy.LastPnPConnectionSuccessful = $false
                        $global:SRxEnv.PnPConnections['SPO'] = $null
                        throw $_.Exception.Message
                    }
                }  
            }
            catch {
                #Write-SRx Warning $("[shell] Can't Connect-SPOService on tenancy: $($global:SRxEnv.Tenancy.AdminURL)")
                #Write-SRx Warning $($_.Exception.Message)
                #$global:__SRxHasInitFailure = $true
                $SRXEnv.Tenancy.LastConnectionSuccessful = $false
                $SRXEnv.Tenancy.LastPnPConnectionSuccessful = $false                
            }                 
        }
        END
        {
            Write-SRx DEBUG "END"
        }
}
#EndRegion '.\Public\Connect-SRxSPOService.ps1' 116
#Region '.\Public\Convert-SRxFromXML.ps1' 0
function Convert-SRxFromXML
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true, ValueFromPipeline)]
        [System.Xml.XmlNode]$node,
        #we are working through the nodes

        [string]$Prefix = '',
        #do we indicate an attribute with a prefix?

        $ShowDocElement = $false #Do we show the document element?
    )
    
    process
    {
        #if option set, we skip the Document element
        if ($node.DocumentElement -and !($ShowDocElement))
        { $node = $node.DocumentElement }
        $oHash = [ordered] @{ } # start with an ordered hashtable.
        #The order of elements is always significant regardless of what they are
        write-verbose "calling with $($node.LocalName)"
        if ($null -ne $node.Attributes) #if there are elements
        # record all the attributes first in the ordered hash
        {
            $node.Attributes | ForEach-Object {
                $oHash.$($Prefix + $_.FirstChild.parentNode.LocalName) = $_.FirstChild.value
            }
        }
        # check to see if there is a pseudo-array. (more than one
        # child-node with the same name that must be handled as an array)
        $node.ChildNodes | #we just group the names and create an empty
        #array for each
        Group-Object -Property LocalName | Where-Object { $_.count -gt 1 } | Select-Object Name |
        ForEach-Object{
            write-verbose "pseudo-Array $($_.Name)"
            $oHash.($_.Name) = @() <# create an empty array for each one#>
        };
        foreach ($child in $node.ChildNodes)
        {
            #now we look at each node in turn.
            write-verbose "processing the '$($child.LocalName)'"
            $childName = $child.LocalName
            if ($child -is [system.xml.xmltext])
            # if it is simple XML text
            {
                write-verbose "simple xml $childname";
                $oHash.$childname += $child.InnerText
            }
            # if it has a #text child we may need to cope with attributes
            elseif ($child.FirstChild.Name -eq '#text' -and $child.ChildNodes.Count -eq 1)
            {
                write-verbose "text";
                if ($null -ne $child.Attributes) #hah, an attribute
                {
                    <#we need to record the text with the #text label and preserve all
                    the attributes #>

                    $aHash = [ordered]@{ };
                    $child.Attributes | ForEach-Object {
                        $aHash.$($_.FirstChild.parentNode.LocalName) = $_.FirstChild.value
                    }
                    #now we add the text with an explicit name
                    $aHash.'#text' += $child.'#text'
                    $oHash.$childname += $aHash
                }
                else
                {
                    #phew, just a simple text attribute.
                    $oHash.$childname += $child.FirstChild.InnerText
                }
            }
            elseif ($null -ne $child.'#cdata-section')
            # if it is a data section, a block of text that isnt parsed by the parser,
            # but is otherwise recognized as markup
            {
                write-verbose "cdata section";
                $oHash.$childname = $child.'#cdata-section'
            }
            elseif ($child.ChildNodes.Count -gt 1 -and
                ($child | Get-Member -MemberType Property).Count -eq 1)
            {
                $oHash.$childname = @()
                foreach ($grandchild in $child.ChildNodes)
                {
                    $oHash.$childname += (Convert-SRxFromXML $grandchild)
                }
            }
            else
            {
                # create an array as a value to the hashtable element
                $oHash.$childname += (Convert-SRxFromXML $child)
            }
        }
        $oHash
    }
}
#EndRegion '.\Public\Convert-SRxFromXML.ps1' 98
#Region '.\Public\Convert-SRxURL_JSON.ps1' 0
Function Convert-SRxURL_JSON {
    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$false)][string]$URL,
        [Parameter(Mandatory=$false)][string]$environment
    )
    PROCESS {
        try {

            if($null -eq $URL) {
                return $null
            }
            if($null -eq $environment) {
                $environment = $global:SRxEnv.PnPEnvironment
            }

            $environmentPrefix = ''
            if($environment) {
                #$environmentConfig = ($global:SRxEnv.DefaultSettings.EcoSystem.Environments | Select-Object -ExpandProperty $environment)
                #$environmentPrefix = $environmentConfig.URLPrefix
                $environment_ = Get-SRxTermByCustomProperty_JSON -Key "ts_Environment" -Value $environment
                $environmentPrefix = Get-SRxCustomPropertyValueByKey_JSON -termHost $environment_ -Key "ts_URLPrefix"           

            }

            $masterPrefix = ""
            #$masterConfig = ($global:SRxEnv.DefaultSettings.EcoSystem.Environments | Select-Object -ExpandProperty "Master")
            $master_ = Get-SRxTermByCustomProperty_JSON -Key "ts_Environment" -Value "Master"  
            $masterPrefix = Get-SRxCustomPropertyValueByKey_JSON -termHost $master_ -Key "ts_URLPrefix"         

            #if( $masterConfig) {
            # $masterPrefix = $masterConfig.URLPrefix
            #}

            $str = $URL.replace('{tenancy}', $global:SRxEnv.Tenancy.Name)
            #if( $global:SRxEnv.EcoSystemURLPrefix) {
            # $ecosystemPrefix = $global:SRxEnv.EcoSystemURLPrefix
            #}
            #else {
            $ecosystemPrefix = Get-SRxCustomPropertyValueByKey_JSON -Key "ts_URLPrefix"
            #$ecosystemPrefix = $global:SRxEnv.Tenancy.EcoSystem
            #}
            $str = $str.replace('{ecosystem}',$ecosystemPrefix)

            if($environmentPrefix -eq '' -or $null -eq $environmentPrefix)   { $str = $str.replace('_{environment}',$environmentPrefix) }
            else                                                             { $str = $str.replace('{environment}',$environmentPrefix) }
            if($masterPrefix -eq '' -or $null -eq $masterPrefix)             { $str = $str.replace('_{master}',$masterPrefix) }
            else                                                             { $str = $str.replace('{master}',$masterPrefix) }
        }
        catch {
            return $null
        }
        return $str
    }
}
#EndRegion '.\Public\Convert-SRxURL_JSON.ps1' 56
#Region '.\Public\Get-Master.ps1' 0
function Get-Master {
<#
.SYNOPSIS
    Invokes a test(s) and handles where the output event(s) get written
     
.DESCRIPTION
    This cmdlet wraps the invocation chain of Invoke-ProvisioningSequence followed by:
        -> Export-SRxToSearchDashboard | Export-SRxToAlertsList
        -> and/or Write-SRxConsole
     
.NOTES
    =========================================
    Project : The Source Shell (SRx)
    -----------------------------------------
    File Name : Get-Master.psm1
    Author : Nikolay Mukhin
    Contributors: Eric Dixon
    Requires :
        PowerShell Version 5.1, The Source Shell (SRx), Microsoft.SharePoint.PowerShell
    ========================================================================================
     
.INPUTS
    Control file (e.g. TestControl.csv)
 
.OUTPUTS
    [ $SRxEvent(s) ]
 
.EXAMPLE
 
#>


    [CmdletBinding()]
    param ( 
            [alias("All")][switch]$RunAllTests,
            [alias("OutNull")][switch]$NoWriteToConsole,
            [switch]$PassThrough,
            [alias("EventLog")][switch]$WriteErrorsToEventLog,
            [switch]$Details,
            [Parameter(Mandatory=$false, ParameterSetName="Get")]
            [string]$ControlFile,
            [Parameter(Mandatory=$false, ParameterSetName="Get")]
            [string]$Test
    )
    BEGIN 
    {
        Write-SRx DEBUG "BEGIN Get-Master"
    }
    PROCESS
    {
        Write-SRx DEBUG "PROCESS - Entered Get-Master process"

        if($Test)
        {
            if($PSCmdlet.MyInvocation.BoundParameters["Debug"].IsPresent) {
                $output = New-SRxReport -Test $Test -Debug
            } else {
                $output = New-SRxReport -Test $Test 
            }
        }
        elseif($ControlFile)
        {
            $output = New-SRxReport -ControlFile $ControlFile 
        }
        else
        {
            $output = New-SRxReport -ControlFile "Get-Master"
        }
        if($PassThrough)
        {
            $output
        }
    }
    END
    {
        Write-SRx DEBUG "END"
    }
}


#EndRegion '.\Public\Get-Master.ps1' 80
#Region '.\Public\Get-SRxConnection.ps1' 0
Function Get-SRxConnection {
    PARAM (
        [Parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$true)][string]$siteURL,
        [Parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$false)][string]$clientId,
        [Parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$false)][string]$thumbprint,
        [switch]$MFA,
        [switch]$APP,
        [switch]$SPO,
        [switch]$Force
    )
    BEGIN {
        $method = "?"
        $connection = $null
        $key = Convert-SRxURL_JSON -URL $siteURL
        $tenant = Convert-SRxURL_JSON -URL $global:SRxEnv.Tenancy.Tenant
        if( -not $clientId) { $clientId = $global:SRxEnv.Tenancy.ClientId }
        if( -not $thumbprint) { $thumbprint = $global:SRxEnv.Tenancy.Thumbprint }
        try {
            if($SPO) {
                $method = "SPO"
                if( -not $global:SRxEnv.PnPConnections['SPO']) {
                    Connect-SRxSPOService
                }
                return
            }
            elseif($MFA) {
                $method = "MFA"
                if($global:SRxEnv.PnPConnections.ContainsKey($key)) { 
                    $method += " --> cache"
                    $connection = $global:SRxEnv.PnPConnections[$key] 
                }
                else {
                    $connection = Connect-PnPOnline -Url $key -Interactive -ReturnConnection -WarningAction SilentlyContinue                    
                }
            }
            elseif($APP) {
                #https://nonodename.com/post/termstore/
                #https://www.leonarmston.com/2022/01/pnp-powershell-csom-now-works-with-sharepoint-sites-selected-permission-using-azure-ad-app/
                $method = "APP"
                if(($global:SRxEnv.Tenancy.PnPLogin -eq "application") -or $Force) {
                    $connection = Connect-PnPOnline -Url $key -ClientId $clientId -Tenant $tenant -Thumbprint $thumbprint -ReturnConnection
                }
                else {
                    $method += " --> MFA"
                    $connection = Connect-PnPOnline -Url $key -Interactive -ReturnConnection -WarningAction SilentlyContinue                    
                }
            }
            elseif($global:SRxEnv.Tenancy.PnPLogin -eq "application") {
                $method = "APP"
                $adminURL = Convert-SRxURL_JSON -URL $global:SRxEnv.Tenancy.AdminUrl
                if( $key -eq $adminURL) {
                    if($global:SRxEnv.Tenancy.MFA) {
                        $method += " --> MFA"
                        $connection = Connect-PnPOnline -Url $key -Interactive -ReturnConnection -WarningAction SilentlyContinue
                    }
                    else {
                        $method += " --> account"
                        $connection = Connect-PnPOnline -Url $key -Credentials $global:SRxEnv.PnPCredentials -ReturnConnection
                    }
                }
                else {
                    $connection = Connect-PnPOnline -Url $key -ClientId $clientId -Tenant $tenant -Thumbprint $thumbprint -ReturnConnection
                }
            }
            elseif($global:SRxEnv.Tenancy.MFA) {
                $method = "MFA"
                $connection = Connect-PnPOnline -Url $key -Interactive -ReturnConnection -WarningAction SilentlyContinue
            }
            else {
                $method = "account"
                $connection = Connect-PnPOnline -Url $key -Credentials $global:SRxEnv.PnPCredentials -ReturnConnection
            }
            #$connection = Connect-PnPOnline -Url $siteURL -UseWebLogin -ReturnConnection -WarningAction SilentlyContinue
            if( -not $connection)     {    $SRXEnv.Tenancy.LastPnPConnectionSuccessful = $false    }
            else                     {    $SRXEnv.Tenancy.LastPnPConnectionSuccessful = $true        }
            $global:SRxEnv.PnPConnections[$key] = $connection
        }
        catch {
            $SRXEnv.Tenancy.LastPnPConnectionSuccessful = $false
            $global:SRxEnv.PnPConnections[$key] = $null
            Write-SRx ERROR $(" > Failed PnPOnline ($method) to $key")
            throw $_.Exception.Message
            return $null 
        }
        Write-SRx VERBOSE $(" > Connected PnPOnline ($method) to $key")
        return $connection
    }
}
#EndRegion '.\Public\Get-SRxConnection.ps1' 89
#Region '.\Public\Get-SRxCustomizationWriter.ps1' 0
function Get-SRxCustomizationWriter{
    return [CustomizationWriter]::new()
}
#EndRegion '.\Public\Get-SRxCustomizationWriter.ps1' 4
#Region '.\Public\Get-SRxCustomPropertyValueByKey_JSON.ps1' 0
Function Get-SRxCustomPropertyValueByKey_JSON {
    [CmdletBinding()]
    PARAM (
        #[Parameter(Mandatory=$false)]$termHost, #TermSet or Term
        [Parameter(ValueFromPipeline,Mandatory=$false)][pscustomobject]$termHost,
        [Parameter(Mandatory=$true)][string]$Key,
        [switch]$ToUrl,
        [switch]$ToBoolean                               
    )
    PROCESS {
        try {
            $retVal = $null
            if( -not $termHost) { $termHost = $global:SRxEnv.ProvisioningDatabase }
            if( -not $termHost.CustomProperties) { return $retVal }

            if( $termHost.CustomProperties -is [array] ) {
                $properties = $termHost.CustomProperties| Where-Object {$_.Key -eq $Key }
                if ($null -ne $properties) {
                    #return $properties[0].Value
                    $retVal = $properties[0].Value
                }                
            }
            else {
                $property = $termHost.CustomProperties.Property
                if( $null -ne $property) {
                    #Write-SRx WARNING "76 $($property.Key) ?? $Key"
                    if( $property.Key -eq $Key) {
                        #Write-SRx ERROR "+ $($property.Value)"
                        #return $property.Value
                        $retVal = $property.Value
                    }
                }
            }
            
            if( $retVal ) {
                if( $ToUrl ) {
                    $environment = $global:SRxEnv.PnPEnvironment
                    $environment_ = Get-SRxTermByCustomProperty_JSON -Key "ts_Environment" -Value $environment
                    $environmentPrefix = Get-SRxCustomPropertyValueByKey_JSON -termHost $environment_ -Key "ts_URLPrefix"           
                    $master_ = Get-SRxTermByCustomProperty_JSON -Key "ts_Environment" -Value "Master"  
                    $masterPrefix = Get-SRxCustomPropertyValueByKey_JSON -termHost $master_ -Key "ts_URLPrefix"         
                    $str = $retVal.replace('{tenancy}', $global:SRxEnv.Tenancy.Name)
                    $str = $str.replace('{site}','/')
                    $ecosystemPrefix = Get-SRxCustomPropertyValueByKey_JSON -Key "ts_URLPrefix"
                    $str = $str.replace('{ecosystem}',$ecosystemPrefix)
        
                    if($environmentPrefix -eq '' -or $null -eq $environmentPrefix)   { $str = $str.replace('_{environment}',$environmentPrefix) }
                    else                                                             { $str = $str.replace('{environment}',$environmentPrefix) }
                    if($masterPrefix -eq '' -or $null -eq $masterPrefix)             { $str = $str.replace('_{master}',$masterPrefix) }
                    else                                                             { $str = $str.replace('{master}',$masterPrefix) }
            
                    return $str
                }
                elseif( $ToBoolean) {
                    return [System.Convert]::ToBoolean($retVal)
                }
                else {
                    return $retVal
                }
            }
        }
        catch {
            return $null
        }
        return $null
    }
}
#EndRegion '.\Public\Get-SRxCustomPropertyValueByKey_JSON.ps1' 68
#Region '.\Public\Get-SRxHostingTerm.ps1' 0
Function Get-SRxHostingTerm {
    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$false)][switch]$Site,
        [Parameter(Mandatory=$false)][switch]$Sites,
        [Parameter(Mandatory=$false)][switch]$Design,
        [Parameter(Mandatory=$false)][switch]$Environment,            
        [Parameter(Mandatory=$false)][switch]$Profile,            
        [Parameter(Mandatory=$false)][string]$siteID,
        [Parameter(Mandatory=$false)][string]$siteDesign,
        [Parameter(Mandatory=$false)][string]$siteEnvironment,            
        [Parameter(Mandatory=$true)]$connection
    )
    PROCESS {
        try {
            $termHost = $null
            if($Profile) {
                $termHost = Get-PnPTermSet -Identity $global:SRxEnv.Tenancy.TermSetID -TermGroup $global:SRxEnv.Tenancy.TermGroupName -Connection $connection
                if( -not $termHost) { Write-SRx VERBOSE $(" ? Profile term not found") }
            }
            elseif($Environment) {

                $environment_ = Get-SRxTermByCustomProperty_JSON -Key "ts_Environment" -Value $siteEnvironment
                $termEnvironmentID = $environment_.ID

                $termHost = Get-PnPTerm -Identity $([GUID]$termEnvironmentID) -ErrorAction SilentlyContinue -IncludeChildTerms -Connection $connection
                if( -not $termHost) { Write-SRx VERBOSE $(" ? $siteEnvironment environment term not found") }
            }
            elseif($Design) {

                $environment_ = Get-SRxTermByCustomProperty_JSON -Key "ts_Environment" -Value $siteEnvironment
                $termEnvironmentID = $environment_.ID

                $termEnvironment = Get-PnPTerm -Identity $([GUID]$termEnvironmentID) -ErrorAction SilentlyContinue -IncludeChildTerms -Connection $connection
                $termHost = Get-SRxTermsWithCustomProperty -termsHost $termEnvironment  -customPropertyKey "ts_Design" -customPropertyValue $siteDesign -connection $connection
                if( -not $termHost) { Write-SRx VERBOSE $(" ? ts_Design = $siteDesign term not found") }
            }
            elseif($Site) {
                $termHost = Get-PnPTerm -Identity $([GUID]$siteID) -ErrorAction SilentlyContinue -IncludeChildTerms -Connection $connection
                if( -not $termHost) { Write-SRx VERBOSE $(" ? Site term not found") }
            }
            elseif($Sites) {
                $environment_ = Get-SRxTermByCustomProperty_JSON -Key "ts_Environment" -Value $siteEnvironment
                $sites_ = Get-SRxTermByCustomProperty_JSON -termHost $environment_ -Key "ts_Sites" -Value $siteDesign
                $termHost = Get-PnPTerm -Identity $([GUID]$($sites_.ID)) -ErrorAction SilentlyContinue -IncludeChildTerms -Connection $connection
            }
            if( $termHost) {
                Get-PnPProperty -ClientObject $termHost -Property CustomProperties, Id, Name, Terms -Connection $connection | Out-Null                
            }
            return $termHost
        }
        catch {

        }
    }
}
#EndRegion '.\Public\Get-SRxHostingTerm.ps1' 57
#Region '.\Public\Get-SRxIsConnected.ps1' 0
function Get-SRxIsConnected {
    $connected = $false
    try {
        $config = Get-Content $global:SRxEnv.CustomConfig -Raw
        $customConfig = ConvertFrom-Json $config
        $propName = "Tenancy"
        $global:SRxEnv.SetCustomProperty($propName, $($customConfig.$propName))
        $provisioningDBExists = Test-SRxProvisioningDatabase_JSON
        if( $($global:SRxEnv.Tenancy.LastConnectionSuccessful) -and $($global:SRxEnv.Tenancy.LastPnPConnectionSuccessful) -and $provisioningDBExists) { #$spo -and $pnp ) { #-and $($global:SRxEnv.Tenancy.LastConnectionSuccessful)) {
            $connected = $true
        }  
    } catch {}
    return $connected
}
#EndRegion '.\Public\Get-SRxIsConnected.ps1' 15
#Region '.\Public\Get-SRxRemoteCommand.ps1' 0
function Get-SRxRemoteCommand {
    [CmdletBinding()]
    PARAM(
        [Parameter(Mandatory=$true)][string]$name,
        [Parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$false)][string]$siteTitle,
        [Parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$false)][string]$environment,
        [Parameter(ValueFromPipeline=$true, Mandatory=$false)]$pipeline
    )
    PROCESS {
        $filter = $null
        if( $null -ne $global:SRxEnv.FilterNodes) { $filter = $global:SRxEnv.FilterNodes.Clone()}
        $command = @{
            ts_ControlFile = $name
            ts_Environment = $environment
            ts_Title       = $siteTitle
            ts_IsAdmin     = $false
            ts_NewSite     = $false
            ts_Rebuild     = $false
            pipeline       = $pipeline #$_
            filter         = $filter
        }
        return $command
    }
}
#EndRegion '.\Public\Get-SRxRemoteCommand.ps1' 25
#Region '.\Public\Get-SRxStoredCredential.ps1' 0
function Get-SRxStoredCredential {
    <#
    .SYNOPSIS
        Retrieves user credentials stored on local machine
 
    .DESCRIPTION
        This cmdlet retieves sredentials saved by current on local machine.
         
    .NOTES
        =========================================
        Project : The Source Shell (SRx)
        -----------------------------------------
        File Name : Get-SRxStoredCredential.psm1
        Author : Nikolay Mukhin
        Requires :
            PowerShell Version 5.1, The Source Shell (SRx), Microsoft.SharePoint.PowerShell
        ========================================================================================
         
    .INPUTS
        -List
                    no keys assumes current user's login name
        -Pa "pa-" current user's login name
        -UserName any desired login name
     
    .OUTPUTS
        $Credential object
     
    .EXAMPLE
        $cred = Get-SRxStoredCredential -List
        $cred = Get-SRxStoredCredential
        $cred = Get-SRxStoredCredential -Pa
        $cred = Get-SRxStoredCredential -UserName "nmukhin@medline.com"
    #>

    
        [CmdletBinding(DefaultParameterSetName = 'List')]
        param (
            [Parameter(Mandatory=$false, ParameterSetName="Get")][string]$UserName,
            [Parameter(Mandatory=$false, ParameterSetName="List")][switch]$List#,
            #[Parameter(Mandatory=$false, ParameterSetName="List")]
            #[switch]$Pa
        )    
        BEGIN 
        {
            Write-SRx DEBUG "BEGIN Get-SRxStoredCredential"
        }
        PROCESS
        {
            Write-SRx DEBUG "PROCESS - Entered Get-SRxStoredCredential process"
    
            $KeyPath = $home + "\" + $env:computername
            if (!(Test-Path $KeyPath)) {
                
                try {
                    #Write-SRx VERBOSE $("<--Creating-user's-local-store-folder-- " + $KeyPath) -ForegroundColor Yellow
                    New-Item -ItemType Directory -Path $KeyPath -ErrorAction STOP | Out-Null
                }
                catch {
                    Write-SRx Warning $("[shell] Can't create user's local store folder: " + $_.Exception.Message)
                    throw $_.Exception.Message
                }           
            }
    
            if ($List) {
    
                try {
                    #Write-SRx VERBOSE $(" > Enumerating-credentials-in-user's-local-store-folder-- " + $KeyPath) #-ForegroundColor Gray
                    $CredentialList = @(Get-ChildItem -Path $keypath -Filter *.cred -ErrorAction STOP)
        
                #foreach ($Cred in $CredentialList) {
                # Write-Host "Username: $($Cred.BaseName)"
                # }
                }
                catch {
                    Write-SRx Warning $("[shell] Can't enumerate credentials stored in user's local store folder: " + $_.Exception.Message)
                    #Write-Warning $_.Exception.Message
                }
                return $CredentialList
            }
    
            $domain = "@$($global:SRxEnv.Tenancy.Domain)"
            if( !($UserName)) {
                #if($Pa) {
                    $UserName = $global:SRxEnv.Tenancy.LoginPrefix + "$env:UserName$domain" #@medline.com"
                #}
                #else {
                # $UserName = "$env:UserName$domain" #@medline.com"
                #}
            }
            
# Write-SRx VERBOSE $(" > Using credentials for " + $Username) #-ForegroundColor Yellow
            if (Test-Path "$($KeyPath)\$($Username).cred") {
                
                $PwdSecureString = Get-Content "$($KeyPath)\$($Username).cred" | ConvertTo-SecureString                    
                $Credential = New-Object System.Management.Automation.PSCredential -ArgumentList $Username, $PwdSecureString
                $global:SRxEnv.SetCustomProperty("PnPUserName", $Username)
            }
            else {
                $global:SRxEnv.SetCustomProperty("PnPUserName", $null)
                Write-SRx Warning $("[shell] Can't find stored credentials for $($Username) on local computer")
                $Credential = $null
                            
            }
        
            return $Credential
            
        }
        END
        {
            Write-SRx DEBUG "END"
        }
}
#EndRegion '.\Public\Get-SRxStoredCredential.ps1' 112
#Region '.\Public\Get-SRxTermByCustomProperty_JSON.ps1' 0
Function Get-SRxTermByCustomProperty_JSON {
    [CmdletBinding()]
    PARAM (
        [Parameter(ValueFromPipeline,Mandatory=$false)][pscustomobject]$termHost, #TermSet or Term [Parameter(ValueFromPipeline)][pscustomobject]$Thing
        [Parameter(Mandatory=$false)][string]$Key,
        [Parameter(Mandatory=$false)][string]$Value,
        [switch]$ID

    )
    PROCESS {
        try {
            $result = @()
            if( -not $termHost) { $termHost = $global:SRxEnv.ProvisioningDatabase }

            $propertyValue = $null
            if( -not $ID) { 
                $propertyValue = Get-SRxCustomPropertyValueByKey_JSON -termHost $termHost -Key $Key 
#Write-SRx WARNING "propertyValue = $propertyValue"
            }
            else          { $propertyValue = $termHost.ID }

            if( $null -ne $propertyValue) {
#Write-SRx VERBOSE "1"
                if( $Value ) {
#Write-SRx VERBOSE "2 $Value"
                    if( $propertyValue -eq $Value) {
                        #Write-SRx ERROR "+ $($termHost.Name) =="
                        $result += $termHost 
                    }        
                }
                else {
#Write-SRx WARNING "+ $($termHost.Name)"
                    $result += $termHost 
                }
            }
#Write-Host ($result | ft | out-string)
            if( $null -ne $termHost.Terms) {
#Write-SRx VERBOSE "Terms: $($termHost.Terms.Count)"
                #if( $termHost.Terms -is [array] ) {
                    $termHost.Terms | Resolve-SRxTermsArray_JSON | ForEach-Object { 
                    #@($termHost.Terms) | ForEach-Object {
                        if( $null -eq $_) {
#Write-SRx ERROR "124"
                        }
                        else {
#Write-SRx VERBOSE "$($_.Name)"
                            if( -not $ID) { $arr = Get-SRxTermByCustomProperty_JSON -termHost $_ -Key $Key -Value $Value }
                            else          { $arr = Get-SRxTermByCustomProperty_JSON -termHost $_ -Key $Key -Value $Value -ID }
                            @($arr) | ForEach-Object { 
#Write-SRx WARNING "+ $($_.Name)"
                                $result += $_ 
                            }
                        }
                    } 
            }            
# return $result
        }
        catch {
            Write-SRx ERROR ($_.Exception.Message) 
            Write-SRx VERBOSE ($_.Exception) 
            return $null  
        }
        return $result
    }
}
#EndRegion '.\Public\Get-SRxTermByCustomProperty_JSON.ps1' 66
#Region '.\Public\Get-SRxTermsWithCustomProperty.ps1' 0
Function Get-SRxTermsWithCustomProperty {
    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$true)]$termsHost, #TermSet or Term
        [Parameter(Mandatory=$true)][string]$customPropertyKey,
        [Parameter(Mandatory=$false)]$customPropertyValue,
        [Parameter(Mandatory=$true)]$connection
    )
    PROCESS {
        try {
            $result = @()

            Get-PnPProperty -ClientObject $termsHost -Property Terms, Name -Connection $connection | Out-Null

            #Write-Host "termHost = $($termsHost)"
            $termsHost.Terms | ForEach-Object { 
                if($_.CustomProperties.ContainsKey($customPropertyKey)) {
                    #Write-Host "got Key !"
                    if($customPropertyValue) {
                        if($_.CustomProperties[$customPropertyKey] -eq $customPropertyValue) {
                            #Write-Host "got Value !"
                            $result = $result + $_ 
                        }
                    }  
                    else {
                        $result = $result + $_
                    } 
                }
                if($customPropertyValue) {
                    Get-SRxTermsWithCustomProperty -termsHost $_  -customPropertyKey $customPropertyKey -customPropertyValue $customPropertyValue -connection $connection | ForEach-Object { $result = $result + $_ }
                }
                else {
                    Get-SRxTermsWithCustomProperty -termsHost $_  -customPropertyKey $customPropertyKey -connection $connection | ForEach-Object {  $result = $result + $_ }
                }
            }
            return $result
        }
        catch {
            Write-SRx ERROR ($_.Exception.Message) 
            Write-SRx VERBOSE ($_.Exception) 
            return $null  
        }
    }
}
#EndRegion '.\Public\Get-SRxTermsWithCustomProperty.ps1' 45
#Region '.\Public\Get-SRxWorkFolder.ps1' 0
function Get-SRxWorkFolder {
    [CmdletBinding()]
    PARAM(
        [string]$folder,
        [Parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$true)][string]$siteDesign
    )
    PROCESS {
            # Work folder
            if (!(Test-Path $($global:SRxEnv.MasterCachePath)))
            {
                Write-SRx VERBOSE " > Create the new folder $($global:SRxEnv.MasterCachePath)"
                New-Item -itemType Directory -Path $($global:SRxEnv.MasterCachePath) | Out-Null
            }
            $designFolderPath = $global:SRxEnv.MasterCachePath + "\" + $siteDesign
            if (!(Test-Path $designFolderPath))
            {
                Write-SRx VERBOSE " > Create the new folder $designFolderPath"
                New-Item -itemType Directory -Path $designFolderPath | Out-Null
            }
            $destfolderPath = $designFolderPath + "\$folder"
            if (!(Test-Path $destfolderPath))
            {
                Write-SRx VERBOSE " > Create the new folder $destfolderPath"
                New-Item -itemType Directory -Path $destfolderPath | Out-Null
            }
            return $destfolderPath
    }
}
#EndRegion '.\Public\Get-SRxWorkFolder.ps1' 29
#Region '.\Public\Hide-SRxWindow.ps1' 0
function Hide-SRxWindow {
    $null = [Win32Functions.Win32Windows]::ShowWindowAsync((Get-Process -PID $pid ).MainWindowHandle, 2)    
}
#EndRegion '.\Public\Hide-SRxWindow.ps1' 4
#Region '.\Public\Initialize-SRxEnv.ps1' 0
Function Initialize-SRxEnv {
    [CmdletBinding(PositionalBinding=$true)]
    PARAM (
        [Parameter(Mandatory=$false)][string]$RootPath,
        [Parameter(Mandatory=$false,ValueFromPipeline=$false)]$SourceID,      #== Source ID if process started as command from GUI
        [Parameter(Mandatory=$false,ValueFromPipeline=$false)]$LoadModule2,   
        [switch]$DisableLogToFile
    )
    BEGIN {
        $env:PNPPOWERSHELL_UPDATECHECK = "off"
    }
    PROCESS {
        try {
            if( -not $global:SRxEnv ) {                

                if( -not $RootPath) { $RootPath = Get-Location }

                Set-Location $RootPath

                #Write-Host "init SRxEnv"
                $SRxLogLevel = [SRxLogLevel]::VERBOSE
                                         #$executionContext.SessionState.Path.CurrentLocation
                $SRxRootPath = $rootpath#$PWD.Path #Get-Location
                #Write-Host "SRxRootPath = $SRxRootPath"
                $SRxConfigPath = "\Config\"  #sub-path to the .json config files

                $configPath = Join-Path $(Join-Path $SRxRootPath $SRxConfigPath) "core.config.json" 
                if (-not (Test-Path $configPath)) {
                    Write-Host " ? - can't find $configPath"
                    return 
                }  

                $config = Get-Content $configPath -Raw -Encoding UTF8 -ErrorAction Stop
                $global:SRxEnv = ConvertFrom-Json $config 

                #== Create utility methods for the $global:SRxEnv object
                $global:SRxEnv | Add-Member -Force -MemberType ScriptMethod -Name UpdateShellTitle    -Value {
                    param([string]$trailingText = "")
                    if ($global:SRxEnv.isBuiltinAdmin) { $adminPrefix = "Administrator: " }
                    if ([String]::IsNullOrEmpty($trailingText) -and ($global:SRxEnv.Version -ne "__REPLACE_WITH_VERSION_NUMBER_NAME__")) { 
                        $trailingText = " - " + $global:SRxEnv.Version 
                    }
                    (Get-Host).UI.RawUI.WindowTitle = $($adminPrefix + $global:SRxEnv.Title + " " + $trailingText)
                }
                #-- Set the title for this shell window
                $global:SRxEnv.UpdateShellTitle("(Initializing...)")


                $global:SRxEnv | Add-Member -Force -MemberType ScriptMethod -Name ResolvePath    -Value {
                    param([string]$pathStub = "")
            
                    if (-not [String]::IsNullOrEmpty($pathStub)) { 
                        if ($pathStub.StartsWith("..\")) { 
                            $length = $global:SRxEnv.Paths.SRxRoot.length
                            $root = $global:SRxEnv.Paths.SRxRoot.substring(0, $length -4) #strip /src
                            return Join-Path $root $pathStub.SubString(3) 
                        } #strip off the "..\"
                        elseif ($pathStub.StartsWith(".\")) { return Join-Path $global:SRxEnv.Paths.SRxRoot $pathStub.SubString(2) } #strip off the ".\"
                        elseif ($pathStub.StartsWith($global:SRxEnv.Paths.SRxRoot)) { return $pathStub }
                        elseif (($pathStub.StartsWith("`"$")) -or ($global:SRxEnv.Paths.$pathStub.StartsWith("$"))) {
                            try {
                                return $(Invoke-Expression $global:SRxEnv.Paths.$pathStub)
                            } catch { }
                        } 
                        elseif (Test-Path $pathStub) { return $pathStub }
                    }
                    
                    #if we're still here, then throw a warning
                    Write-SRx WARNING $("~~[`$global:SRxEnv.ResolvePath(`$path)] Unable to resolve path: " + $pathStub)
                }
            
                $global:SRxEnv | Add-Member -Force -MemberType ScriptMethod -Name CreateFileWithReadPermissions -Value { 
                    param ($filePath)
                    
                    if (-not [string]::IsNullOrEmpty($filePath)) {
                        if (-not $(Test-Path $filePath)) { 
                            $file = New-Item -Path $filePath -ItemType File
            
                            # set permissions for user group so any user can open/write (if Admin is the first, other non-Admin users would get locked out)
                            $rule = New-Object System.Security.AccessControl.FileSystemAccessRule(".\Users","FullControl","Allow")
                            $acl = Get-Acl $file.FullName
                            $acl.SetAccessRule($rule)
                            Set-Acl $file.Fullname $acl 
                        }
                    }
                }
            
                $global:SRxEnv | Add-Member -Force -MemberType ScriptMethod -Name SetCustomProperty -Value { 
                    param ([String]$propertyName, $propertyValue)
                    
                    #if (($propertyName.Contains(".")) -and ($(Get-Command "Set-SRxCustomProperty" -ErrorAction SilentlyContinue) -ne $null)) {
                    # $this | Set-SRxCustomProperty $propertyName $propertyValue
                    #} else
                    #{
                        if ( $null -ne $( $this | Get-Member -Name $propertyName) ) { 
                            #if the property already exists, just set it
                            $this.$propertyName = $propertyValue
                        } else {
                            #add the new member property and set it
                            $this | Add-Member -Force -MemberType "NoteProperty" -Name $propertyName -Value $propertyValue 
                        }
                    #}
                }

                $global:SRxEnv | Add-Member -Force -MemberType ScriptMethod -Name PersistCustomProperty -Value { 
                    param ([String]$propertyName, $propertyValue, [bool]$persistToFile = $true)
                    #Write-SRx VERBOSE " > Saving 1"
                    $this.SetCustomProperty($propertyName, $propertyValue)
                    if ($persistToFile) {
                        #Write-SRx VERBOSE " > Saving 2"
                        if ($global:SRxEnv.CustomConfigIsReadOnly) {
                            Write-SRx Warning $("~~~ The property '" + $propertyName + "' cannot be persisted (the custom config file is read only or inaccessible) - skipping...")
                        } 
                        else { 
                            # -- add propert info to $global:SRxEnv.CustomConfig (e.g. custom.config.json)
                            $readlockRetries = 40
                            $persistFailure = $false
                            $config = $null
                            #Write-SRx VERBOSE " > Saving 3"
                            do {
                                try {
                                    #Write-SRx VERBOSE " > Saving 4"
                                    Write-SRx VERBOSE $(" > Saving '" + $propertyName + "' to " + $global:SRxEnv.CustomConfig) #-ForegroundColor Yellow
                                    $config = Get-Content $($global:SRxEnv.CustomConfig) -Raw -Encoding UTF8 -ErrorAction Stop
                                    if ([string]::isNullOrEmpty($config)) {
                                        $persistFailure = $true
                                        Write-SRx Warning $("~~~ The property '" + $propertyName + "' cannot be persisted (the custom config file is null or empty) - skipping...")
                                    } else {                        
                                        try {
                                            $tmpSRxEnv = ConvertFrom-Json $config -ErrorAction SilentlyContinue
                                            if ($null -ne $(Get-Command "Set-SRxCustomProperty" -ErrorAction SilentlyContinue)) {
                                                $tmpSRxEnv | Set-SRxCustomProperty $propertyName $propertyValue
                                            } else {
                                                $tmpSRxEnv | Add-Member -Force -MemberType "NoteProperty" -Name $propertyName -Value $propertyValue
                                            }
                                            $jsonConfig = ConvertTo-Json $tmpSRxEnv -Depth 12
                                        } catch { 
                                            $persistFailure = $true
                                            Write-Error $(" --> Caught exception converting `$global:SRxEnv.CustomConfig to object/JSON. Check for syntax errors - skipping: " + $propertyName)
                                        } 
                                    }
                                } catch [System.IO.IOException] {
                                    if ($readlockRetries -eq 40) { Write-SRx INFO " --> Unable to read $configPath - Retrying up to 10s" -NoNewline -ForegroundColor Yellow }
                                    elseif ($($readlockRetries % 4) -eq 0) {  Write-SRx INFO "." -NoNewline }
                                    Start-Sleep -Milliseconds 250
                                    $readlockRetries--
                                } catch {
                                    #Write-SRx VERBOSE " > Saving 5"
                                    $persistFailure = $true
                                    Write-Error $(" --> Caught exception reading `$global:SRxEnv.CustomConfig when persisting a change - skipping: " + $propertyName)
                                }
                            } while (($readlockRetries -gt 0) -and ([string]::isNullOrEmpty($jsonConfig)) -and (-not $persistFailure))
                            #Write-SRx VERBOSE " > Saving 6"
                            if ([string]::isNullOrEmpty($jsonConfig) -and ($readlockRetries -lt 1)) {
                                Write-SRx Warning $("~~~ Timed out waiting for a read/write lock on `$global:SRxEnv.CustomConfig - skipping: " + $propertyName)
                            } elseif (-not [string]::isNullOrEmpty($jsonConfig)) {
                                try {
                                    $jsonConfig | Set-Content $($global:SRxEnv.CustomConfig) -ErrorAction Stop
                                } catch {
                                    Write-SRx Warning $("~~~ Unable to obtain a read/write lock on `$global:SRxEnv.CustomConfig - skipping: " + $propertyName)
                                }
                            }
                        }
                    }
                }
<#
                $global:SRxEnv | Add-Member -Force -MemberType ScriptMethod -Name LoadModule -Value {
                    param( $name, $version, $debug2)
 
                    $version = if ( $null -eq $version ) { $global:SRxEnv.Version } else { $version }
                    $debug = if ( $null -eq $debug2 ) { $global:SRxEnv.Debug } else { $debug2 }
                    $module = $null
             
                    if ($debug) {
                        #write-host "Module $name debug $version imported."
                        if ($debug2) { $module = $("$PSScriptRoot\Modules\$name\$version\$name.psm1") }
                        else { $module = $(Join-Path $global:SRxEnv.paths.SRxRoot "Modules\$name\$($global:SRxEnv.Version)\$name.psm1") }
                        if ( -not $(Test-Path -path $module -PathType Leaf )) { $module = $null }
                    }
             
                    # If module is imported say that and do nothing
                    if (Get-Module -Name $name -Verbose:$false ) {
                        #write-host "Module $name $version is already imported."
                    }
                    else {
                        if ($debug -and $module) {
                            write-host "Module $name local source code version $version imported."
                            Import-Module $module -DisableNameChecking -Verbose:$false -Force
                        }
                        else {
                            # If module is not imported, but available on disk then import
                            if( $null -ne $(Get-Module -ListAvailable -Name $name -Verbose:$false | Where-Object { $_.Name -eq $name -and $_.Version -eq [System.Version]$version })) {
                                if ( (Import-Module -Name $name -RequiredVersion $version -PassThru -Verbose:$false -WarningAction silentlyContinue).Version -eq [System.Version]$version) {
                                    #write-host "Module $name $version imported."
                                }
                                else {
                                    write-host "Module $name $version not imported, exiting."
                                    EXIT 1
                                }
                            }
                            else {
                                write-host "Module $name $version is not available on disk, trying to import and install..."
                                # If module is not imported, not available on disk, but is in online gallery then install and import
                                if (Find-Module -Name $name -RequiredVersion $version ) { #-MinimumVersion $version
                                    write-host "Module $name $version found."
                                    Install-Module -Name $name -RequiredVersion $version -Force -Verbose:$false -Scope CurrentUser
                                    write-host "Module $name $version installed."
                                    Import-Module $name -RequiredVersion $version -DisableNameChecking -Verbose:$false
                                    write-host "Module $name $version imported."
                                }
                                else {
                                    # If the module is not imported, not available and not in the online gallery then abort
                                    write-host "Module $name $version not imported, not available and not in an online gallery, exiting."
                                    EXIT 1
                                }
                            }
                        }
                    }
                }
#>
                
                #-- local log location path
                $global:SRxEnv.SetCustomProperty("LocalLogPath", $($home + "\" + $env:computername + "\Log"))
                if($LoginAs)    {
                    $global:SRxEnv.SetCustomProperty("LoginAs", $LoginAs)        
                }
                #-- Ensure the $global:SRxEnv.Paths object is defined

                if ($global:SRxEnv.Paths -isNot [PSCustomObject]) { $global:SRxEnv.SetCustomProperty("Paths", $(New-Object PSObject)) }
                
                if (-not $global:SRxEnv.Paths.SRxRoot) { 
                    $global:SRxEnv.Paths | Add-Member -Force "SRxRoot" $SRxRootPath 
                } else { 
                    $global:SRxEnv.Paths.SRxRoot = $SRxRootPath 
                }
                
                if (-not $global:SRxEnv.Paths.Mgmt) { 
                    $global:SRxEnv.Paths | Add-Member -Force "Mgmt" $(Join-Path $SRxRootPath $SRxConfigPath) 
                } else { 
                    $global:SRxEnv.Paths.Mgmt = $global:SRxEnv.ResolvePath($global:SRxEnv.Paths.Mgmt) 
                }

                #== Configure SRx Logging
                #-- Ensure the $global:SRxEnv.Log object is defined
                if ($global:SRxEnv.Log -isNot [PSCustomObject]) { 
                    $logObj = New-Object PSObject -Property @{
                        "Level" = "INFO";
                        "ToFile" = $true #if( $DisableLogToFile ) { $false } else { $true };
                        "RetentionBytes" = 1000MB;
                        "RetentionDays" = 90;
                    }
                    $global:SRxEnv.SetCustomProperty("Log", $logObj)
                }
                if( $DisableLogToFile ) { $global:SRxEnv.Log.ToFile = $false }

                #-- Set the verbosity of logging...
                if ([string]::IsNullOrWhiteSpace($global:SRxEnv.Log.Level)) { 
                    $global:SRxEnv.Log | Add-Member -Force "Level" $SRxLogLevel 
                } else { 
                    $global:SRxEnv.Log.Level = $SRxLogLevel 
                }
                #-- enable/disable logging to a file
                if ($global:SRxEnv.Log.ToFile -is [bool]) { 
                    $actualLogToFileSetting = $global:SRxEnv.Log.ToFile 
                } else { 
                    $actualLogToFileSetting = $false
                    $global:SRxEnv.Log | Add-Member -Force "ToFile" $true
                }
                #-- if the SRx log folder/file is not already configured or it does not exist...
                if ([String]::IsNullOrEmpty($global:SRxEnv.LogFile) -or (-not (Test-Path $global:SRxEnv.LogFile))) {
                    #...then temporarily disable file logging ($EnableLogToFile = $false) until further into the initialization
                    $global:SRxEnv.Log.ToFile = $false
                    #if ($TrackDebugTimings) { $global:SRxEnv.DebugTimings["[shell]"].Add( @{ $(Get-Date) = "Tested path for SRx LogFile, disabled Log.ToFile..." } ) | Out-Null }
                } else { 
                    $global:SRxEnv.Log.ToFile = $actualLogToFileSetting
                    #if ($TrackDebugTimings) { $global:SRxEnv.DebugTimings["[shell]"].Add( @{ $(Get-Date) = "Tested path for SRx LogFile, enabled Log.ToFile..." } ) | Out-Null }
                }
                if( $DisableLogToFile ) { $global:SRxEnv.Log.ToFile = $false }
                #----------------
                # Write-SRx
                #----------------
                #== Apply any overrides supplied in custom.config.json (if it exists)
                if ($SRxConfig -is [String]) {
                    if ($SRxConfig.EndsWith(".config.json")) {
                        $customConfigPath = $global:SRxEnv.ResolvePath($SRxConfig)
                        Write-SRx INFO $("[shell] Custom config specified: ") -ForegroundColor Cyan -NoNewline
                        Write-SRx INFO $customConfigPath
                        if (-not (Test-Path $customConfigPath)) {
                            Write-SRx WARNING $("...ignoring custom config file specified with the -SRxConfig param (file does not exist)")
                            $customConfigPath = $null
                        }
                    } else {
                        Write-SRx WARNING $("[shell] Invalid custom config file specified with the -SRxConfig param (specified file does not end with *.config.json)")
                        $customConfigPath = $null
                    }
                } 

                # Write-SRx VERBOSE " > Loading: ecosystem.config.json ..."
                $ecosystemConfigPath = Join-Path $(Join-Path $SRxRootPath $SRxConfigPath) "ecosystem.config.json"
                $ecosystemDB = Get-Content $ecosystemConfigPath -Raw -Encoding UTF8 -ErrorAction Stop
                $global:SRxEcoSystem = ConvertFrom-Json $ecosystemDB
                if($global:SRxEnv.DefaultSettings  ) {
                    $global:SRxEnv.psobject.members.remove("DefaultSettings")
                }
                $global:SRxEnv | Add-Member -Force -MemberType "NoteProperty" -Name "DefaultSettings" -Value $global:SRxEcoSystem    

                $global:SRxEnv.SetCustomProperty("CachePath", $($home + "\" + $env:computername))
                $global:SRxEnv.SetCustomProperty("MasterCachePath", $($home + "\" + $env:computername + "\MasterCache"))
                $path = $global:SRxEnv.CachePath
                if (-not (Test-Path $path))
                {
                    Write-SRx VERBOSE " > Create the new folder $path"
                    New-Item -itemType Directory -Path $path | Out-Null
                }

                if ([string]::isNullOrEmpty($customConfigPath)) {
                    $coreConfigPath = Join-Path $global:SRxEnv.Paths.Mgmt "custom.config.json"
                    $customConfigPath = Join-Path $global:SRxEnv.CachePath "custom.config.json"
                    if( -not (Test-Path $customConfigPath)) {
                        Copy-Item -Path $coreConfigPath -Destination $customConfigPath

                    }
                }
                #-- Set this property in case any other modules want to persist any custom configuration (even if the path does not exist)
                $global:SRxEnv.SetCustomProperty("CustomConfig", $customConfigPath)
                $global:SRxEnv.SetCustomProperty("CustomConfigIsReadOnly", $UseReadOnlyConfig)

                if ((-not [string]::isNullOrEmpty($global:SRxEnv.CustomConfig)) -and (Test-Path $global:SRxEnv.CustomConfig)) {
                    #Write-SRx VERBOSE $("--Loading--> " + $global:SRxEnv.CustomConfig)
            
                    try {
                        $config = Get-Content $global:SRxEnv.CustomConfig -Raw -Encoding UTF8
                        $customConfig = ConvertFrom-Json $config
                    } catch { 
                        Write-SRx ERROR ("!!! Exception occurred getting content from " + $global:SRxEnv.CustomConfig)
                        Write-SRx ERROR ($_.Exception.Message) 
                        Write-SRx VERBOSE ($_.Exception) 
                        Write-SRx WARNING $("[shell] Unable to load the custom config file; Will block any attempts to persist options here.")
                        $global:SRxEnv.SetCustomProperty("CustomConfigIsReadOnly", $true)
                    }
            
                    #temporary helper function
                    #function OverlaySubProperties () {
                       # #For $customConfig.$propName, extract each child property and append/overwrite as applicable...
                       # $options = $($customConfig.$propName | Get-Member -MemberType NoteProperty -ErrorAction SilentlyContinue).Name
                       # foreach ($configName in $options) {
                          # Write-SRx VERBOSE $("Custom Config -> $propName." + $configName + " = " + [string]($customConfig.$propName.$configName)) -ForegroundColor Yellow
                          # if ( $( $global:SRxEnv.$propName | Get-Member -Name $configName) -ne $null ) {
                             # #if the property already exists, just set it
                             # $global:SRxEnv.$propName.$configName = $customConfig.$propName.$configName
                          # } else {
                             # #add the new member property and set it
                             # $global:SRxEnv.$propName | Add-Member -Force -MemberType "NoteProperty" -Name $configName -Value $customConfig.$propName.$configName
                          # }
                       # }
                    #}
            
                    $propertyBag = $customConfig | Get-Member -MemberType NoteProperty -ErrorAction SilentlyContinue
                    foreach ($propName in $propertyBag.Name) {
            # Write-SRx VERBOSE $(" > Custom Config: " + $propName) -ForegroundColor Yellow
                        switch ($propName) {
                            "Paths" {
                                #OverlaySubProperties
                                
                                #For Paths, extract each child path name and append/overwrite as applicable...
                                $pathNames = $($customConfig.Paths | Get-Member -MemberType NoteProperty -ErrorAction SilentlyContinue).Name
                                foreach ($pathName in $pathNames) {
            # Write-SRx VERBOSE $(" Custom Config -> Path : `$pathName = " + $pathName + " [" + $customConfig.Paths.$pathName + "]") -ForegroundColor Yellow
                                    if ( $null -ne $( $global:SRxEnv.Paths | Get-Member -Name $pathName) ) { 
                                        #if the property already exists, just set it
                                        $global:SRxEnv.Paths.$pathName = $customConfig.Paths.$pathName
                                    } else {
                                        #add the new member property and set it
                                        $global:SRxEnv.Paths | Add-Member -Force -MemberType "NoteProperty" -Name $pathName -Value $customConfig.Paths.$pathName 
                                    }                    
                                }
                                break
                            }
                            "Override" {
                                #Create base object if it does not exist...
                                if ($null -eq $global:SRxEnv.Override) { $global:SRxEnv.SetCustomProperty("Override", (New-Object PSObject)) }
                                #OverlaySubProperties
            
                                #For Override options, extract each child config and append/overwrite as applicable...
                                $options = $($customConfig.Override | Get-Member -MemberType NoteProperty -ErrorAction SilentlyContinue).Name
                                foreach ($configName in $options) {
            # Write-SRx VERBOSE $(" Custom Config -> Override." + $configName + " = " + [string]($customConfig.Override.$configName)) -ForegroundColor Yellow
                                    if ( $null -ne $( $global:SRxEnv.Override | Get-Member -Name $configName) ) { 
                                        #if the property already exists, just set it
                                        $global:SRxEnv.Override.$configName = $customConfig.Override.$configName
                                    } else {
                                        #add the new member property and set it
                                        $global:SRxEnv.Override | Add-Member -Force -MemberType "NoteProperty" -Name $configName -Value $customConfig.Override.$configName 
                                    }                    
                                }
                                break
                            }
                            #"Servers" {
                                # -- future location for next generation server objects --
                            #}
                            "Log" {
                                #For Log options, extract each child config and append/overwrite as applicable...
                                $options = $($customConfig.Log | Get-Member -MemberType NoteProperty -ErrorAction SilentlyContinue).Name
                                if ($null -eq $global:SRxEnv.Log) { $global:SRxEnv.SetCustomProperty("Log", (New-Object PSObject)) }
                                foreach ($configName in $options) {
            # Write-SRx VERBOSE $(" Custom Config -> Log." + $configName + " = " + [string]($customConfig.Log.$configName)) -ForegroundColor Yellow
                                    switch ($configName) {
                                        "ToFile" { 
                                            $actualLogToFileSetting = $customConfig.Log.ToFile 
                                            break
                                        }
                                        "Level" {
                                            if ([int]$([SRxLogLevel]::$($customConfig.Log.Level)) -gt [int]$([SRxLogLevel]::$($SRxLogLevel))) { 
                                                $global:SRxEnv.Log.Level = [SRxLogLevel]::$($customConfig.Log.Level)
                                            }
                                            break
                                        }
                                        default {
                                            if ( $null -ne $( $global:SRxEnv.Log | Get-Member -Name $configName) ) { 
                                                #if the property already exists, just set it
                                                $global:SRxEnv.Log.$configName = $customConfig.Log.$configName
                                            } else {
                                                #add the new member property and set it
                                                $global:SRxEnv.Log | Add-Member -Force -MemberType "NoteProperty" -Name $configName -Value $customConfig.Log.$configName  
                                            }
                                        }
                                    }    
                                }
                                $logObj = $null 
                                break
                            }
                            "HandleMap" {
                                if ($customConfig.HandleMap -is [Hashtable]) {
                                    $global:SRxEnv.SetCustomProperty("HandleMap", @($($customConfig.$propName)))
                                } else {
                                    $global:SRxEnv.SetCustomProperty("HandleMap", $($customConfig.$propName))
                                }
                                break
                            }
                            default { $global:SRxEnv.SetCustomProperty($propName, $($customConfig.$propName)) }
                        }
                    }
            
                    #if ($TrackDebugTimings) { $global:SRxEnv.DebugTimings["[shell]"].Add( @{ $(Get-Date) = "Loaded custom.config.json..." } ) | Out-Null }
                
                } 
                elseif (-not [string]::isNullOrEmpty($global:SRxEnv.CustomConfig)) {
                    Write-SRx VERBOSE $("--Creating--> " + $global:SRxEnv.CustomConfig)
                    #Write-SRx WARNING $global:SRxEnv
                    $global:SRxEnv.CreateFileWithReadPermissions( $($global:SRxEnv.CustomConfig) )
                    try {
                        <#
                        New-Object PSObject -Property @{
                            "Paths" = @{ "Log" = ".\var\log" };
                            "RequiredModules" = @();
                            "Log" = @{
                                "Level" = "INFO";
                                "ToFile" = $true
                                "RetentionBytes" = 1048576000;
                                "RetentionDays" = 90;
                            };
                            "Tmp" = @{
                                "RetentionBytes" = 1048576000;
                                "RetentionDays" = 8;
                            };
                            "Override" = @{
                                "CrawlVisualizationQueryLimit" = 100;
                            };
                        } | ConvertTo-Json | Set-Content $global:SRxEnv.CustomConfig -ErrorAction Stop
                        #>

                        Get-Content $configPath -Raw -ErrorAction Stop | Set-Content $global:SRxEnv.CustomConfig -ErrorAction Stop
                        $logObj = $null
                    } catch {
                        Write-SRx Error $("~~~ Unable to obtain a read/write lock when creating `$global:SRxEnv.CustomConfig - skipping its creation...")
                        $global:SRxEnv.SetCustomProperty("CustomConfigIsReadOnly", $true)
                    }
                    #if ($TrackDebugTimings) { $global:SRxEnv.DebugTimings["[shell]"].Add( @{ $(Get-Date) = "Created custom.config.json..." } ) | Out-Null }
                } else {
                    Write-SRx WARNING $("[shell] `$global:SRxEnv.CustomConfig is null; Will block any attempts to persist options here.")
                    $global:SRxEnv.SetCustomProperty("CustomConfigIsReadOnly", $true)
                }
                
                 #== Configure SRx Logging
                if ($null -ne $logObj) { $global:SRxEnv.PersistCustomProperty("Log", $logObj) }
            
                #-- set the logging path
                if (-not $global:SRxEnv.Paths.Log) { 
                    $global:SRxEnv.Paths | Add-Member -Force "Log" $($global:SRxEnv.LocalLogPath) 
                } else {
                    #-- Ensure $global:SRxEnv.Paths.Log is resolved to a full path (e.g. not a relative path such as .\var\log)
                    #$global:SRxEnv.Paths.Log = $global:SRxEnv.ResolvePath($global:SRxEnv.Paths.Log)
                    $global:SRxEnv.Paths.Log = $global:SRxEnv.LocalLogPath        
                }

                if ((-not $global:SRxEnv.LogFile) -or (-not (Test-Path $global:SRxEnv.LogFile))) {
                    $logFolder = Join-Path $global:SRxEnv.Paths.Log "Session"
                    if (-not (Test-Path $logFolder)) { 
                        New-Item -path $logFolder -ItemType Directory | Out-Null
                    }
                    if($SourceID) {
                        $sourceTag = $SourceID.Replace("-","")
                        $currentLogFile = Join-Path -Path $logFolder $("SRx-" + $(Get-Date -Format "yyyyMMddHHmmss") + "-" + $pid + "-tag-" + $sourceTag + ".log")
                        $global:SRxEnv.SetCustomProperty("SourceTag", $sourceTag)
                    }
                    else {
                        $currentLogFile = Join-Path -Path $logFolder $("SRx-" + $(Get-Date -Format "yyyyMMddHHmmss") + "-" + $pid + ".log")
                    }
                    
                } else {
                    $currentLogFile = $global:SRxEnv.ResolvePath($global:SRxEnv.LogFile)
                }
                $global:SRxEnv.SetCustomProperty("LogFile", $currentLogFile)
                $global:SRxEnv.Log.ToFile = $actualLogToFileSetting
                if( $DisableLogToFile ) { $global:SRxEnv.Log.ToFile = $false }
            # if ($global:SRxEnv.Log.ToFile) {
            <#
                    if( $global:SRxEnv.Log.Level -eq "VERBOSE") {
                        Write-SRx INFO $("[shell] Current log file: ") -ForegroundColor DarkCyan -NoNewline
                        Write-SRx INFO ($global:SRxEnv.LogFile)
                    }
            #>
        
            # }
                #if ($TrackDebugTimings) { $global:SRxEnv.DebugTimings["[shell]"].Add( @{ $(Get-Date) = "Configured SRx log file..." } ) | Out-Null }
                
                #== Set the run-time properties
                $global:SRxEnv.SetCustomProperty("CurrentUser", $SRxCurrentUser)
                $global:SRxEnv.SetCustomProperty("isBuiltinAdmin",    $isBuiltinAdmin)
                            
                #== Update the properties for this PowerShell window (e.g. the UI) ===
                if (-not $global:SRxEnv.UI) { $global:SRxEnv.SetCustomProperty("UI", $( New-Object PSObject -Property @{ "MinWidth" = 125; "MinHeight" = 5000; })) } 
                if ((-not $global:SRxEnv.UI.MinWidth) -or ($global:SRxEnv.UI.MinWidth -lt 125)) { $global:SRxEnv.UI | Add-Member -Force -MemberType "NoteProperty" -Name "MinWidth" -Value 125 } 
                if ((-not $global:SRxEnv.UI.MinHeight) -or ($global:SRxEnv.UI.MinHeight -lt 5000)) { $global:SRxEnv.UI | Add-Member -Force -MemberType "NoteProperty" -Name "MinHeight" -Value 5000 } 

                #-- Ensure minimum width and height for this shell window
                if ($Host -and $Host.UI -and $Host.UI.RawUI) {
                    $rawUI = $Host.UI.RawUI
                    $oldSize = $rawUI.BufferSize
                    if ($oldSize.Width -gt $global:SRxEnv.UI.MinWidth) {
                        $global:SRxEnv.UI.MinWidth = $oldSize.Width
                    }
                    if ($oldSize.Height -gt $global:SRxEnv.UI.MinHeight) {
                        $global:SRxEnv.UI.MinHeight = $oldSize.Height
                    }
                    $typeName = $oldSize.GetType().FullName
                    $newSize = New-Object $typeName ($global:SRxEnv.UI.MinWidth, $global:SRxEnv.UI.MinHeight)
                    $rawUI.BufferSize = $newSize
                }
                #== Resolve any relative or variable valued paths ..and validate existance
                foreach ($pathName in $($global:SRxEnv.Paths | Get-Member | Where-Object {$_.MemberType -eq "NoteProperty"}).Name) {
                    #Ensure this path is resolved to a full path (e.g. not a relative path such as ./somePathName or $PWD/somePathName)
                    if( $pathName -ne "Log") {
                        $global:SRxEnv.Paths.$pathName = $global:SRxEnv.ResolvePath($global:SRxEnv.Paths.$pathName)
                    }
                    
                    switch ($pathName) {
                        "Modules" {
                            #== "Modules" path for "Search" specific modules, "Core" helper modules (e.g. utility functionality), and any product specific modules (e.g. "SP2013", "SP2016", etc)
                            #-- x:\<SRxProjectPath>\Modules
                            if (-not (Test-Path $global:SRxEnv.Paths.Modules)) {    
                                Write-SRx WARNING $("[shell] Invalid path specified for Modules: " + $global:SRxEnv.Paths.Modules)
                                $global:__SRxHasInitFailure = $true
                            }
                            break
                        }
                        "Log" { 
                            #== "Log" path for results and user output
                            #-- x:\<current-path>\var\log
                            if (-not (Test-Path $global:SRxEnv.Paths.Log)) {    
                                New-Item $global:SRxEnv.Paths.Log -Type Directory -ErrorAction SilentlyContinue| Out-Null
                                #Verify that it did get created...
                                if (-not (Test-Path $global:SRxEnv.Paths.Log)) {    
                                    Write-SRx WARNING $("[shell] Unable to create the Log path: " + $global:SRxEnv.Paths.Log)
                                    $global:__SRxHasInitFailure = $true
                                } else {
                                Write-SRx INFO $("[shell] Results and Logging Path: " + $global:SRxEnv.Paths.Log) 
                            }
                            }
                            break
                        }
                        "Scripts" {
                            #== "Scripts" path (e.g. useful for custom scripts)
                            #-- x:\<SRxProjectPath>\Scripts
                            if (-not (Test-Path $global:SRxEnv.Paths.Scripts)) {
                                Write-SRx WARNING $("[shell] Invalid path specified for Scripts: " + $global:SRxEnv.Paths.Scripts)
                            } else {
                                if (-not ($Env:Path).ToLower().Contains($global:SRxEnv.Paths.Scripts.ToLower())) {
                                    #Append to the $ENV:Path (if it doesn't already exist)
                                    $ENV:Path += ";" + $global:SRxEnv.Paths.Scripts
                                    #Write-SRx VERBOSE $(" > Appending the Scripts folder (" + $global:SRxEnv.Paths.Scripts + ") to `$ENV:Path")
                                }
                            }
                            break
                        }
                        "Tools" {
                            #== "Tools" path (e.g. for ULSViewer)
                            #-- x:\<SRxProjectPath>\Tools
                            if (-not (Test-Path $global:SRxEnv.Paths.Tools)) {
                                if ($global:SRxEnv.Paths.Tools.toLower().startsWith($global:SRxEnv.Paths.SRxRoot.toLower())) {
                                    Write-SRx VERBOSE $("[shell] Removing `$global:SRxEnv.Paths.Tools because the specified path does not exist")
                                    $global:SRxEnv.Paths.PSObject.Properties.Remove("Tools")
                                } else {
                                    Write-SRx WARNING $("[shell] Invalid path specified for Tools: " + $global:SRxEnv.Paths.Tools)
                                }
                            } else {
                                if (-not ($Env:Path).ToLower().Contains($global:SRxEnv.Paths.Tools.ToLower())) {
                                    #Append to the $ENV:Path (if it doesn't already exist)
                                    $ENV:Path += ";" + $global:SRxEnv.Paths.Tools
                                    Write-SRx INFO $("[shell] Appending the Tools folder (" + $global:SRxEnv.Paths.Tools + ") to `$ENV:Path")
                            }
                            }
                            break
                        }
                        default {  
                            if (-not (Test-Path $global:SRxEnv.Paths.$pathName)) {    
                                #Write-SRx WARNING $("[shell] Invalid path specified for " + $pathName + ": " + $global:SRxEnv.Paths.$pathName)
                            }
                        }
                    }    
                }
#Write-SRx INFO "9 ---------------------------------"


                $global:SRxEnv.SetCustomProperty("PnPConnections", @{})
                $global:SRxEnv.PnPConnections['SPO'] = $null

                $connected = Get-SRxIsConnected
                $global:SRxEnv.SetCustomProperty("isSetup",    $(-not $connected))
                if($connected) { $global:SRxEnv.SetCustomProperty("isStakeHolder",    $($global:SRxEnv.Tenancy.IsStakeholder)) }

                #final
                $global:SRxEnv.SetCustomProperty("Exists", $true) 
#Write-Host "10"
                #shell
                if($LoadModule2) {
                    $global:SRxEnv | Add-Member -Force -MemberType ScriptMethod -Name LoadModule -Value $LoadModule2
                }
                else {
                    $global:SRxEnv | Add-Member -Force -MemberType ScriptMethod -Name LoadModule -Value $LoadModule
                }
#Write-Host "11"
            }
            <#
            else {
                Write-Host ( $global:SRxEnv | FL | Out-String )
                if( -not $global:SRxEnv.PnPConnections ) {
                    Write-Host "init SRxEnv-1"
                    $global:SRxEnv.SetCustomProperty("PnPConnections", @{})
                    Write-Host "init SRxEnv-2"
                    $global:SRxEnv.PnPConnections['SPO'] = $null
                }
            }
            #>

            
        }
        catch {
            return $null
        }
        return $str
    }
}
#EndRegion '.\Public\Initialize-SRxEnv.ps1' 660
#Region '.\Public\New-SRxReport.ps1' 0
function New-SRxReport {
<#
.SYNOPSIS
    Invokes a test(s) and handles where the output event(s) get written
     
.DESCRIPTION
    This cmdlet wraps the invocation chain of Test-SRx followed by:
        -> Export-SRxToSearchDashboard | Export-SRxToAlertsList
        -> and/or Write-SRxConsole
     
.NOTES
    =========================================
    Project : Search Health Reports (SRx)
    -----------------------------------------
    File Name : New-SRxReport.psm1
    Author : Eric Dixon
    Requires :
        PowerShell Version 5.1, Search Health Reports (SRx), Microsoft.SharePoint.PowerShell
     
    ========================================================================================
    This Sample Code is provided for the purpose of illustration only and is not intended to
    be used in a production environment.
     
        THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY
        OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
        WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
 
    We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to
    reproduce and distribute the object code form of the Sample Code, provided that You agree:
        (i) to not use Our name, logo, or trademarks to market Your software product in
            which the Sample Code is embedded;
        (ii) to include a valid copyright notice on Your software product in which the
             Sample Code is embedded;
        and
        (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against
              any claims or lawsuits, including attorneys' fees, that arise or result from
              the use or distribution of the Sample Code.
 
    ========================================================================================
     
.INPUTS
    Control file (e.g. TestControl.csv)
 
.OUTPUTS
    [ $SRxEvent(s) ]
 
.EXAMPLE
 
#>


    [CmdletBinding()]
    param ( 
            [alias("All")][switch]$RunAllTests,
            [alias("OutNull")][switch]$NoWriteToConsole,
            [switch]$PassThrough,
            [alias("EventLog")][switch]$WriteErrorsToEventLog,
            [switch]$Details
    )

    DynamicParam 
    {
        # Set the dynamic parameters' name
        $ParameterControlFile = 'ControlFile'
        $ParameterTest = 'Test'
        
        # Create the dictionary
        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

        # Create the collection of attributes
        $AttributeCollectionControlFile = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $AttributeCollectionTest = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        
        # Create and set the parameters' attributes
        $ParameterAttributeControlFile = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttributeControlFile.Mandatory = $false
        $ParameterAttributeControlFile.Position = 1

        $ParameterAttributeTest = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttributeTest.Mandatory = $false
        $ParameterAttributeTest.Position = 1

        # Add the attributes to the attributes collection
        $AttributeCollectionControlFile.Add($ParameterAttributeControlFile)

        $AttributeCollectionTest.Add($ParameterAttributeTest)

        # Generate and set the ValidateSet
        $rulesControlFolder = $global:SRxEnv.Paths.RuleSets #Join-Path $global:SRxEnv.Paths.Config "TestControls"
        #$arrSet = Get-ChildItem -Path $rulesControlFolder -File "*.csv" -Recurse | Select-Object -ExpandProperty Name
           $arrSet = Get-ChildItem -Path $rulesControlFolder -File "*.csv" -Recurse `
            | Select-Object -ExpandProperty Name `
            | % {if($_.EndsWith(".csv","CurrentCultureIgnoreCase")){$_.Substring(0,$_.Length-4)}} 

        $ValidateSetAttributeControlFile = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)

        $rulesDefinitions = $global:SRxEnv.Paths.Rules
        #$arrSet2 = Get-ChildItem -Path $rulesDefinitions -File "Rule-*.ps1" -Recurse | Select-Object -ExpandProperty Name
        $arrSet2 = Get-ChildItem -Path $rulesDefinitions -File "Rule-*.ps1" -Recurse `
            | Select-Object -ExpandProperty Name `
            | % {if($_.StartsWith("Rule-","CurrentCultureIgnoreCase")){$_.Substring(5)}} `
            | % {if($_.EndsWith(".ps1","CurrentCultureIgnoreCase")){$_.Substring(0,$_.Length-4)}} 
        $ValidateSetAttributeTest = New-Object System.Management.Automation.ValidateSetAttribute($arrSet2)

        # Add the ValidateSet to the attributes collection
        $AttributeCollectionControlFile.Add($ValidateSetAttributeControlFile)

        $AttributeCollectionTest.Add($ValidateSetAttributeTest)

        # Create and return the dynamic parameter
        $RuntimeParameterControlFile = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterControlFile, [string], $AttributeCollectionControlFile)
        $RuntimeParameterDictionary.Add($ParameterControlFile, $RuntimeParameterControlFile)
        $RuntimeParameterTest = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterTest, [string], $AttributeCollectionTest)
        $RuntimeParameterDictionary.Add($ParameterTest, $RuntimeParameterTest)
        return $RuntimeParameterDictionary
    }

    BEGIN 
    {
        Write-SRx DEBUG "BEGIN New-SRxReport"
        $ControlFile = $PsBoundParameters[$ParameterControlFile]
        $Test = $PsBoundParameters[$ParameterTest]
        if($RunAllTests)
        {
            Write-SRx VERBOSE "Running SRx Report -RunAllTests"
        }
        elseif($Test)
        {
            Write-SRx VERBOSE "Running SRx Report with test: $Test"
        }
        elseif($ControlFile)
        {
            Write-SRx VERBOSE "Running SRx Report with control file: $ControlFile"
        }
    }
    PROCESS
    {
        Write-SRx DEBUG "PROCESS - Entered New-SRxReport process"

        if($RunAllTests)
        {
            $output = Test-SRx -RunAllTests 
        }
        elseif($Test)
        {
            if($PSCmdlet.MyInvocation.BoundParameters["Debug"].IsPresent) {
                $output = Test-SRx -Name $Test -Debug
            } else {
                $output = Test-SRx -Name $Test 
            }
        }
        elseif($ControlFile)
        {
            $output = Test-SRx -ControlFile $ControlFile 
        }
        else
        {
            Write-SRx ERROR "You must supply command line parameter -Test <test name>, -ControlFile <control file name>, or -RunAllTests"
            return
        }

        if($global:SRxEnv.Dashboard.Initialized)
        {
            $output | Export-SRxToSearchDashboard | Export-SRxToAlertsList | Out-Null
        }

        if($global:SRxEnv.OMS.Initialized)
        {
            $output | Export-SRxToOMS | Out-Null
        }

        if(-not $NoWriteToConsole -and $output -ne $null)
        {
            $output | Write-SRxConsole -Details:$Details
        }

        if($WriteErrorsToEventLog)
        {
            foreach($o in $output)
            {
                $message = $null
                if($o.Level -eq "Warning") 
                {
                    $message = $o.Name +":"+ $o.Headline +"("+ $o.RunId +")"
                    $eventId = 21121
                    $eventType = [System.Diagnostics.EventLogEntryType]::Warning
                }
                if($o.Level -eq "Error") 
                {
                    $message = $o.Name +":"+ $o.Headline +"("+ $o.RunId +")"
                    $eventId = 21122
                    $eventType = [System.Diagnostics.EventLogEntryType]::Error
                }
                if($output.Level -eq "Exception") 
                {
                    $message = $o.Name +":"+ $o.Headline +"("+ $o.RunId +")"
                    $eventType = [System.Diagnostics.EventLogEntryType]::Error
                    $eventId = 21123
                }
                if(-not [string]::IsNullOrEmpty($message))
                {
                    try
                    {
                        Write-EventLog -Source "SharePoint Search Health Reports Dashboard" -LogName Application -EntryType $eventType -Message $message -EventId $eventId
                        Write-SRx INFO "Wrote test output to event log."
                    }
                    catch
                    {
                        Write-SRx ERROR "Failed writing test result to Event Log"
                    }
                }
            }
        }

        if($PassThrough)
        {
            $output
        }
    }
    END
    {
        Write-SRx DEBUG "END"
    }
}

#EndRegion '.\Public\New-SRxReport.ps1' 225
#Region '.\Public\New-SRxStoredCredential.ps1' 0
function New-SRxStoredCredential {
    <#
    .SYNOPSIS
        Create a new stored credential stored on local machine
 
    .DESCRIPTION
        This function will save a new stored credential to a .cred file into current user's folder on local machine.
         
    .NOTES
        =========================================
        Project : The Source Shell (SRx)
        -----------------------------------------
        File Name : New-SRxStoredCredential.psm1
        Author : Nikolay Mukhin
        Requires :
            PowerShell Version 5.1, The Source Shell (SRx), Microsoft.SharePoint.PowerShell
        ========================================================================================
         
    .INPUTS
     
    .OUTPUTS
        $Credential object
     
    .EXAMPLE
        New-SRxStoredCredential
    #>

    
        [CmdletBinding()]
        param (
        )    
        BEGIN 
        {
            Write-SRx DEBUG "BEGIN New-SRxStoredCredential"
        }
        PROCESS
        {
            Write-SRx DEBUG "PROCESS - Entered New-SRxStoredCredential process"
    
            $KeyPath = $home + "\" + $env:computername
            if (!(Test-Path $KeyPath)) {
                
                try {
                    New-Item -ItemType Directory -Path $KeyPath -ErrorAction STOP | Out-Null
                }
                catch {
                    Write-SRx Warning $("[shell] Can't create user's local store folder: " + $_.Exception.Message)
                    throw $_.Exception.Message
                }           
            }
            try {
                $Credential = Get-Credential -Message "Enter a user name and password"
                if( -not [string]::isNullOrEmpty($Credential.Username) ) {
                    $Credential.Password | ConvertFrom-SecureString | Out-File "$($KeyPath)\$($Credential.Username).cred" -Force        
                    # Return a PSCredential object (with no password) so the caller knows what credential username was entered for future recalls
                    New-Object -TypeName System.Management.Automation.PSCredential($Credential.Username,(new-object System.Security.SecureString))    
                }
            }
            catch {}
        }
        END
        {
            Write-SRx DEBUG "END"
        }
}
#EndRegion '.\Public\New-SRxStoredCredential.ps1' 65
#Region '.\Public\Read-SRxPipeline.ps1' 0
Function Read-SRxPipeline {
    $path = $global:SRxEnv.SchedulerPath
    $pipeline = $null
    try {
            
        if (!(Test-Path $path))
        {
            Write-SRx VERBOSE "Create the new folder $path"
            New-Item -itemType Directory -Path $path | Out-Null
            New-Item -itemType File -Path $path -Name "Pipeline.json" -Value '{}' | Out-Null
        }
       
        $schedulerFolderPath = $path #+ "\" + "Scheduler"
        <#
        if (!(Test-Path $schedulerFolderPath))
        {
            Write-SRx VERBOSE "Create the new folder $schedulerFolderPath"
            New-Item -itemType Directory -Path $schedulerFolderPath | Out-Null
            New-Item -itemType File -Path $schedulerFolderPath -Name "Pipeline.json" -Value '{}' | Out-Null
        }
        #>

        #Set-Content -Path $schedulerPipeline -Value '{}'
        $schedulerPipeline = $schedulerFolderPath + "\Pipeline.json" 
        $pipeline = Get-Content $($schedulerPipeline) -Raw -ErrorAction Stop
        if ([string]::isNullOrEmpty($pipeline)) {
            Write-SRx Warning $("~~~ The property cannot be persisted (the custom pipeline file is null or empty) - skipping...")
        } else {

        }
    }
    catch {
        $pipeline = $null
    }
    return $pipeline
}
#EndRegion '.\Public\Read-SRxPipeline.ps1' 36
#Region '.\Public\Read-SRxPipelineValue.ps1' 0
Function Read-SRxPipelineValue {
    $pipeline = Read-SRxPipeline   
    if ([string]::isNullOrEmpty($pipeline)) {
        #Write-SRx Warning $("empty pipeline - skipping...")
        return $null
    } 
    $tmp = ConvertFrom-Json $pipeline -ErrorAction SilentlyContinue    
    Set-SRxPipeline -init $($tmp.Pipeline)
    return $tmp.Pipeline                      
}
#EndRegion '.\Public\Read-SRxPipelineValue.ps1' 11
#Region '.\Public\Remove-SRxStringSpecialCharacter.ps1' 0
function Remove-SRxStringSpecialCharacter {
    <#
.SYNOPSIS
    This function will remove the special character from a string.
 
.DESCRIPTION
    This function will remove the special character from a string.
    I'm using Unicode Regular Expressions with the following categories
    \p{L} : any kind of letter from any language.
    \p{Nd} : a digit zero through nine in any script except ideographic
 
    http://www.regular-expressions.info/unicode.html
    http://unicode.org/reports/tr18/
 
.PARAMETER String
    Specifies the String on which the special character will be removed
 
.PARAMETER SpecialCharacterToKeep
    Specifies the special character to keep in the output
 
.EXAMPLE
    Remove-SRxStringSpecialCharacter -String "^&*@wow*(&(*&@"
    wow
 
.EXAMPLE
    Remove-SRxStringSpecialCharacter -String "wow#@!`~)(\|?/}{-_=+*"
 
    wow
.EXAMPLE
    Remove-SRxStringSpecialCharacter -String "wow#@!`~)(\|?/}{-_=+*" -SpecialCharacterToKeep "*","_","-"
    wow-_*
 
.NOTES
    Francois-Xavier Cat
    @lazywinadmin
    lazywinadmin.com
    github.com/lazywinadmin
#>

    [CmdletBinding()]
    param
    (
        [Parameter(ValueFromPipeline)]
        [ValidateNotNullOrEmpty()]
        [Alias('Text')]
        [System.String[]]$String,

        [Alias("Keep")]
        #[ValidateNotNullOrEmpty()]
        [String[]]$SpecialCharacterToKeep
    )
    PROCESS {
        try {
            IF ($PSBoundParameters["SpecialCharacterToKeep"]) {
                $Regex = "[^\p{L}\p{Nd}"
                Foreach ($Character in $SpecialCharacterToKeep) {
                    IF ($Character -eq "-") {
                        $Regex += "-"
                    }
                    else {
                        $Regex += [Regex]::Escape($Character)
                    }
                    #$Regex += "/$character"
                }

                $Regex += "]+"
            } #IF($PSBoundParameters["SpecialCharacterToKeep"])
            ELSE { $Regex = "[^\p{L}\p{Nd}]+" }

            FOREACH ($Str in $string) {
                Write-Verbose -Message "Original String: $Str"
                $Str -replace $regex, ""
            }
        }
        catch {
            $PSCmdlet.ThrowTerminatingError($_)
        }
    } #PROCESS
}
#EndRegion '.\Public\Remove-SRxStringSpecialCharacter.ps1' 79
#Region '.\Public\Resolve-SRxPropertiesArray_JSON.ps1' 0
function Resolve-SRxPropertiesArray_JSON {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)][pscustomobject]$Thing
    )
    process {
        if($null -eq $Thing) { 
            #$null
            @()
        }
        elseif($($Thing.Psobject.Properties.value.count) -eq 0) { @() }
        elseif( $Thing.Property) { @($($Thing.Property)) }
        else { $Thing }
    }
}
#EndRegion '.\Public\Resolve-SRxPropertiesArray_JSON.ps1' 16
#Region '.\Public\Resolve-SRxTermsArray_JSON.ps1' 0
function Resolve-SRxTermsArray_JSON {
    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline)][pscustomobject]$Thing
    )
    process {
        if($null -eq $Thing) { $null }
        elseif($($Thing.Psobject.Properties.value.count) -eq 0) { @() }
        elseif( $Thing.Term) { @($($Thing.Term)) }
        else { $Thing }
    }
}
#EndRegion '.\Public\Resolve-SRxTermsArray_JSON.ps1' 13
#Region '.\Public\Save-SRxConfig.ps1' 0
function Save-SRxConfig() {
    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$true)]$propertyName,
        [Parameter(Mandatory=$true)]$propertyValue
    )
    BEGIN {
    }
    PROCESS {

        $readlockRetries = 40
        $persistFailure = $false
        $config = $null
        #Write-SRx VERBOSE " > Saving 3"
        do {
            try {
                #Write-SRx VERBOSE " > Saving 4"
                Write-SRx VERBOSE $(" > Saving '" + $propertyName + "' to " + $global:SRxEnv.CustomConfig) #-ForegroundColor Yellow
                $config = Get-Content $($global:SRxEnv.CustomConfig) -Raw -Encoding UTF8 -ErrorAction Stop
                if ([string]::isNullOrEmpty($config)) {
                    $persistFailure = $true
                    Write-SRx Warning $("~~~ The property '" + $propertyName + "' cannot be persisted (the custom config file is null or empty) - skipping...")
                } else {                        
                    try {
                        $tmpSRxEnv = ConvertFrom-Json $config -ErrorAction SilentlyContinue
                        if ($null -ne $(Get-Command "Set-SRxCustomProperty" -ErrorAction SilentlyContinue)) {
                            $tmpSRxEnv | Set-SRxCustomProperty $propertyName $propertyValue
                        } else {
                            $tmpSRxEnv | Add-Member -Force -MemberType "NoteProperty" -Name $propertyName -Value $propertyValue
                        }
                        $jsonConfig = ConvertTo-Json $tmpSRxEnv -Depth 12
                    } catch { 
                        $persistFailure = $true
                        Write-Error $(" --> Caught exception converting `$global:SRxEnv.CustomConfig to object/JSON. Check for syntax errors - skipping: " + $propertyName)
                    } 
                }
            } catch [System.IO.IOException] {
                if ($readlockRetries -eq 40) { Write-SRx INFO " --> Unable to read $configPath - Retrying up to 10s" -NoNewline -ForegroundColor Yellow }
                elseif ($($readlockRetries % 4) -eq 0) {  Write-SRx INFO "." -NoNewline }
                Start-Sleep -Milliseconds 250
                $readlockRetries--
            } catch {
                #Write-SRx VERBOSE " > Saving 5"
                $persistFailure = $true
                Write-Error $(" --> Caught exception reading `$global:SRxEnv.CustomConfig when persisting a change - skipping: " + $propertyName)
            }
        } while (($readlockRetries -gt 0) -and ([string]::isNullOrEmpty($jsonConfig)) -and (-not $persistFailure))
        #Write-SRx VERBOSE " > Saving 6"
        if ([string]::isNullOrEmpty($jsonConfig) -and ($readlockRetries -lt 1)) {
            Write-SRx Warning $("~~~ Timed out waiting for a read/write lock on `$global:SRxEnv.CustomConfig - skipping: " + $propertyName)
        } elseif (-not [string]::isNullOrEmpty($jsonConfig)) {
            try {
                $jsonConfig | Set-Content $($global:SRxEnv.CustomConfig) -ErrorAction Stop
            } catch {
                Write-SRx Warning $("~~~ Unable to obtain a read/write lock on `$global:SRxEnv.CustomConfig - skipping: " + $propertyName)
            }
        }
    }
}
#EndRegion '.\Public\Save-SRxConfig.ps1' 60
#Region '.\Public\Save-SRxProfile.ps1' 0
Function Save-SRxProfile {
    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$true)]$TermGroupName
    )
    BEGIN {
    }
    PROCESS {
        try {
            Get-SRxConnection -siteUrl $global:SRxEnv.Tenancy.AdminUrl -SPO | Out-Null
            $connection = Get-SRxConnection -siteUrl $global:SRxEnv.Tenancy.AdminUrl    
    
            $count = 13
            $counter = 0
            $Activity = "Exporting TermGroup $TermGroupName ..."
            $status = ""

            #Write-Host ($termGroup | Format-List | Out-String)
            Write-SRx VERBOSE ""
            #Write-SRx WARNING " Exporting TermGroup $TermGroupName ..." #$($global:SRxEnv.Tenancy.TermGroupName)"
            $termGroup = Get-PnPTermGroup -Identity $TermGroupName -ErrorAction SilentlyContinue -Connection $connection
            #$TermGroupID
            $global:SRxEnv.Tenancy.TermGroupID = $termGroup.Id

            # Work folder
            $profilesFolderPath = $global:SRxEnv.CachePath + "\Profiles"
            if (!(Test-Path $profilesFolderPath))
            {
                Write-SRx VERBOSE " > Create the new folder $profilesFolderPath"
                New-Item -itemType Directory -Path $profilesFolderPath | Out-Null
            }
            if( $global:SRxEnv.Tenancy.TermGroupID -ne "") {
                #ensure folder for term group
                $sgroupName = (Remove-SRxStringSpecialCharacter -String $global:SRxEnv.Tenancy.TermGroupName)
                $profileFolderPath = $profilesFolderPath + "\" + $sgroupName
                if (!(Test-Path $profileFolderPath))
                {
                    Write-SRx VERBOSE " > Create the new folder $profileFolderPath"
                    New-Item -itemType Directory -Path $profileFolderPath | Out-Null
                }

                #saving copy of custom.config.json
                #$customConfigPath = Join-Path $global:SRxEnv.Paths.Mgmt "custom.config.json"
                $customConfigPath = Join-Path $global:SRxEnv.CachePath "custom.config.json"
                $backupConfigPath = $profileFolderPath + "\custom.config.json"

                $status = "Saving custom.config.json to $backupConfigPath ..."
                Write-Progress -Activity $Activity -Status $status -PercentComplete $(($counter++ / $count) * 100)
                Write-SRx VERBOSE " > $status"

                Copy-Item -Path $customConfigPath -Destination $backupConfigPath

                #export taxonomy group to xml file
                $backupTermGroupPath = $profileFolderPath + "\termGroup.xml"

                $status = "Exporting Taxonomy Group to $backupTermGroupPath ..."
                Write-Progress -Activity $Activity -Status $status -PercentComplete $(($counter++ / $count) * 100)
                Write-SRx VERBOSE " > $status"

                if ((Test-Path $backupTermGroupPath))
                {
                    Remove-Item $backupTermGroupPath -Confirm:$false
                }
                #Write-SRx VERBOSE " > termGroupID = $($termGroup.Id)"

                $termGroup | Export-PnPTermGroupToXml -Out $backupTermGroupPath -ErrorAction:Stop -FullTemplate -Force -Connection $connection 

                if ((Test-Path $backupTermGroupPath))
                {

                    #filtering group xml to extract only provisioning schema termset into separate xml file
                    $backupTermSetPath = $profileFolderPath + "\termSet.xml"

                    $status = "Saving provisioning schema to $backupTermSetPath..."
                    Write-Progress -Activity $Activity -Status $status -PercentComplete $(($counter++ / $count) * 100)
                    Write-SRx VERBOSE " > $status"

                    $siteXML = [xml](Get-Content -Path $backupTermGroupPath -Raw -Encoding UTF8)
                    $termSets = $siteXML.Provisioning.Templates.ProvisioningTemplate.TermGroups.TermGroup.TermSets

                    if( $null -ne $termSets) {
                        ($termSets.ChildNodes | Where-Object {
                            ($($global:SRxEnv.Tenancy.TermSetID) -ne $_.ID)
                        }) | ForEach-Object { 
                            $termSets.RemoveChild($_) | Out-Null 
                        }
                    }        
                    $siteXML.Save($backupTermSetPath)

                    $status = "Saving provisioning schema to json..."
                    Write-Progress -Activity $Activity -Status $status -PercentComplete $(($counter++ / $count) * 100)
                    Write-SRx VERBOSE " > $status"

                    [xml[]] (Get-Content -Raw -Encoding UTF8 -Path $backupTermSetPath) | Convert-SRxFromXML | ConvertTo-Json -Depth 50 | Format-SRxJson -Indentation 1 | Set-Content $($profileFolderPath + "\ProvisioningDatabase.json")           

                    $status = "Saving provisioning schema to minified json..."
                    Write-Progress -Activity $Activity -Status $status -PercentComplete $(($counter++ / $count) * 100)
                    Write-SRx VERBOSE " > $status"

                    [xml[]] (Get-Content -Raw -Encoding UTF8 -Path $backupTermSetPath) | Convert-SRxFromXML | ConvertTo-Json -Depth 50 | Format-SRxJson -Minify | Set-Content $($profileFolderPath + "\ProvisioningDatabase.min.json") 

                    #filtering group xml to extract everything,but provisioning schema termset into separate xml file
                    $backupTermSetPath = $profileFolderPath + "\termSetFiltered.xml"

                    $status = "Saving filtered group to $backupTermSetPath..."
                    Write-Progress -Activity $Activity -Status $status -PercentComplete $(($counter++ / $count) * 100)
                    Write-SRx VERBOSE " > $status"

                    $siteXML = [xml](Get-Content -Path $backupTermGroupPath -Raw -Encoding UTF8)
                    $termSets = $siteXML.Provisioning.Templates.ProvisioningTemplate.TermGroups.TermGroup.TermSets

                    if( $null -ne $termSets) {
                        ($termSets.ChildNodes | Where-Object {
                            ($($global:SRxEnv.Tenancy.TermSetID) -eq $_.ID)
                        }) | ForEach-Object { 
                            $termSets.RemoveChild($_) | Out-Null 
                        }
                    }        
                    $siteXML.Save($backupTermSetPath)
                    
                    $status = "Saving filtered group to json..."
                    Write-Progress -Activity $Activity -Status $status -PercentComplete $(($counter++ / $count) * 100)
                    Write-SRx VERBOSE " > $status"

                    [xml[]] (Get-Content -Raw -Encoding UTF8 -Path $backupTermSetPath) | Convert-SRxFromXML | ConvertTo-Json -Depth 50 | Format-SRxJson -Indentation 1 | Set-Content $($profileFolderPath + "\the_source_term_group.json")           

                    $status = "Saving filtered group to minified json..."
                    Write-Progress -Activity $Activity -Status $status -PercentComplete $(($counter++ / $count) * 100)
                    Write-SRx VERBOSE " > $status"

                    [xml[]] (Get-Content -Raw -Encoding UTF8 -Path $backupTermSetPath) | Convert-SRxFromXML | ConvertTo-Json -Depth 50 | Format-SRxJson -Minify | Set-Content $($profileFolderPath + "\the_source_term_group.min.json") 

                    $status = "Activating provisioning schema..."
                    Write-Progress -Activity $Activity -Status $status -PercentComplete $(($counter++ / $count) * 100)
                    Write-SRx VERBOSE ""
                    Write-SRx VERBOSE " > $status"

                    Connect-SRxProvisioningDatabase_JSON

                    $isStakeholder = Test-SRxIsStakeHolder

                    $global:SRxEnv.SetCustomProperty("isStakeHolder",    $isStakeholder)
                    $status = "isStakeHolder = $isStakeholder"
                    Write-Progress -Activity $Activity -Status $status -PercentComplete $(($counter++ / $count) * 100)
                    Write-SRx VERBOSE " > $status"
                    $global:SRxEnv.Tenancy.IsStakeholder = $isStakeholder
                    $status = "Saving Tenancy..."
                    Write-Progress -Activity $Activity -Status $status -PercentComplete $(($counter++ / $count) * 100)
                    Write-SRx VERBOSE " > $status"
                    #$global:SRxEnv.PersistCustomProperty("Tenancy", $global:SRxEnv.Tenancy)
                    Save-SRxConfig -propertyName "Tenancy" -propertyValue $global:SRxEnv.Tenancy       
                    $status = "Completed."
                    Write-Progress -Activity $Activity -Status $status -PercentComplete $(($counter++ / $count) * 100)
                    Write-SRx VERBOSE " > $status"

                }
                else {
                    Write-SRx ERROR " > Can't export $TermGroupName ID = $($termGroup.Id)"
                    return "exception"
                }

                if($Activity) { Write-Progress -Activity $Activity -Status "Ready" -Completed }
            }
        } 
        catch {
            if($Activity) { Write-Progress -Activity $Activity -Status "Ready" -Completed }
            Write-SRx ERROR ($_.Exception.Message) 
            Write-SRx VERBOSE ($_.Exception) 
            Write-SRx INFO ("Hit enter to continue...") 
            #Read-Host
            return "exception"  
        }
        return 0 #success
    }
    END {
        if($Activity) { Write-Progress -Activity $Activity -Status "Ready" -Completed }
        #Disconnect-PnPOnline -Connection $connection
    }
}
#EndRegion '.\Public\Save-SRxProfile.ps1' 180
#Region '.\Public\Set-SRxCommand.ps1' 0
Function Set-SRxCommand {
    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$false)]$command
    )
    BEGIN {
    }
    PROCESS {
        try {
            if( $command ) {
                $global:SRxEnv.SetCustomProperty("Command", $true)

                $hashtable = @{}
                $hashtable["ts_Title"]       = $command.ts_Title 
                $hashtable["ts_ControlFile"] = $command.ts_ControlFile 
                $hashtable["ts_IsAdmin"]     = [System.Convert]::ToBoolean($command.ts_IsAdmin)
                $hashtable["ts_NewSite"]     = [System.Convert]::ToBoolean($command.ts_NewSite)
                $hashtable["ts_Rebuild"]     = [System.Convert]::ToBoolean($command.ts_Rebuild)
                $hashtable["ts_Environment"] = $command.ts_Environment  
                $sopobject = [pscustomobject]$hashtable
                $global:SRxEnv.SetCustomProperty("SOP", $sopobject)
    
                $pipeline = @()
                foreach( $site in @($command.pipeline)) { 

                    if( -not [string]::IsNullOrEmpty($site.siteDesign) ) {  
                         
                        $design_name = $site.siteDesign
                        $master_ = Get-SRxTermByCustomProperty_JSON -Key "ts_Environment" -Value "Master"          
                        $master_design_ = Get-SRxTermByCustomProperty_JSON -termHost $master_ -Key "ts_Design" -Value $design_name
                        $master_site_ = Get-SRxTermByCustomProperty_JSON -termHost $master_design_ -Key "ts_Site" -Value $design_name
                        $masterUrl = Get-SRxCustomPropertyValueByKey_JSON -termHost $master_site_ -Key "ts_URL" -ToUrl   
        
                        $site.masterUrl               = $masterUrl
                        $site.HubURL                  = Convert-SRxURL_JSON -URL $site.HubURL -environment $site.environment
                    }
                    if( -not [string]::IsNullOrEmpty($site.siteTitle) ) {                       
                        $site.siteName                = Remove-SRxStringSpecialCharacter -String $site.siteTitle
                    }
                    
                    $pipeline += $site
                }
               
                $global:SRxEnv.SetCustomProperty("Pipeline", $pipeline)   
                if($null -ne $global:SRxEnv.SOP) {
                    $global:SRxEnv.SetCustomProperty("PnPEnvironment", $global:SRxEnv.SOP.ts_Environment)
                    $global:SRxEnv.SetCustomProperty("PnPRebuild", [boolean]$global:SRxEnv.SOP.ts_Rebuild)
                    $global:SRxEnv.SetCustomProperty("PnPNewSite", [boolean]$global:SRxEnv.SOP.ts_NewSite)
                }
            }
        } 
        catch {
            Write-SRx ERROR " > Set-SRxCommand error:"
            Write-SRx ERROR ($_.Exception.Message) 
            Write-SRx VERBOSE ($_.Exception) 
            return "exception"  
        }
        return 0 #success
    }
    END {
    }
}
#EndRegion '.\Public\Set-SRxCommand.ps1' 63
#Region '.\Public\Set-SRxCustomProperty.ps1' 0
function Set-SRxCustomProperty {
    <#
    .SYNOPSIS
        Internal helper for adding or refreshing a property to an existing object
         
    .DESCRIPTION
     
    .EXAMPLE
        $targetObject | Set-SRxCustomProperty "_CustomPropertyName" $propertyValue
     
    #>

    [CmdletBinding()]
    param (
        [parameter(Mandatory=$true,ValueFromPipeline=$true)]$object, 
        [parameter(Mandatory=$true,ValueFromPipeline=$false,Position=0)][String]$propertyName, 
        [parameter(Mandatory=$false,ValueFromPipeline=$false,Position=1)]$propertyValue
    )
        if ($propertyName.Contains(".")) {
            Write-SRx VERBOSE ("[Set-SRxCustomProperty] `$propertyName contains '.' (" + $propertyName + ")")
            if ($propertyName.Contains("..") -or $propertyName.EndsWith(".")) {
                Write-SRx WARNING ("[Set-SRxCustomProperty] Skipping Invalid Property Name: " + $propertyName)
                return
            } else {
                $firstPos = $propertyName.indexOf(".")
                $parent = $propertyName.Substring(0, $firstPos)
                $child = $propertyName.Substring($firstPos + 1)
                Write-SRx DEBUG (" --> [Set-SRxCustomProperty] `$propertyName contains tokens ( p: " + $parent + " , c: " + $child +" )")
                
                if (($null -ne $object.$parent) -and (($object.$parent -is [PSObject]) -or ($object.$parent -is [Hashtable]))) {
                    Write-SRx DEBUG (" --> [Set-SRxCustomProperty] Recursing... ( p: " + $parent + " , c: " + $child +" )")
                    $object.$parent | Set-SRxCustomProperty $child $propertyValue
                }
            }
        } else {
            if ($object -is [Hashtable]) {
                $object.$propertyName = $propertyValue
            } elseif ($object -is [PSObject]) {
                if ( $null -ne $( $object | Get-Member -Name $propertyName) ) { 
                    #if the property already exists, just set it
                    $object.$propertyName = $propertyValue
                } else {
                    #add the new member property and set it
                    $object | Add-Member -Force -MemberType "NoteProperty" -Name $propertyName -Value $propertyValue 
                }
            }
        }
    }
    
#EndRegion '.\Public\Set-SRxCustomProperty.ps1' 49
#Region '.\Public\Set-SRxPipeline.ps1' 0
function Set-SRxPipeline {
    PARAM (
        [switch]$Print,
        [Parameter(Mandatory=$false)]$init, #[hashtable[]]$Init,
        [Parameter(Mandatory=$false)]$update, #[hashtable[]]$Update
        [Parameter(Mandatory=$false)]$ID
    )
    BEGIN {

    }
    PROCESS {
        if($Print) {
            Write-SRx WARNING "PIPELINE=$($global:SRxEnv.Pipeline)"
            $pipelineObject = $global:SRxEnv.Pipeline[0]          
            Write-SRx WARNING "PIPELINE OBJECT=$($pipelineObject)"
            $hashtable = @{}
            foreach( $property in $pipelineObject.psobject.properties.name ) { $hashtable[$property] = $pipelineObject.$property }
            Write-SRx WARNING "HASHTABLE=$($hashtable | Out-String)"
            return
        }
        if($null -ne $init) {
            $global:SRxEnv.SetCustomProperty("Pipeline", $init)    
        }
        elseif( $null -ne $update) {
            if($null -ne $global:SRxEnv.Pipeline) {
                $pipe = [System.Collections.ArrayList]@() 
                #$id = 0
                foreach( $pipelineObject in $global:SRxEnv.Pipeline ) {
                    #Write-SRx VERBOSE "Update pipeline id = $($pipelineObject.id)"
                    $hashtable = @{}
                    foreach( $property in $pipelineObject.psobject.properties.name ) { 
                        $hashtable[$property] = $pipelineObject.$property 
                        #Write-SRx VERBOSE "property = $property Value = $($hashtable[$property])"
                    }

                    #Write-SRx VERBOSE "Pipeline object id = $( $hashtable['id'])"
                    #Write-SRx VERBOSE "Input object id = $( $update['id'])"
                    if( $ID) {
                        if($hashtable[$ID] -eq $update[$ID]) {
                            foreach ($key in $update.Keys) { 
                                #Write-SRx VERBOSE "Replacing values..."
                                if($key -ne $ID) {
                                    #Write-SRx VERBOSE "Replace property = $key Old Value = $($hashtable[$key]) New Value = $($update[$key])"
                                    $hashtable[$key]  = $update[$key]
                                } 
                            } 
                        }
                    }
                    else {
                        if($hashtable['id'] -eq $update['id']) {
                            foreach ($key in $update.Keys) { 
                                #Write-SRx VERBOSE "Replacing values..."
                                if($key -ne 'id') {
                                    #Write-SRx VERBOSE "Replace property = $key Old Value = $($hashtable[$key]) New Value = $($update[$key])"
                                    $hashtable[$key]  = $update[$key]
                                } 
                            } 
                        }
                    }

                    #$hashtable['id'] = $id++
                    $site = [pscustomobject]$hashtable
                    $pipe.Add($site) | Out-Null        
                }         
                #Write-SRx ERROR "Pipe.Count = $($pipe.Count)"
                $pipeline = $pipe.ToArray()
                $global:SRxEnv.SetCustomProperty("Pipeline", $pipeline)    
                }
            else {
                return 3 #error
            }
        }
        else {
            $pipe = [System.Collections.ArrayList]@()
            $site = [PSCustomObject]@{
                id = 0
                environment = $null
                siteDesign = $null
                siteOwner = $null
                termID = $null
                siteTitle = $null
                siteName = $null
                siteURL = $null
                HubURL = $null
            } 
            $pipe.Add($site) | Out-Null
            $pipeline = $pipe.ToArray()
            $global:SRxEnv.SetCustomProperty("Pipeline", $pipeline)    
        }
    }
    END {

    }
}
#EndRegion '.\Public\Set-SRxPipeline.ps1' 95
#Region '.\Public\Set-SRxTermDesign.ps1' 0
Function Set-SRxTermDesign {
    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$true)][string]$siteDesign,
        [Parameter(Mandatory=$false)][string]$title,
        [Parameter(Mandatory=$false)][string]$siteEnvironment,            
        [Parameter(Mandatory=$true)]$connection
    )
    PROCESS {
        try {
            $siteTitle = $siteDesign
            if( $title ) { $siteTitle = $title }
            $termDesign = Get-SRxHostingTerm -Design -siteDesign $siteDesign -siteEnvironment $siteEnvironment -Connection $connection

            if( -not $termDesign ) {
                # environment
                $environment_ = Get-SRxTermByCustomProperty_JSON -Key "ts_Environment" -Value $siteEnvironment
                $termEnvironmentID = $environment_.ID
#Write-SRx WARNING "termEnvironmentID = $termEnvironmentID"
                $termEnvironment = Get-PnPTerm -Identity $([GUID]$termEnvironmentID) -ErrorAction SilentlyContinue -IncludeChildTerms -Connection $connection


                # design
                $termDesign = Get-SRxTermsWithCustomProperty -termsHost $termEnvironment  -customPropertyKey "ts_Design" -customPropertyValue $siteDesign -connection $connection
                if(-not $termDesign) {
                    Write-SRx VERBOSE " > Creating new $siteTitle term folder ..."
                    # designs
                    $termDesigns = Get-SRxTermsWithCustomProperty -termsHost $termEnvironment  -customPropertyKey "ts_Designs" -customPropertyValue $siteEnvironment -connection $connection
                    if(-not $termDesigns) {
                        $customProperties = @{}
                        $customProperties.add("ts_Designs", $siteEnvironment)
                        $termDesigns = Add-PnPTermToTerm -ParentTerm $termEnvironment.Id -Name "Designs" -CustomProperties $customProperties -Connection $connection  
                        Start-Sleep -Seconds 3  
                    }
                    $customProperties = @{}
                    $customProperties.add("ts_Design", $siteDesign)
                    $termDesign = Add-PnPTermToTerm -ParentTerm $termDesigns.Id -Name $siteTitle -CustomProperties $customProperties -Connection $connection 
                    Start-Sleep -Seconds 3   
                }
            }
            return $termDesign

        }
        catch {
            Write-SRx ERROR ($_.Exception.Message) 
            Write-SRx VERBOSE ($_.Exception) 
            return $null  
        }
    }    
}
#EndRegion '.\Public\Set-SRxTermDesign.ps1' 51
#Region '.\Public\Show-SRxNotImplementedWarning.ps1' 0
function Show-SRxNotImplementedWarning {
    Write-Host 
    Write-Host $("-" * 51) -ForegroundColor Yellow -BackgroundColor Yellow
    Write-Host $(" OPERATION IS NOT IMPLEMENTED ") -ForegroundColor Black -BackgroundColor Yellow
    Write-Host $("-" * 51) -ForegroundColor Yellow -BackgroundColor Yellow
    Write-Host 
    return 0
}
#EndRegion '.\Public\Show-SRxNotImplementedWarning.ps1' 9
#Region '.\Public\Show-SRxProductionWarning.ps1' 0
function Show-SRxProductionWarning {
    if(($global:SRxEnv.PnPEnvironment -eq "Production") -or ($global:SRxEnv.PnPEnvironment -eq "Release") -or ($global:SRxEnv.PnPEnvironment -eq "Master")) {
        Write-Host 
        Write-Host $("-" * 51) -ForegroundColor DarkRed -BackgroundColor DarkRed
        if($global:SRxEnv.PnPEnvironment -eq "Master") {
            Write-Host $(" OPERATION ON MASTER DESIGN ") -ForegroundColor White -BackgroundColor DarkRed
        }
        else {
            Write-Host $(" OPERATION ON PRODUCTION LANDSCAPE ") -ForegroundColor White -BackgroundColor DarkRed
        }
        Write-Host $("-" * 51) -ForegroundColor DarkRed -BackgroundColor DarkRed
    }
}
#EndRegion '.\Public\Show-SRxProductionWarning.ps1' 14
#Region '.\Public\Show-SRxShell.ps1' 0
Function Show-SRxShell {
    PARAM (
        [Parameter(Mandatory=$false)][switch]$Clear        
    )
    BEGIN {
        if( -not $(Get-Alias -Name ecosystem -ErrorAction SilentlyContinue)) {
            New-Alias -Name ecosystem -Value _EcoSystem -Force -Scope Global -Option Constant
        }
        if( -not $(Get-Alias -Name log -ErrorAction SilentlyContinue)) {
            New-Alias -Name log -Value _Log -Force -Scope Global -Option Constant
        }
        if( -not $(Get-Alias -Name cache -ErrorAction SilentlyContinue)) {
            New-Alias -Name cache -Value _Cache -Force -Scope Global -Option Constant
        }
        if( -not $(Get-Alias -Name task -ErrorAction SilentlyContinue)) {
            New-Alias -Name task -Value _Task -Force -Scope Global -Option Constant
        }
        if( -not $(Get-Alias -Name shell -ErrorAction SilentlyContinue)) {
            New-Alias -Name shell -Value _Shell -Force -Scope Global -Option Constant
        }
        if( -not $(Get-Alias -Name menu -ErrorAction SilentlyContinue)) {
            New-Alias -Name menu -Value Start-SOP -Force -Scope Global -Option Constant
        }
        if( -not $(Get-Alias -Name systray -ErrorAction SilentlyContinue)) {
            New-Alias -Name systray -Value Start-SysTray -Force -Scope Global -Option Constant
        }
        #if( -not $(Get-Alias -Name gui -ErrorAction SilentlyContinue)) {
        # New-Alias -Name gui -Value _Gui -Force -Scope Global -Option Constant
        #}
        if( -not $(Get-Alias -Name docs -ErrorAction SilentlyContinue)) {
            New-Alias -Name docs -Value _Docs -Force -Scope Global -Option Constant
        }
        if( -not $(Get-Alias -Name pipeline -ErrorAction SilentlyContinue)) {
            New-Alias -Name pipeline -Value _Pipeline -Force -Scope Global -Option Constant
        }
    }
    PROCESS {
        try {
                    
            Write-Host 
            Write-Host $("-" * 53) -ForegroundColor Black -BackgroundColor DarkCyan
            Write-Host $(" Provisioning Shell ") -ForegroundColor White -BackgroundColor DarkCyan
            Write-Host $(" Commands: menu | pipeline | log | cache | ecosystem ") -ForegroundColor Black -BackgroundColor DarkCyan
            Write-Host $(" Cmdlets: Start-Rule, Start-Ruleset, Get-Master ") -ForegroundColor Black -BackgroundColor DarkCyan
            Write-Host $("-" * 53) -ForegroundColor Black -BackgroundColor DarkCyan

        }
        catch { Write-Host "error"
        }
    }
}
#EndRegion '.\Public\Show-SRxShell.ps1' 52
#Region '.\Public\Show-SRxSOPTitle.ps1' 0
function Show-SRxSOPTitle {
    PARAM (
        [Parameter(Mandatory=$false)][switch]$Clear        
    )

    PROCESS {
        if( $global:SRxEnv.SOP) {
            # Clear console
            try {
                if( $Clear -eq $true) { Clear-Host }

                $title = $global:SRxEnv.SOP.ts_Title
                $even = ""
                if( $($title.Length) % 2 -eq 0) { $even = " " }

                $width = 51
                $side = ($width - $title.Length) / 2
                Write-Host 
                Write-Host $("-" * $width) -ForegroundColor Black -BackgroundColor DarkCyan
                Write-Host $(" " * $side) -ForegroundColor White -BackgroundColor DarkCyan -NoNewline
                Write-Host $($title + $even) -ForegroundColor White -BackgroundColor DarkCyan -NoNewline
                Write-Host $(" " * $side) -ForegroundColor White -BackgroundColor DarkCyan -NoNewline
                Write-Host
                Write-Host $("-" * $width) -ForegroundColor Black -BackgroundColor DarkCyan    
                Write-Host 
            }
            catch {}
        }
    }
}
#EndRegion '.\Public\Show-SRxSOPTitle.ps1' 31
#Region '.\Public\Show-SRxWindow.ps1' 0
function Show-SRxWindow {
    $null = [Win32Functions.Win32Windows]::ShowWindowAsync((Get-Process -PID $pid ).MainWindowHandle, 10)
}
#EndRegion '.\Public\Show-SRxWindow.ps1' 4
#Region '.\Public\Start-Rule.ps1' 0
function Start-Rule {
<#
.SYNOPSIS
    Invokes a test(s) and handles where the output event(s) get written
     
.DESCRIPTION
    This cmdlet wraps the invocation chain of Test-SRx followed by:
        -> Export-SRxToSearchDashboard | Export-SRxToAlertsList
        -> and/or Write-SRxConsole
     
.NOTES
    =========================================
    Project : Search Health Reports (SRx)
    -----------------------------------------
    File Name : Start-Rule.psm1
    Author : Eric Dixon
    Requires :
        PowerShell Version 5.1, Search Health Reports (SRx), Microsoft.SharePoint.PowerShell
     
    ========================================================================================
    This Sample Code is provided for the purpose of illustration only and is not intended to
    be used in a production environment.
     
        THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY
        OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
        WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
 
    We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to
    reproduce and distribute the object code form of the Sample Code, provided that You agree:
        (i) to not use Our name, logo, or trademarks to market Your software product in
            which the Sample Code is embedded;
        (ii) to include a valid copyright notice on Your software product in which the
             Sample Code is embedded;
        and
        (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against
              any claims or lawsuits, including attorneys' fees, that arise or result from
              the use or distribution of the Sample Code.
 
    ========================================================================================
     
.INPUTS
    Control file (e.g. TestControl.csv)
 
.OUTPUTS
    [ $SRxEvent(s) ]
 
.EXAMPLE
 
#>


    [CmdletBinding()]
    param ( 
            [alias("All")][switch]$RunAllTests,
            [alias("OutNull")][switch]$NoWriteToConsole,
            [switch]$PassThrough,
            [alias("EventLog")][switch]$WriteErrorsToEventLog,
            [switch]$Details,
            [Parameter(Mandatory=$false)][string]$environment,
            [Parameter(Mandatory=$false)][boolean]$isnewsite,
            [Parameter(Mandatory=$false)][boolean]$isrebuild,
            [Parameter(Mandatory=$false)][boolean]$isadmin
    )

    DynamicParam 
    {
        # Set the dynamic parameters' name
        $ParameterControlFile = 'ControlFile'
        $ParameterTest = 'Test'
        
        # Create the dictionary
        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary

        # Create the collection of attributes
        $AttributeCollectionControlFile = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $AttributeCollectionTest = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        
        # Create and set the parameters' attributes
        $ParameterAttributeControlFile = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttributeControlFile.Mandatory = $false
        $ParameterAttributeControlFile.Position = 1

        $ParameterAttributeTest = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttributeTest.Mandatory = $false
        $ParameterAttributeTest.Position = 1

        # Add the attributes to the attributes collection
        $AttributeCollectionControlFile.Add($ParameterAttributeControlFile)

        $AttributeCollectionTest.Add($ParameterAttributeTest)

        # Generate and set the ValidateSet
        $rulesControlFolder =$global:SRxEnv.Paths.RuleSets # Join-Path $global:SRxEnv.Paths.Config "TestControls"
        #$arrSet = Get-ChildItem -Path $rulesControlFolder -File "*.csv" -Recurse | Select-Object -ExpandProperty Name
           $arrSet = Get-ChildItem -Path $rulesControlFolder -File "*.csv" -Recurse `
            | Select-Object -ExpandProperty Name `
            | % {if($_.EndsWith(".csv","CurrentCultureIgnoreCase")){$_.Substring(0,$_.Length-4)}} 

        $ValidateSetAttributeControlFile = New-Object System.Management.Automation.ValidateSetAttribute($arrSet)

        $rulesDefinitions = $global:SRxEnv.Paths.Rules
        #$arrSet2 = Get-ChildItem -Path $rulesDefinitions -File "Rule-*.ps1" -Recurse | Select-Object -ExpandProperty Name
        $arrSet2 = Get-ChildItem -Path $rulesDefinitions -File "Rule-*.ps1" -Recurse `
            | Select-Object -ExpandProperty Name `
            | % {if($_.StartsWith("Rule-","CurrentCultureIgnoreCase")){$_.Substring(5)}} `
            | % {if($_.EndsWith(".ps1","CurrentCultureIgnoreCase")){$_.Substring(0,$_.Length-4)}} 
        $ValidateSetAttributeTest = New-Object System.Management.Automation.ValidateSetAttribute($arrSet2)

        # Add the ValidateSet to the attributes collection
        $AttributeCollectionControlFile.Add($ValidateSetAttributeControlFile)

        $AttributeCollectionTest.Add($ValidateSetAttributeTest)

        # Create and return the dynamic parameter
        $RuntimeParameterControlFile = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterControlFile, [string], $AttributeCollectionControlFile)
        $RuntimeParameterDictionary.Add($ParameterControlFile, $RuntimeParameterControlFile)
        $RuntimeParameterTest = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterTest, [string], $AttributeCollectionTest)
        $RuntimeParameterDictionary.Add($ParameterTest, $RuntimeParameterTest)
        return $RuntimeParameterDictionary
    }

    BEGIN 
    {
        Write-SRx DEBUG "BEGIN Start-Rule"
        if($environment) { $global:SRxEnv.SetCustomProperty("PnPEnvironment", $environment) }
        if($isnewsite) { $global:SRxEnv.SetCustomProperty("PnPNewSite", $([boolean]$isnewsite)) }
        if($isrebuild) { $global:SRxEnv.SetCustomProperty("PnPRebuild", $([boolean]$isrebuild)) }

        $ControlFile = $PsBoundParameters[$ParameterControlFile]
        $Test = $PsBoundParameters[$ParameterTest]
        if($RunAllTests)
        {
            Write-SRx VERBOSE "Running SRx Report -RunAllTests"
        }
        elseif($Test)
        {
            Write-SRx VERBOSE "Running SRx Report with test: $Test"
        }
        elseif($ControlFile)
        {
            Write-SRx VERBOSE "Running SRx Report with control file: $ControlFile"
        }
    }
    PROCESS
    {
        Write-SRx DEBUG "PROCESS - Entered Start-Rule process"

        if($RunAllTests)
        {
            $output = Test-SRx -RunAllTests 
        }
        elseif($Test)
        {
            if($PSCmdlet.MyInvocation.BoundParameters["Debug"].IsPresent) {
                $output = Test-SRx -Name $Test -Debug
            } else {
                $output = Test-SRx -Name $Test 
            }
        }
        elseif($ControlFile)
        {
            $output = Test-SRx -ControlFile $ControlFile 
        }
        else
        {
            Write-SRx ERROR "You must supply command line parameter -Test <test name>, -ControlFile <control file name>, or -RunAllTests"
            return
        }

        if($global:SRxEnv.Dashboard.Initialized)
        {
            $output | Export-SRxToSearchDashboard | Export-SRxToAlertsList | Out-Null
        }

        if($global:SRxEnv.OMS.Initialized)
        {
            $output | Export-SRxToOMS | Out-Null
        }

        if(-not $NoWriteToConsole -and $output -ne $null)
        {
            $output | Write-SRxConsole -Details:$Details
        }

        if($WriteErrorsToEventLog)
        {
            foreach($o in $output)
            {
                $message = $null
                if($o.Level -eq "Warning") 
                {
                    $message = $o.Name +":"+ $o.Headline +"("+ $o.RunId +")"
                    $eventId = 21121
                    $eventType = [System.Diagnostics.EventLogEntryType]::Warning
                }
                if($o.Level -eq "Error") 
                {
                    $message = $o.Name +":"+ $o.Headline +"("+ $o.RunId +")"
                    $eventId = 21122
                    $eventType = [System.Diagnostics.EventLogEntryType]::Error
                }
                if($output.Level -eq "Exception") 
                {
                    $message = $o.Name +":"+ $o.Headline +"("+ $o.RunId +")"
                    $eventType = [System.Diagnostics.EventLogEntryType]::Error
                    $eventId = 21123
                }
                if(-not [string]::IsNullOrEmpty($message))
                {
                    try
                    {
                        Write-EventLog -Source "SharePoint Search Health Reports Dashboard" -LogName Application -EntryType $eventType -Message $message -EventId $eventId
                        Write-SRx INFO "Wrote test output to event log."
                    }
                    catch
                    {
                        Write-SRx ERROR "Failed writing test result to Event Log"
                    }
                }
            }
        }

        if($PassThrough)
        {
            $output
        }
    }
    END
    {
        Write-SRx DEBUG "END"
    }
}

#EndRegion '.\Public\Start-Rule.ps1' 233
#Region '.\Public\Start-Ruleset.ps1' 0
function Start-Ruleset { 
<#
.SYNOPSIS
    Evaluates test definition file and generates a corresponding SRxEvent
     
.DESCRIPTION
    Can specify a control file as a parameter, which defines a specific set
    of tests to perform where each test specified in the control generates
    an SRxEvent
     
.NOTES
    =========================================
    Project : Search Health Reports (SRx)
    -----------------------------------------
    File Name : Start-Ruleset.psm1
    Author : Eric Dixon
 
    Requires :
        PowerShell Version 5.1, Search Health Reports (SRx), Microsoft.SharePoint.PowerShell
     
    ========================================================================================
    This Sample Code is provided for the purpose of illustration only and is not intended to
    be used in a production environment.
     
        THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY
        OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
        WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
 
    We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to
    reproduce and distribute the object code form of the Sample Code, provided that You agree:
        (i) to not use Our name, logo, or trademarks to market Your software product in
            which the Sample Code is embedded;
        (ii) to include a valid copyright notice on Your software product in which the
             Sample Code is embedded;
        and
        (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against
              any claims or lawsuits, including attorneys' fees, that arise or result from
              the use or distribution of the Sample Code.
 
    ========================================================================================
     
.INPUTS
    Control file (e.g. TestControl.csv)
 
.OUTPUTS
    [ $SRxEvent(s) ]
 
.EXAMPLE
    Start-Ruleset -ControlFile TestControl.csv
 
#>

    [CmdletBinding()]
    param ( 
            [Parameter(ParameterSetName='DefaultSet')]
            [alias("All")][switch]$RunAllTests,
            [Parameter(ParameterSetName='DefaultSet')]
            $Params=$null,
            [Parameter(ParameterSetName='DefaultSet')]
            [switch]$WhatIf=$false
    )

    DynamicParam 
    {
        # Set the dynamic parameters' name
        $ParameterControlFile = 'ControlFile'
        $ParameterTest = 'Name'
        
        # Generate the parameter values (e.g. control and test file names) for validation
        $__rulesControlFolder = $global:SRxEnv.Paths.RuleSets #Join-Path $global:SRxEnv.Paths.Config "TestControls"
        $__ruleControlFiles = Get-ChildItem -Path $__rulesControlFolder -File "*.csv" -Recurse 

        $__ruleDefinitionsFolder = $global:SRxEnv.Paths.Rules
        $__ruleDefinitionFiles = Get-ChildItem -Path $__ruleDefinitionsFolder -File "Rule-*.ps1" -Recurse

        # Create the collection of attributes
        $AttributeCollectionControlFile = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $AttributeCollectionTest = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        
        # Create and set the parameters' attributes
        $ParameterAttributeControlFile = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttributeControlFile.Mandatory = $false
        $ParameterAttributeControlFile.Position = 1
        $ParameterAttributeControlFile.ParameterSetName = 'ControlFileSet'

        $ParameterAttributeTest = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttributeTest.Mandatory = $false
        $ParameterAttributeTest.Position = 1
        $ParameterAttributeTest.ParameterSetName = 'TestNameSet'

        # Add the control file and test parameters to the attribute collections
        $AttributeCollectionControlFile.Add($ParameterAttributeControlFile)
        $ValidateSetAttributeControlFile = New-Object System.Management.Automation.ValidateSetAttribute(
            $( $__ruleControlFiles | Select-Object -ExpandProperty Name | 
                % {if($_.EndsWith(".csv","CurrentCultureIgnoreCase")){$_.Substring(0,$_.Length-4)}} )
        )
        $AttributeCollectionControlFile.Add($ValidateSetAttributeControlFile)

        $AttributeCollectionTest.Add($ParameterAttributeTest)
        $ValidateSetAttributeTest = New-Object System.Management.Automation.ValidateSetAttribute(
            $( $__ruleDefinitionFiles | Select-Object -ExpandProperty Name |
                % {if($_.StartsWith("Rule-","CurrentCultureIgnoreCase")){$_.Substring(5)}} |
                % {if($_.EndsWith(".ps1","CurrentCultureIgnoreCase")){$_.Substring(0,$_.Length-4)}} )
        )
        $AttributeCollectionTest.Add($ValidateSetAttributeTest)

        # Create and return the dynamic parameter
        $RuntimeParameterControlFile = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterControlFile, [string], $AttributeCollectionControlFile)
        $RuntimeParameterTest = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterTest, [string], $AttributeCollectionTest)

        # Create and return the dynamic parameter dictionary
        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        $RuntimeParameterDictionary.Add($ParameterControlFile, $RuntimeParameterControlFile)
        $RuntimeParameterDictionary.Add($ParameterTest, $RuntimeParameterTest)

        # Add "Helpful Features" for Rules and Control Files
        # -ListControlFiles
           $ParameterTestListControls = 'ListControlFiles'
        $ParameterAttributeTestListControls = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttributeTestListControls.Mandatory = $false
        $ParameterAttributeTestListControls.Position = 2
        $ParameterAttributeTestListControls.ParameterSetName = 'TestNameSet'

        $AttributeCollectionTestListControls = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $AttributeCollectionTestListControls.Add($ParameterAttributeTestListControls)
        $RuntimeParameterTestListControls = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterTestListControls, [switch], $AttributeCollectionTestListControls)
        $RuntimeParameterDictionary.Add($ParameterTestListControls, $RuntimeParameterTestListControls)

        # -ListTests
           $ParameterControlFileListTests = 'ListTests'
        $ParameterAttributeControlFileListTests = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttributeControlFileListTests.Mandatory = $false
        $ParameterAttributeControlFileListTests.Position = 2
        $ParameterAttributeControlFileListTests.ParameterSetName = 'ControlFileSet'

        $AttributeCollectionControlFileListTests = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $AttributeCollectionControlFileListTests.Add($ParameterAttributeControlFileListTests)
        $RuntimeParameterControlFileListTests = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterControlFileListTests, [switch], $AttributeCollectionControlFileListTests)
        $RuntimeParameterDictionary.Add($ParameterControlFileListTests, $RuntimeParameterControlFileListTests)


        return $RuntimeParameterDictionary
    }

    BEGIN 
    {
        $startAll = Get-Date
        Write-SRx DEBUG "BEGIN Start-Ruleset"
        $ControlFile = $PsBoundParameters[$ParameterControlFile]
        $Test = $PsBoundParameters[$ParameterTest]

        if($RunAllTests)
        {
            $__rulesControl = @()
            $__ruleDefinitionFiles = $__ruleDefinitionFiles | ? {($_.fullname -notLike "*\Example\*") -and ($_.fullname -notLike "*\InDev\*")}
            foreach($f in $__ruleDefinitionFiles) {
                $o = New-Object PSObject -Property @{ "Rule" = $([System.IO.Path]::GetFileNameWithoutExtension($f)); "WriteToDashboard" = "true" ; "AlertOnFailure" = "true" }
                $__rulesControl += $o
            }
            $ControlFile = "RunAllTests"
        }
        elseif($Test)
        {
            $TestListControls = $PsBoundParameters[$ParameterTestListControls]
            $__rulesControl = @()
            if(-not $Test.EndsWith(".ps1", "CurrentCultureIgnoreCase")) { $Test += ".ps1" }
            if(-not $Test.StartsWith("Rule-", "CurrentCultureIgnoreCase")) { $Test = "Rule-" + $Test }
            $o = New-Object PSObject -Property @{ "Rule" = $([System.IO.Path]::GetFileNameWithoutExtension($Test)); "WriteToDashboard" = "true" ; "AlertOnFailure" = "true" }
            $__rulesControl += $o
            $ControlFile = $Test
        }
        elseif($ControlFile)
        {
            $ControlsListTests = $PsBoundParameters[$ParameterControlFileListTests]
            if(-not $ControlFile.EndsWith(".csv", "CurrentCultureIgnoreCase")) { $ControlFile += ".csv" }
            
            if(Test-Path $(Join-Path $(Join-Path $__rulesControlFolder "Core") $ControlFile)) {
                $__rulesControl = Import-Csv $(Join-Path $(Join-Path $__rulesControlFolder "Core") $ControlFile)
            } elseif(Test-Path $(Join-Path $(Join-Path $__rulesControlFolder "Premier") $ControlFile)) {
                $__rulesControl = Import-Csv $(Join-Path $(Join-Path $__rulesControlFolder "Premier") $ControlFile)
            }
        }
        else
        {
            Write-SRx ERROR "You must supply command line parameter -Test <test name>, -ControlFile <control file name>, or -RunAllTests"
            return
        }

        # timestamp for RunId
        $timestamp = $(Get-Date -f "yyyyMMddHHmmss")
    }
    PROCESS
    {
        Write-SRx DEBUG "PROCESS - Entered Start-Ruleset PROCESS"
        if($TestListControls ){
            ListControls
        } elseif($ControlsListTests){
            ListTests
        } else {
            ProcessRules
        }
    }
    END
    {
        $endAll = Get-Date
        $spanAll = New-TimeSpan $startAll $endAll
        Write-SRx INFO "Rule(s) finished in Time: [$($spanAll.Hours):$($spanAll.Minutes):$($spanAll.Seconds).$($spanAll.Milliseconds)]" -ForegroundColor Yellow
        Write-SRx DEBUG "END"
    }
}





#EndRegion '.\Public\Start-Ruleset.ps1' 215
#Region '.\Public\Start-SOP.ps1' 0
function Start-SOP {
<#
.SYNOPSIS
    Invokes a test(s) and handles where the output event(s) get written
     
.DESCRIPTION
    This cmdlet wraps the invocation chain of Invoke-ProvisioningSequence followed by:
        -> Export-SRxToSearchDashboard | Export-SRxToAlertsList
        -> and/or Write-SRxConsole
     
.NOTES
    =========================================
    Project : The Source Shell (SRx)
    -----------------------------------------
    File Name : Start-SOP.psm1
    Author : Nikolay Mukhin
    Requires :
        PowerShell Version 5.1, The Source Shell (SRx), Microsoft.SharePoint.PowerShell
    ========================================================================================
     
.INPUTS
    Control file (e.g. TestControl.csv)
 
.OUTPUTS
    [ $SRxEvent(s) ]
 
.EXAMPLE
 
#>


    [CmdletBinding()]
    param ( 
            [alias("All")][switch]$RunAllTests,
            [alias("OutNull")][switch]$NoWriteToConsole,
            [switch]$PassThrough,
            #[switch]$Production,
            #[switch]$Rebuild,
            [alias("EventLog")][switch]$WriteErrorsToEventLog,
            [switch]$Details,
            [Parameter(Mandatory=$false, ParameterSetName="Get")]
            [string]$ControlFile,
            [Parameter(Mandatory=$false, ParameterSetName="Get")]
            [string]$Test
    )
    BEGIN 
    {
        Write-SRx DEBUG "BEGIN Start-SOP"
    }
    PROCESS
    {
        Write-SRx DEBUG "PROCESS - Entered Start-SOP process"

        $global:SRxEnv.SetCustomProperty("PnPEnvironment", "Test")
        $global:SRxEnv.SetCustomProperty("PnPNewSite", $false)
        $global:SRxEnv.SetCustomProperty("PnPRebuild", $false)
        $global:SRxEnv.SetCustomProperty("SOP", $null)


        if($Test)
        {
            if($PSCmdlet.MyInvocation.BoundParameters["Debug"].IsPresent) {
                $output = New-SRxReport -Test $Test -Debug
            } else {
                $output = New-SRxReport -Test $Test 
            }
        }
        elseif($ControlFile)
        {
            Set-SRxPipeline
            $output = New-SRxReport -ControlFile $ControlFile 
        }
        else
        {
            
            $shell = $false
            $exit = $false
            $SOP = "Operations"
            $group = "System Utilities"
            $member = "Update Provisioning Schema in local cache"
            do {
                Set-SRxPipeline

                $sopobject = $global:SRxEnv.DefaultSettings.EcoSystem.$SOP.$group.$member
                $global:SRxEnv.SetCustomProperty("SOP", $sopobject)
                $global:SRxEnv.SOP.ts_Title = "Provisioning Operations" 

                $ProgressPreference = "SilentlyContinue"
                $output = Test-SRx -ControlFile "Start-SOP"
                $ProgressPreference = "Continue"

                if($null -ne $global:SRxEnv.SOP) {
                    if($global:SRxEnv.SOP -eq "Exit") {
                        $exit = $true
                    }
                    elseif( $global:SRxEnv.SOP.ts_ControlFile -eq "Exit") {
                        $exit = $true
                    }
                    else {
                        $global:SRxEnv.SetCustomProperty("PnPEnvironment", $global:SRxEnv.SOP.ts_Environment)
                        $global:SRxEnv.SetCustomProperty("PnPRebuild", [boolean]$global:SRxEnv.SOP.ts_Rebuild)
                        $global:SRxEnv.SetCustomProperty("PnPNewSite", [boolean]$global:SRxEnv.SOP.ts_NewSite)
                        Show-SRxSOPTitle -Clear | Out-Null
                        if( $global:SRxEnv.SOP.ts_IsAdmin -and (-not $global:SRxEnv.isStakeHolder)) {
                            $output = New-SRxReport -ControlFile "Invoke-NotImplemented"
                        }
                        else {
                            $output = New-SRxReport -ControlFile $global:SRxEnv.SOP.ts_ControlFile
                        }
                    }
                }
                if( -not $exit) {
                    if( $global:SRxEnv.SOP.ts_ControlFile -ne "Format-EcoSystem") {
                        $decision = $Host.UI.PromptForChoice("------------------------------------------------------", '', @('&Main Menu'; '&Shell'), 0)
                        Write-Host    
                        if( $decision -eq 1) {
                            $shell = $true 
                            $exit = $true 
                        }
                    }
                }
            }
            while(-not $exit)
            if( -not $shell) { Clear-Host }

        }
        if($PassThrough)
        {
            $output
        }
    }
    END
    {
        $global:SRxEnv.SetCustomProperty("PnPEnvironment", "Test")
        $global:SRxEnv.SetCustomProperty("PnPNewSite", $false)
        $global:SRxEnv.SetCustomProperty("PnPRebuild", $false)
        $global:SRxEnv.SetCustomProperty("SOP", $null)
        Start-SRxShell
        Write-SRx DEBUG "END"
    }
}


#EndRegion '.\Public\Start-SOP.ps1' 143
#Region '.\Public\Start-SRxShell.ps1' 0
Function Start-SRxShell {
    [CmdletBinding(PositionalBinding=$true)]
    PARAM (
        [Parameter(Mandatory=$false)][string]$RootPath,
        [parameter(Mandatory=$false,ValueFromPipeline=$false)][string]$ControlFile,
        [switch]$Setup,                    #== Use this to open Setup Menu
        [Parameter(Mandatory=$false)][switch]$isJob
    )
    PROCESS {
        try {
            if( -not $global:SRxEnv ) { 
                Initialize-SRxEnv -RootPath $RootPath
                if( -not $global:SRxEnv ) { return }
            }           

            $global:SRxEnv.LoadModule("Microsoft.Online.SharePoint.PowerShell",$global:SRxEnv.Modules."Microsoft.Online.SharePoint.PowerShell",$false)
            #$global:SRxEnv.LoadModule("PnP.Powershell","1.8.0",$false)
            $global:SRxEnv.LoadModule("PnP.Powershell",$global:SRxEnv.Modules."PnP.Powershell",$false)

            $global:SRxEnv.SetCustomProperty("PnPEnvironment", "Test")
            $global:SRxEnv.SetCustomProperty("PnPNewSite", $false)
            $global:SRxEnv.SetCustomProperty("PnPRebuild", $false)
            #Set-SRxPipeline
            $global:SRxEnv.SetCustomProperty("MasterDesign", "Department")                        
            Read-SRxPipelineValue | Out-Null
            $global:SRxEnv.SetCustomProperty("SOP", $null)

            if( -not $global:SRxEnv.Tenancy.LastConnectionSuccessful)       { New-SRxReport -Test Format-EcoSystem | Out-Null }
            else {
                Get-SRxConnection -SPO -siteUrl $global:SRxEnv.Tenancy.AdminUrl | Out-Null 
                if( -not $global:SRxEnv.Tenancy.LastConnectionSuccessful)   { New-SRxReport -Test Format-EcoSystem | Out-Null }
            }

            Connect-SRxProvisioningDatabase_JSON | Out-Null               
            $global:SRxEnv.SetCustomProperty("isStakeHolder",    $($global:SRxEnv.Tenancy.IsStakeholder))   
                            
            Set-Item Env:\PNPPOWERSHELL_UPDATECHECK "off" 

            $global:SRxEnv.UpdateShellTitle()
            
            if($Setup) {
                
                #-----------------------------------------------
                # Format windows shortcuts
                #-----------------------------------------------
                #-----------------------------
                # auto-startup for systray
                #-----------------------------
                $programs = @{
                    "Provisioning Explorer" = @("%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe", "Startup", 7, ' -NoProfile -noexit -WindowStyle hidden -ExecutionPolicy Bypass -Command ".\shell.ps1 -SysTray"') #minimized
                }
                $startMenu = $($home + "\AppData\Roaming\Microsoft\Windows\Start Menu\Programs")
                Format-SRxFileShortcut -programs $programs -startMenu $startMenu

                #-----------------------------
                # start menu
                #-----------------------------
                $programs = @{
                    "Provisioning Shell"    = @("%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe", "SharePoint", 1, ' -NoProfile -noexit -ExecutionPolicy Bypass -Command ".\shell.ps1"') #normal
                    "Provisioning Explorer" = @("%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe", "SharePoint", 7, ' -NoProfile -noexit -WindowStyle hidden -ExecutionPolicy Bypass -Command ".\shell.ps1 -SysTray"') #minimized
                    #"Debug" = @("%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe", "SharePoint", 1, ' -NoProfile -noexit -ExecutionPolicy Bypass -Command ".\shell.ps1 -SysTray"') #normal
                }
                $startMenu = "$($home)\AppData\Roaming\Microsoft\Windows\Start Menu\Programs"
                Format-SRxFileShortcut -programs $programs -startMenu $startMenu
                #-----------------------------
                # provisioning folder
                #-----------------------------
                
                $programs = @{
                    "shell"    = @("%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe", $null, 1, ' -NoProfile -noexit -ExecutionPolicy Bypass -Command ".\shell.ps1"') #normal
                    "explorer" = @("%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe", $null, 7, ' -NoProfile -noexit -WindowStyle hidden -ExecutionPolicy Bypass -Command ".\shell.ps1 -SysTray"') #minimized
                    "debug"    = @("%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe", $null, 1, ' -NoProfile -noexit -ExecutionPolicy Bypass -Command ".\shell.ps1 -SysTray"') #normal
                }
                $str = $SRxEnv.Paths.SRxRoot
                $workingdirectory = $str.substring(0, $str.lastindexof('\'))                
                #$startMenu = "$($home)\AppData\Roaming\Microsoft\Windows\Start Menu\Programs"
                Format-SRxFileShortcut -programs $programs -startMenu $workingdirectory
                
                #-----------------------------
                # start setup
                #-----------------------------
                New-SRxReport -Test Format-EcoSystem | Out-Null    
                if($global:SRxEnv.isSetup) { #close window and terminate process
                    $window.Close()
                    Stop-Process $pid                
                }        
            }
            elseif($ControlFile -eq "Start-Command") { 
                Test-SRx -ControlFile $ControlFile | Out-Null 
                return $null
            }
            else {
                if($ControlFile) { Test-SRx -ControlFile $ControlFile | Out-Null }
                # Write-SRx VERBOSE $("[shell] Configuring commands: config, log, cache, task, menu") -ForegroundColor DarkCyan
<#
                if( -not $(Get-Alias -Name ecosystem -ErrorAction SilentlyContinue)) {
                    New-Alias -Name ecosystem -Value _EcoSystem -Force -Scope Global -Option Constant
                }
                if( -not $(Get-Alias -Name log -ErrorAction SilentlyContinue)) {
                    New-Alias -Name log -Value _Log -Force -Scope Global -Option Constant
                }
                if( -not $(Get-Alias -Name cache -ErrorAction SilentlyContinue)) {
                    New-Alias -Name cache -Value _Cache -Force -Scope Global -Option Constant
                }
                if( -not $(Get-Alias -Name task -ErrorAction SilentlyContinue)) {
                    New-Alias -Name task -Value _Task -Force -Scope Global -Option Constant
                }
                if( -not $(Get-Alias -Name shell -ErrorAction SilentlyContinue)) {
                    New-Alias -Name shell -Value _Shell -Force -Scope Global -Option Constant
                }
                if( -not $(Get-Alias -Name menu -ErrorAction SilentlyContinue)) {
                    New-Alias -Name menu -Value Start-SOP -Force -Scope Global -Option Constant
                }
                if( -not $(Get-Alias -Name systray -ErrorAction SilentlyContinue)) {
                    New-Alias -Name systray -Value Start-SysTray -Force -Scope Global -Option Constant
                }
                #if( -not $(Get-Alias -Name gui -ErrorAction SilentlyContinue)) {
                # New-Alias -Name gui -Value _Gui -Force -Scope Global -Option Constant
                #}
                if( -not $(Get-Alias -Name docs -ErrorAction SilentlyContinue)) {
                    New-Alias -Name docs -Value _Docs -Force -Scope Global -Option Constant
                }
                if( -not $(Get-Alias -Name pipeline -ErrorAction SilentlyContinue)) {
                    New-Alias -Name pipeline -Value _Pipeline -Force -Scope Global -Option Constant
                }
#>
    
                if( -not $global:__SRxHasInitFailure ) {
                    if( $global:SRxEnv.Log.Level -ne "VERBOSE") { Clear-Host }
                }
    
                if( -not $isJob) { Show-SRxShell }
            }
         }
        catch {
            return $null
        }
        return $null
    }
}
    
#EndRegion '.\Public\Start-SRxShell.ps1' 141
#Region '.\Public\Start-SRxSysTray.ps1' 0
Add-Type -assembly System.Windows.Forms
Add-Type -assembly System.Data
Add-Type -assembly System.Drawing
Add-Type -assembly System.Design

function Start-SRxSysTray {
<#
.SYNOPSIS
    Invokes a test(s) and handles where the output event(s) get written
     
.DESCRIPTION
    This cmdlet wraps the invocation chain of Invoke-ProvisioningSequence followed by:
        -> Export-SRxToSearchDashboard | Export-SRxToAlertsList
        -> and/or Write-SRxConsole
     
.NOTES
    =========================================
    Project : The Source Shell (SRx)
    -----------------------------------------
    File Name : Start-SysTray.psm1
    Author : Nikolay Mukhin
    Requires :
        PowerShell Version 5.1, The Source Shell (SRx), Microsoft.SharePoint.PowerShell
    ========================================================================================
     
.INPUTS
    Control file (e.g. TestControl.csv)
 
.OUTPUTS
    [ $SRxEvent(s) ]
 
.EXAMPLE
 
#>

    [CmdletBinding(PositionalBinding=$true)]
    PARAM ( 
        [Parameter(Mandatory=$false)][string]$RootPath,
        [switch]$PassThrough
    )
    BEGIN 
    {
        #Write-Host "BEGIN Start-SysTray"
        if( -not $global:SRxEnv ) { 
            Initialize-SRxEnv -RootPath $RootPath
        }
        if( $global:SRxEnv.Exists ) {  
            $global:SRxEnv.LoadModule("SRxSysTrayHost",$null,$null)
            #-----------------------------
            # auto-startup for systray
            #-----------------------------
            $programs = @{
                "Provisioning Explorer" = @("%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe", "Startup", 7, ' -NoProfile -noexit -WindowStyle hidden -ExecutionPolicy Bypass -Command ".\shell.ps1 -SysTray"') #minimized
            }
            $startMenu = $($home + "\AppData\Roaming\Microsoft\Windows\Start Menu\Programs")
            Format-SRxFileShortcut -programs $programs -startMenu $startMenu

            #-----------------------------
            # start menu
            #-----------------------------
            $programs = @{
                "Provisioning Shell"    = @("%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe", "SharePoint", 1, ' -NoProfile -noexit -ExecutionPolicy Bypass -Command ".\shell.ps1"') #normal
                "Provisioning Explorer" = @("%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe", "SharePoint", 7, ' -NoProfile -noexit -WindowStyle hidden -ExecutionPolicy Bypass -Command ".\shell.ps1 -SysTray"') #minimized
                #"Debug" = @("%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe", "SharePoint", 1, ' -NoProfile -noexit -ExecutionPolicy Bypass -Command ".\shell.ps1 -SysTray"') #normal
            }
            $startMenu = "$($home)\AppData\Roaming\Microsoft\Windows\Start Menu\Programs"
            Format-SRxFileShortcut -programs $programs -startMenu $startMenu
        }
    }
    PROCESS
    {
        try {  
            if( -not $global:SRxEnv.Exists ) { return }  
            $global:SRxEnv.UpdateShellTitle()
            Write-SRx DEBUG "PROCESS - Entered Start-SysTray process"

            # 'dirty' implementation for singleton process when use -SysTray argument
            $activeProcessId = Get-WmiObject win32_process -Filter "commandline like '%-SysTray%'"  | Select-Object -Property processid
            ($activeProcessId | Where-Object { ($($pid) -ne $_.processid) }) | ForEach-Object {  Stop-Process $($_.processid) | Out-Null }
        

            # Force garbage collection just to start slightly lower RAM usage.
            [System.GC]::Collect()


            $f = New-SysTrayHost
            $f.Run2()            
        }
        catch {
            Write-Host "--- Start-SysTray ---"
            Write-Host ($_.Exception.Message) 
            Write-Host ($_.Exception) 

            Read-Host
        }    
    }
    END
    {
        #Write-Host "END Start-SysTray"
        #Read-Host
        if( $global:SRxEnv.Exists ) { Stop-Process $pid }
        #Write-Host "END Start-SysTray"
    }
}
#EndRegion '.\Public\Start-SRxSysTray.ps1' 104
#Region '.\Public\Test-SRx.ps1' 0
function Test-SRx { 
<#
.SYNOPSIS
    Evaluates test definition file and generates a corresponding SRxEvent
     
.DESCRIPTION
    Can specify a control file as a parameter, which defines a specific set
    of tests to perform where each test specified in the control generates
    an SRxEvent
     
.NOTES
    =========================================
    Project : Search Health Reports (SRx)
    -----------------------------------------
    File Name : Test-SRx.psm1
    Author : Eric Dixon
 
    Requires :
        PowerShell Version 5.1, Search Health Reports (SRx), Microsoft.SharePoint.PowerShell
     
    ========================================================================================
    This Sample Code is provided for the purpose of illustration only and is not intended to
    be used in a production environment.
     
        THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY
        OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
        WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
 
    We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to
    reproduce and distribute the object code form of the Sample Code, provided that You agree:
        (i) to not use Our name, logo, or trademarks to market Your software product in
            which the Sample Code is embedded;
        (ii) to include a valid copyright notice on Your software product in which the
             Sample Code is embedded;
        and
        (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against
              any claims or lawsuits, including attorneys' fees, that arise or result from
              the use or distribution of the Sample Code.
 
    ========================================================================================
     
.INPUTS
    Control file (e.g. TestControl.csv)
 
.OUTPUTS
    [ $SRxEvent(s) ]
 
.EXAMPLE
    Test-SRx -ControlFile TestControl.csv
 
#>

    [CmdletBinding()]
    param ( 
            [Parameter(ParameterSetName='DefaultSet')]
            [alias("All")][switch]$RunAllTests,
            [Parameter(ParameterSetName='DefaultSet')]
            $Params=$null,
            [Parameter(ParameterSetName='DefaultSet')]
            [switch]$WhatIf=$false
    )

    DynamicParam 
    {
        # Set the dynamic parameters' name
        $ParameterControlFile = 'ControlFile'
        $ParameterTest = 'Name'
        
        # Generate the parameter values (e.g. control and test file names) for validation
        $__rulesControlFolder = $global:SRxEnv.Paths.RuleSets #Join-Path $global:SRxEnv.Paths.Config "TestControls"
        $__ruleControlFiles = Get-ChildItem -Path $__rulesControlFolder -File "*.csv" -Recurse 

        $__ruleDefinitionsFolder = $global:SRxEnv.Paths.Rules
        $__ruleDefinitionFiles = Get-ChildItem -Path $__ruleDefinitionsFolder -File "Rule-*.ps1" -Recurse

        # Create the collection of attributes
        $AttributeCollectionControlFile = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $AttributeCollectionTest = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        
        # Create and set the parameters' attributes
        $ParameterAttributeControlFile = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttributeControlFile.Mandatory = $false
        $ParameterAttributeControlFile.Position = 1
        $ParameterAttributeControlFile.ParameterSetName = 'ControlFileSet'

        $ParameterAttributeTest = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttributeTest.Mandatory = $false
        $ParameterAttributeTest.Position = 1
        $ParameterAttributeTest.ParameterSetName = 'TestNameSet'

        # Add the control file and test parameters to the attribute collections
        $AttributeCollectionControlFile.Add($ParameterAttributeControlFile)
        $ValidateSetAttributeControlFile = New-Object System.Management.Automation.ValidateSetAttribute(
            $( $__ruleControlFiles | Select-Object -ExpandProperty Name | 
                % {if($_.EndsWith(".csv","CurrentCultureIgnoreCase")){$_.Substring(0,$_.Length-4)}} )
        )
        $AttributeCollectionControlFile.Add($ValidateSetAttributeControlFile)

        $AttributeCollectionTest.Add($ParameterAttributeTest)
        $ValidateSetAttributeTest = New-Object System.Management.Automation.ValidateSetAttribute(
            $( $__ruleDefinitionFiles | Select-Object -ExpandProperty Name |
                % {if($_.StartsWith("Rule-","CurrentCultureIgnoreCase")){$_.Substring(5)}} |
                % {if($_.EndsWith(".ps1","CurrentCultureIgnoreCase")){$_.Substring(0,$_.Length-4)}} )
        )
        $AttributeCollectionTest.Add($ValidateSetAttributeTest)

        # Create and return the dynamic parameter
        $RuntimeParameterControlFile = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterControlFile, [string], $AttributeCollectionControlFile)
        $RuntimeParameterTest = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterTest, [string], $AttributeCollectionTest)

        # Create and return the dynamic parameter dictionary
        $RuntimeParameterDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        $RuntimeParameterDictionary.Add($ParameterControlFile, $RuntimeParameterControlFile)
        $RuntimeParameterDictionary.Add($ParameterTest, $RuntimeParameterTest)

        # Add "Helpful Features" for Rules and Control Files
        # -ListControlFiles
           $ParameterTestListControls = 'ListControlFiles'
        $ParameterAttributeTestListControls = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttributeTestListControls.Mandatory = $false
        $ParameterAttributeTestListControls.Position = 2
        $ParameterAttributeTestListControls.ParameterSetName = 'TestNameSet'

        $AttributeCollectionTestListControls = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $AttributeCollectionTestListControls.Add($ParameterAttributeTestListControls)
        $RuntimeParameterTestListControls = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterTestListControls, [switch], $AttributeCollectionTestListControls)
        $RuntimeParameterDictionary.Add($ParameterTestListControls, $RuntimeParameterTestListControls)

        # -ListTests
           $ParameterControlFileListTests = 'ListTests'
        $ParameterAttributeControlFileListTests = New-Object System.Management.Automation.ParameterAttribute
        $ParameterAttributeControlFileListTests.Mandatory = $false
        $ParameterAttributeControlFileListTests.Position = 2
        $ParameterAttributeControlFileListTests.ParameterSetName = 'ControlFileSet'

        $AttributeCollectionControlFileListTests = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
        $AttributeCollectionControlFileListTests.Add($ParameterAttributeControlFileListTests)
        $RuntimeParameterControlFileListTests = New-Object System.Management.Automation.RuntimeDefinedParameter($ParameterControlFileListTests, [switch], $AttributeCollectionControlFileListTests)
        $RuntimeParameterDictionary.Add($ParameterControlFileListTests, $RuntimeParameterControlFileListTests)


        return $RuntimeParameterDictionary
    }

    BEGIN 
    {
        $startAll = Get-Date
        Write-SRx DEBUG "BEGIN Test-SRx"
        $ControlFile = $PsBoundParameters[$ParameterControlFile]
        $Test = $PsBoundParameters[$ParameterTest]

        if($RunAllTests)
        {
            $__rulesControl = @()
            $__ruleDefinitionFiles = $__ruleDefinitionFiles | ? {($_.fullname -notLike "*\Example\*") -and ($_.fullname -notLike "*\InDev\*")}
            foreach($f in $__ruleDefinitionFiles) {
                $o = New-Object PSObject -Property @{ "Rule" = $([System.IO.Path]::GetFileNameWithoutExtension($f)); "WriteToDashboard" = "true" ; "AlertOnFailure" = "true" }
                $__rulesControl += $o
            }
            $ControlFile = "RunAllTests"
        }
        elseif($Test)
        {
            $TestListControls = $PsBoundParameters[$ParameterTestListControls]
            $__rulesControl = @()
            if(-not $Test.EndsWith(".ps1", "CurrentCultureIgnoreCase")) { $Test += ".ps1" }
            if(-not $Test.StartsWith("Rule-", "CurrentCultureIgnoreCase")) { $Test = "Rule-" + $Test }
            $o = New-Object PSObject -Property @{ "Rule" = $([System.IO.Path]::GetFileNameWithoutExtension($Test)); "WriteToDashboard" = "true" ; "AlertOnFailure" = "true" }
            $__rulesControl += $o
            $ControlFile = $Test
        }
        elseif($ControlFile)
        {
            $ControlsListTests = $PsBoundParameters[$ParameterControlFileListTests]
            if(-not $ControlFile.EndsWith(".csv", "CurrentCultureIgnoreCase")) { $ControlFile += ".csv" }
            
            if(Test-Path $(Join-Path $(Join-Path $__rulesControlFolder "Core") $ControlFile)) {
                $__rulesControl = Import-Csv $(Join-Path $(Join-Path $__rulesControlFolder "Core") $ControlFile)
            } elseif(Test-Path $(Join-Path $(Join-Path $__rulesControlFolder "Premier") $ControlFile)) {
                $__rulesControl = Import-Csv $(Join-Path $(Join-Path $__rulesControlFolder "Premier") $ControlFile)
            }
        }
        else
        {
            Write-SRx ERROR "You must supply command line parameter -Test <test name>, -ControlFile <control file name>, or -RunAllTests"
            return
        }

        # timestamp for RunId
        $timestamp = $(Get-Date -f "yyyyMMddHHmmss")
    }
    PROCESS
    {
        Write-SRx DEBUG "PROCESS - Entered Test-SRx PROCESS"
        if($TestListControls ){
            ListControls
        } elseif($ControlsListTests){
            ListTests
        } else {
            ProcessRules
        }
    }
    END
    {
        $endAll = Get-Date
        $spanAll = New-TimeSpan $startAll $endAll
        Write-SRx INFO "Rule(s) finished in Time: [$($spanAll.Hours):$($spanAll.Minutes):$($spanAll.Seconds).$($spanAll.Milliseconds)]" -ForegroundColor Yellow
        Write-SRx DEBUG "END"
    }
}





#EndRegion '.\Public\Test-SRx.ps1' 215
#Region '.\Public\Test-SRxAllowedForRecycleBinDeletion.ps1' 0
Function Test-SRxAllowedForRecycleBinDeletion {
    [CmdletBinding()]
    PARAM (
        [Parameter(Mandatory=$true)][string]$siteURL,
        [Parameter(Mandatory=$true)][string]$environment
    )
    PROCESS {
        try {
            #----------------------------------------------------------------------------------------
            # Protection for recycle bin deletion for any sites except dev, test and release
            # URL structure https://{tenancy}.sharepoint.com/sites/{ecosystem}_{environment)_sitename
            #-----------------------------------------------------------------------------------------
            $enableDeleteRecycleBin = $false
            # 1. Any site in production environment can't be deleted from recycle bin
            $enableDeleteRecycleBin = $($environment -ne "Production")
            if($enableDeleteRecycleBin) {
                # 2. URL must start with {ecosystem} prefix
                $ecosystemPrefix = Get-SRxCustomPropertyValueByKey_JSON -Key "ts_URLPrefix"
                $enableDeleteRecycleBin = $siteURL.Contains("sites/$($ecosystemPrefix)_")
                if($enableDeleteRecycleBin) {
                    # 3. URL must include one of {environment} substrings listed below:
                    $environmentURL = @('_test_', '_release_', '_development_')
                    $enableDeleteRecycleBin = ($null -ne ($environmentURL | Where-Object { $siteURL -match $_ }))
                }
            }
            Write-SRx VERBOSE " > Site allowance for deletion in RecycleBin: $enableDeleteRecycleBin"            
        }
        catch {
            Write-SRx ERROR ($_.Exception.Message) 
            Write-SRx VERBOSE ($_.Exception) 
            return $false  
        }
        return $enableDeleteRecycleBin
    }
}
#EndRegion '.\Public\Test-SRxAllowedForRecycleBinDeletion.ps1' 36
#Region '.\Public\Test-SRxIsStakeHolder.ps1' 0
Function Test-SRxIsStakeHolder {
    PROCESS {
        try {
            $retVal = $false
            #----------------------------------------------------------------------------------------
            # Test to enforce protection for secitive operations
            #-----------------------------------------------------------------------------------------
            $connection = Get-SRxConnection -siteUrl $global:SRxEnv.Tenancy.AdminUrl

            $termSet = Get-PnPTermSet -Identity $global:SRxEnv.Tenancy.TermSetID -TermGroup $global:SRxEnv.Tenancy.TermGroupName -Connection $connection
            Get-PnPProperty -ClientObject $termSet -Property Stakeholders -Connection $connection | Out-Null

            $searcher = [adsisearcher]"(samaccountname=$env:USERNAME)"
            $account = $searcher.FindOne().Properties.mail
            Write-SRx VERBOSE "Stakeholders: $($termSet.Stakeholders)"
            Write-SRx VERBOSE "account: $account"

# $samlAccount = $("i:0#.f|membership|$account").ToLower()
# $retVal = $( $samlAccount -in $($termSet.Stakeholders))
            #$retVal = $( $account -in $($termSet.Stakeholders))
            $retVal = $( $(@($termSet.Stakeholders -like "*$account").Count) -ne 0)

            Write-SRx VERBOSE " > Stakeholder permission for user $account : $retVal" 
        }
        catch {
            #Write-SRx ERROR ($_.Exception.Message)
            #Write-SRx VERBOSE ($_.Exception)
            $retVal = $false
            Write-SRx VERBOSE " > Stakeholder permission for user $account : $retVal" 
            return $false  
        }
        return $retVal
    }
}
#EndRegion '.\Public\Test-SRxIsStakeHolder.ps1' 35
#Region '.\Public\Test-SRxProvisioningDatabase_JSON.ps1' 0
Function Test-SRxProvisioningDatabase_JSON {
    $retVal = $false
    if( $global:SRxEnv.Tenancy.TermGroupID -ne "") {
        $profilesFolderPath = $global:SRxEnv.CachePath + "\Profiles"
        $sgroupName = (Remove-SRxStringSpecialCharacter -String $global:SRxEnv.Tenancy.TermGroupName)
        $profileFolderPath = $profilesFolderPath + "\" + $sgroupName
        $configDPath = $profileFolderPath + "\ProvisioningDatabase.min.json"
        if ((Test-Path $configDPath)) { $retVal = $true }
    }
    return $retVal
}
#EndRegion '.\Public\Test-SRxProvisioningDatabase_JSON.ps1' 12
#Region '.\Public\Update-SRxTermCustomProperty.ps1' 0
Function Update-SRxTermCustomProperty {
    [CmdletBinding()]
    PARAM (
        [Parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$false)]$termID,
        [Parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$true)][string]$Key,
        [Parameter(ValueFromPipelineByPropertyName=$true, Mandatory=$true)]$Value,
        [switch]$termSet         
    )
    BEGIN {
        $connection = Get-SRxConnection -siteUrl $global:SRxEnv.Tenancy.AdminUrl
    }
    PROCESS {
        try {
            #$customProperties = @{}
            #$customProperties.add($Key, $Value)
            #Set-PnPTerm -Identity $([GUID]$termID) -CustomProperties $customProperties -Connection $connection
            #Start-Sleep -Seconds 3
            #return
            if( -not $termSet) { $termSite = Get-PnPTerm -Identity $([GUID]$termID) -IncludeChildTerms -Connection $connection }
            else               { $termSite = Get-PnPTermSet -Identity $global:SRxEnv.Tenancy.TermSetID -TermGroup $global:SRxEnv.Tenancy.TermGroupName -Connection $connection }

            if($termSite) {
                Get-PnPProperty -ClientObject $termSite -Property CustomProperties, Name -Connection $connection | Out-Null
                $termSite.SetCustomProperty($Key, $Value)
                #$status = "Updating $environment/Designs/$termDesign/Sites/$siteTitle ..."
                #Write-Progress -Activity $Activity -Status $status -PercentComplete $(($counter++ / $count) * 100)
                #Write-SRx VERBOSE " > $status"
                Invoke-PnPQuery -RetryCount 5 -Connection $connection 
                Start-Sleep -Seconds 3 
            }               
            #Write-SRx VERBOSE " > Updated."
        } catch {
            Write-SRx ERROR ("Update-SRxTermCustomProperty: $($_.Exception.Message)") 
            Write-SRx VERBOSE ($_.Exception) 
            return "exception"  
        }
        return 0
    }
    END {
        #if( $connection) { Disconnect-PnPOnline -Connection $connection }
    }
}
#EndRegion '.\Public\Update-SRxTermCustomProperty.ps1' 43
#Region '.\Public\Write-SRx.ps1' 0
$global:__SRxUseSimpleTemplate = $false
function Write-SRx 
{ 
<#
.SYNOPSIS
    Custom logging module
 
.DESCRIPTION
    Compares the logging level of the current Write-SRx invocation with
    the $global:SRxEnv.Log.Level to assess if this should be logged or not.
     
    Additionally, can log to a common log file based on the current setting
    for $global:SRxEnv.Log.ToFile
     
    All errors are automatically logged to:
        $global:SRxEnv.Paths.Log "errors.log"
     
.NOTES
    =========================================
    Project : Search Health Reports (SRx)
    -----------------------------------------
    File Name : Write-SRx.psm1
    Author : Eric Dixon
    Requires : PowerShell Version 5.1, Search Health Reports (SRx)
    ========================================================================================
    This Sample Code is provided for the purpose of illustration only and is not intended to
    be used in a production environment.
     
        THIS SAMPLE CODE AND ANY RELATED INFORMATION ARE PROVIDED "AS IS" WITHOUT WARRANTY
        OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE IMPLIED
        WARRANTIES OF MERCHANTABILITY AND/OR FITNESS FOR A PARTICULAR PURPOSE.
 
    We grant You a nonexclusive, royalty-free right to use and modify the Sample Code and to
    reproduce and distribute the object code form of the Sample Code, provided that You agree:
        (i) to not use Our name, logo, or trademarks to market Your software product in
            which the Sample Code is embedded;
        (ii) to include a valid copyright notice on Your software product in which the
             Sample Code is embedded;
        and
        (iii) to indemnify, hold harmless, and defend Us and Our suppliers from and against
              any claims or lawsuits, including attorneys' fees, that arise or result from
              the use or distribution of the Sample Code.
 
    ========================================================================================
     
.EXAMPLE
    Write-SRx VERBOSE "some message here..." -ForegroundColor Cyan -NoNewline
 
#>

[CmdletBinding()]
param ( 
        [parameter(Mandatory=$false)]
        #[ValidateSet([SRxLogLevel]::SILENT, [SRxLogLevel]::INFO, [SRxLogLevel]::VERBOSE, [SRxLogLevel]::WARNING, [SRxLogLevel]::ERROR, [SRxLogLevel]::DEBUG)]
        [SRxLogLevel]$Level=[SRxLogLevel]::INFO,
        [parameter(Mandatory=$false,ValueFromPipeline=$true)]
        [string]$Message,
        [string]$Product="",
        [string]$Category="",
        [string]$Thread="",
        [parameter(Mandatory=$false)]
        [System.ConsoleColor]$ForegroundColor=(get-host).ui.rawui.ForegroundColor,
        [System.ConsoleColor]$BackgroundColor=(get-host).ui.rawui.BackgroundColor,
        [switch]$NoNewline=$false
        )

    BEGIN 
    {
        #if($global:SRxEnv.SRxProgressBar) {
        # $global:SRxEnv.SetCustomProperty("SRxProgressBar", $false)
        # Write-Host
        #}

        if($ForegroundColor -lt 0) { $ForegroundColor = [System.ConsoleColor]::White }
        if($BackgroundColor -lt 0) { $BackgroundColor = [System.ConsoleColor]::DarkBlue }
        if($global:__SRxUseSimpleTemplate)
        {
            $msgTempl = "{0}"
        }
        else
        {
            $now = Get-Date -Format "MM/dd/yyyy HH:mm:ss"
            $msgTempl = "$now`t`t$Thread`t$Product`t$Category`t`t$Level`t{0}"
        }
        $global:__SRxUseSimpleTemplate = $NoNewline
    }
    PROCESS
    {
        $logToFile_ONLY = $false
        if([int]$Level -gt [int]$([SRxLogLevel]::$($global:SRxEnv.Log.Level)))
        {
            #return
            $logToFile_ONLY = $true
        }

        # this handles Write-SRx with no parameters. Just writes the time stamp and nothing to log file.
        if([string]::IsNullOrEmpty($Message))
        {
            Write-Host 
            return
        }

        foreach($msg in $Message)
        {
            if( -not $logToFile_ONLY) {
                switch($Level)
                {
                    ERROR {
                        Show-SRxWindow
                        Write-Host -ForegroundColor Red $msg
                    }
                    WARNING {Write-Host -ForegroundColor Yellow $msg}
                    INFO {Write-Host $msg -ForegroundColor $ForegroundColor -BackgroundColor $BackgroundColor -NoNewline:$NoNewline}
                    VERBOSE {Write-Host $msg -ForegroundColor $ForegroundColor -BackgroundColor $BackgroundColor -NoNewline:$NoNewline}
                    DEBUG {Write-Host $msg -ForegroundColor Gray}
                    Default {return} # bail on invalid log levels
                }
            }

            $m = $msgTempl -f $msg
            if($global:SRxEnv.Log.ToFile)
            {
                Add-Content $global:SRxEnv.LogFile $m
            }

            if([int]$Level -eq [int]$([SRxLogLevel]::ERROR))
            {
                $errorFile = Join-Path $global:SRxEnv.Paths.Log "errors.log"
                $global:SRxEnv.CreateFileWithReadPermissions($errorFile)
                Add-Content $errorFile $m
            }
        }
    }
    END
    {
    }
}

#EndRegion '.\Public\Write-SRx.ps1' 138
#Region '.\Public\Write-SRxConsole.ps1' 0

function Write-SRxConsole 
{ 
[CmdletBinding()]
param ( 
        [parameter(Mandatory=$true,ValueFromPipeline=$true)]
        [object[]]$data,
        [switch]$Details
        )

    BEGIN 
    {
        [System.Collections.ArrayList]$dataList = @()
    }
    PROCESS
    {
        foreach($d in $data)
        {
            $dataList.Add($d) | Out-Null
            Write-SRx DEBUG "PROCESS[$d]"
        }
    }
    END
    {
        Write-SRx DEBUG " --> [formatting data for output"
        if ($global:SRxEnv.Log.Level -eq "Debug")
        {
            $dataList | Format-List | Out-String -Stream
        } 
        else 
        {
            $dataList | Format-Table -AutoSize  @{l='Rule'; e={$_.Name.Substring(5)}}, Level, Headline | Out-String -Stream | ColorByLevel -IncludeDetails:$Details
        }
    }
}
#EndRegion '.\Public\Write-SRxConsole.ps1' 36
#Region '.\Public\Write-SRxMenu.ps1' 0
function Write-SRxMenu {
    <#
        .SYNOPSIS
            Outputs a command-line menu which can be navigated using the keyboard.
 
        .DESCRIPTION
            Outputs a command-line menu which can be navigated using the keyboard.
 
            * Automatically creates multiple pages if the entries cannot fit on-screen.
            * Supports nested menus using a combination of hashtables and arrays.
            * No entry / page limitations (apart from device performance).
            * Sort entries using the -Sort parameter.
            * -MultiSelect: Use space to check a selected entry, all checked entries will be invoked / returned upon confirmation.
            * Jump to the top / bottom of the page using the "Home" and "End" keys.
            * "Scrolling" list effect by automatically switching pages when reaching the top/bottom.
            * Nested menu indicator next to entries.
            * Remembers parent menus: Opening three levels of nested menus means you have to press "Esc" three times.
 
            Controls Description
            -------- -----------
            Up Previous entry
            Down Next entry
            Left / PageUp Previous page
            Right / PageDown Next page
            Home Jump to top
            End Jump to bottom
            Space Check selection (-MultiSelect only)
            Enter Confirm selection
            Esc / Backspace Exit / Previous menu
 
        .EXAMPLE
            PS C:\>$menuReturn = Write-SRxMenu -Title 'Menu Title' -Entries @('Menu Option 1', 'Menu Option 2', 'Menu Option 3', 'Menu Option 4')
 
            Output:
 
              Menu Title
 
               Menu Option 1
               Menu Option 2
               Menu Option 3
               Menu Option 4
 
        .EXAMPLE
            PS C:\>$menuReturn = Write-SRxMenu -Title 'AppxPackages' -Entries (Get-AppxPackage).Name -Sort
 
            This example uses Write-SRxMenu to sort and list app packages (Windows Store/Modern Apps) that are installed for the current profile.
 
        .EXAMPLE
            PS C:\>$menuReturn = Write-SRxMenu -Title 'Advanced Menu' -Sort -Entries @{
                'Command Entry' = '(Get-AppxPackage).Name'
                'Invoke Entry' = '@(Get-AppxPackage).Name'
                'Hashtable Entry' = @{
                    'Array Entry' = "@('Menu Option 1', 'Menu Option 2', 'Menu Option 3', 'Menu Option 4')"
                }
            }
 
            This example includes all possible entry types:
 
            Command Entry Invoke without opening as nested menu (does not contain any prefixes)
            Invoke Entry Invoke and open as nested menu (contains the "@" prefix)
            Hashtable Entry Opened as a nested menu
            Array Entry Opened as a nested menu
 
        .NOTES
            Write-SRxMenu by QuietusPlus (inspired by "Simple Textbased Powershell Menu" [Michael Albert])
 
        .LINK
            https://quietusplus.github.io/Write-Menu
 
        .LINK
            https://github.com/QuietusPlus/Write-Menu
    #>


    [CmdletBinding()]

    <#
        Parameters
    #>


    param(
        # Array or hashtable containing the menu entries
        [Parameter(Mandatory=$true, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('InputObject')]
        $Entries,

        # Title shown at the top of the menu.
        [Parameter(ValueFromPipelineByPropertyName = $true)]
        [Alias('Name')]
        [string]
        $Title,

        # Sort entries before they are displayed.
        [Parameter()]
        [switch]
        $Sort,

        # Select multiple menu entries using space, each selected entry will then get invoked (this will disable nested menu's).
        [Parameter()]
        [switch]
        $MultiSelect
    )

    <#
        Configuration
    #>


    # Entry prefix, suffix and padding
    $script:cfgPrefix = ' '
    $script:cfgPadding = 2
    $script:cfgSuffix = ' '
    $script:cfgNested = ' >'

    # Minimum page width
    $script:cfgWidth = 30

    # Hide cursor
    [System.Console]::CursorVisible = $false

    # Save initial colours
    $script:colorForeground = [System.Console]::ForegroundColor
    $script:colorBackground = [System.Console]::BackgroundColor

    <#
        Checks
    #>


    # Check if entries has been passed
    if ($Entries -like $null) {
        Write-Error "Missing -Entries parameter!"
        return
    }

    # Check if host is console
    if ($host.Name -ne 'ConsoleHost') {
        Write-Error "[$($host.Name)] Cannot run inside current host, please use a console window instead!"
        return
    }

    <#
        Set-Color
    #>


    function Set-Color ([switch]$Inverted) {
        switch ($Inverted) {
            $true {
                [System.Console]::ForegroundColor = $colorBackground
                [System.Console]::BackgroundColor = $colorForeground
            }
            Default {
                [System.Console]::ForegroundColor = $colorForeground
                [System.Console]::BackgroundColor = $colorBackground
            }
        }
    }

    <#
        Get-Menu
    #>


    function Get-Menu ($script:inputEntries) {
        # Clear console
        Clear-Host

        #Write-Host( $inputEntries | Format-List | Out-String)
        #Write-Host( "Type = " + $inputEntries.GetType().Name)
        #Read-Host

        # Check if -Title has been provided, if so set window title, otherwise set default.
        
        if ($Title -notlike $null) {
            #$host.UI.RawUI.WindowTitle = $Title
            $script:menuTitle = "$Title"
        } else {
            $script:menuTitle = 'Menu'
        }
        

        # Set menu height
        $script:pageSize = ($host.UI.RawUI.WindowSize.Height - 5)
        if($global:SRxEnv.SOP) {
            $script:pageSize = ($host.UI.RawUI.WindowSize.Height - 10)
        }

        # Convert entries to object
        $script:menuEntries = @()
        switch ($inputEntries.GetType().Name) {
            'String' {
                # Set total entries
                $script:menuEntryTotal = 1
                # Create object
                $script:menuEntries = New-Object PSObject -Property @{
                    Command = ''
                    Name = $inputEntries
                    Selected = $false
                    onConfirm = 'Name'
                }; break
            }
            'Object[]' {
                # Get total entries
                $script:menuEntryTotal = $inputEntries.Length
                # Loop through array
                foreach ($i in 0..$($menuEntryTotal - 1)) {

                    #$tempName = $($inputEntries.Keys)[$i]
                    $tempName = $($inputEntries)[$i]
                    $tempCommand = ''
                    $tempAction = 'Name'

                    #Write-Host( "(inputEntries)[i].GetType().Name = " + $($inputEntries)[$i].GetType().Name)
                    # Check if command contains nested menu
                    if ($($inputEntries)[$i].GetType().Name -eq 'PSCustomObject') {
                        $tempName = $($inputEntries)[$i].Title
                        $tempCommand = $($inputEntries)[$i].Menus
                        $tempAction = 'Hashtable'
                    #} elseif ($tempCommand.Substring(0,1) -eq '@') {
                    # $tempAction = 'Invoke'
                    } 
                    elseif ($($inputEntries)[$i].GetType().Name -eq 'String') {
                        $tempName = $($inputEntries)[$i]
                        $tempCommand = ''
                        $tempAction = 'Name'
                    }
                    else {
                        $tempCommand = '' #$tempName
                        #$tempAction = 'Command'
                        $tempAction = 'Name'
                    }

                    # Create object
                    $script:menuEntries += New-Object PSObject -Property @{
                        #Command = ''
                        #Name = $($inputEntries)[$i]
                        #Selected = $false
                        #onConfirm = 'Name'
                        Name = $tempName
                        Command = $tempCommand
                        Selected = $false
                        onConfirm = $tempAction
                    }; $i++
                }; 
#Write-Host "menuEntries"
#Write-Host ($script:menuEntries | Format-List | Out-String )
#Read-Host
                break
            }
            'Hashtable' {
                # Get total entries
                $script:menuEntryTotal = $inputEntries.Count
                # Loop through hashtable
                foreach ($i in 0..($menuEntryTotal - 1)) {
                    # Check if hashtable contains a single entry, copy values directly if true
                    if ($menuEntryTotal -eq 1) {
                        $tempName = $($inputEntries.Keys)
                        $tempCommand = $($inputEntries.Values)
                    } else {
                        $tempName = $($inputEntries.Keys)[$i]
                        $tempCommand = $($inputEntries.Values)[$i]
                    }

                    # Check if command contains nested menu
                    if ($tempCommand.GetType().Name -eq 'Hashtable') {
                        $tempAction = 'Hashtable'
                    } elseif ($tempCommand.Substring(0,1) -eq '@') {
                        $tempAction = 'Invoke'
                    } else {
                        $tempAction = 'Command'
                    }

                    # Create object
                    $script:menuEntries += New-Object PSObject -Property @{
                        Name = $tempName
                        Command = $tempCommand
                        Selected = $false
                        onConfirm = $tempAction
                    }; $i++
                }; break
            }
            Default {
                Write-Error "Type `"$($inputEntries.GetType().Name)`" not supported, please use an array or hashtable."
                exit
            }
        }

        # Sort entries
        if ($Sort -eq $true) {
            $script:menuEntries = $menuEntries | Sort-Object -Property Name
        }

        # Get longest entry
        $script:entryWidth = ($menuEntries.Name | Measure-Object -Maximum -Property Length).Maximum
        # Widen if -MultiSelect is enabled
        if ($MultiSelect) { $script:entryWidth += 4 }
        # Set minimum entry width
        if ($entryWidth -lt $cfgWidth) { $script:entryWidth = $cfgWidth }
        # Set page width
        $script:pageWidth = $cfgPrefix.Length + $cfgPadding + $entryWidth + $cfgPadding + $cfgSuffix.Length

        # Set current + total pages
        $script:pageCurrent = 0
        $script:pageTotal = [math]::Ceiling((($menuEntryTotal - $pageSize) / $pageSize))

        # Insert new line
        #[System.Console]::WriteLine("")

        if($global:SRxEnv.SOP) {
            Show-SRxSOPTitle
        }
        else {
            [System.Console]::WriteLine("")
        }

        # Save title line location + write title
        $script:lineTitle = [System.Console]::CursorTop
        [System.Console]::WriteLine(" $menuTitle" + "`n")

        # Save first entry line location
        $script:lineTop = [System.Console]::CursorTop
    }

    <#
        Get-Page
    #>


    function Get-Page {
        # Update header if multiple pages
        if ($pageTotal -ne 0) { Update-Header }

        # Clear entries
        for ($i = 0; $i -le $pageSize; $i++) {
            # Overwrite each entry with whitespace
            [System.Console]::WriteLine("".PadRight($pageWidth) + ' ')
        }

        # Move cursor to first entry
        [System.Console]::CursorTop = $lineTop

        # Get index of first entry
        $script:pageEntryFirst = ($pageSize * $pageCurrent)

        # Get amount of entries for last page + fully populated page
        if ($pageCurrent -eq $pageTotal) {
            $script:pageEntryTotal = ($menuEntryTotal - ($pageSize * $pageTotal))
        } else {
            $script:pageEntryTotal = $pageSize
        }

        # Set position within console
        $script:lineSelected = 0

        # Write all page entries
        for ($i = 0; $i -le ($pageEntryTotal - 1); $i++) {
            Write-Entry $i
        }
    }

    <#
        Write-Entry
    #>


    function Write-Entry ([int16]$Index, [switch]$Update) {
        # Check if entry should be highlighted
        switch ($Update) {
            $true { $lineHighlight = $false; break }
            Default { $lineHighlight = ($Index -eq $lineSelected) }
        }

        # Page entry name
        $pageEntry = $menuEntries[($pageEntryFirst + $Index)].Name

        # Prefix checkbox if -MultiSelect is enabled
        if ($MultiSelect) {
            switch ($menuEntries[($pageEntryFirst + $Index)].Selected) {
                $true { $pageEntry = "[X] $pageEntry"; break }
                Default { $pageEntry = "[ ] $pageEntry" }
            }
        }

        # Full width highlight + Nested menu indicator
        switch ($menuEntries[($pageEntryFirst + $Index)].onConfirm -in 'Hashtable', 'Invoke') {
            $true { $pageEntry = "$pageEntry".PadRight($entryWidth) + "$cfgNested"; break }
            Default { $pageEntry = "$pageEntry".PadRight($entryWidth + $cfgNested.Length) }
        }

        # Write new line and add whitespace without inverted colours
        [System.Console]::Write("`r" + $cfgPrefix)
        # Invert colours if selected
        if ($lineHighlight) { Set-Color -Inverted }
        # Write page entry
        [System.Console]::Write("".PadLeft($cfgPadding) + $pageEntry + "".PadRight($cfgPadding))
        # Restore colours if selected
        if ($lineHighlight) { Set-Color }
        # Entry suffix
        [System.Console]::Write($cfgSuffix + "`n")
    }

    <#
        Update-Entry
    #>


    function Update-Entry ([int16]$Index) {
        # Reset current entry
        [System.Console]::CursorTop = ($lineTop + $lineSelected)
        Write-Entry $lineSelected -Update

        # Write updated entry
        $script:lineSelected = $Index
        [System.Console]::CursorTop = ($lineTop + $Index)
        Write-Entry $lineSelected

        # Move cursor to first entry on page
        [System.Console]::CursorTop = $lineTop
    }

    <#
        Update-Header
    #>


    function Update-Header {
        # Set corrected page numbers
        $pCurrent = ($pageCurrent + 1)
        $pTotal = ($pageTotal + 1)

        # Calculate offset
        $pOffset = ($pTotal.ToString()).Length

        # Build string, use offset and padding to right align current page number
        $script:pageNumber = "{0,-$pOffset}{1,0}" -f "$("$pCurrent".PadLeft($pOffset))","/$pTotal"

        # Move cursor to title
        [System.Console]::CursorTop = $lineTitle
        # Move cursor to the right
        [System.Console]::CursorLeft = ($pageWidth - ($pOffset * 2) - 1)
        # Write page indicator
        [System.Console]::WriteLine("$pageNumber")
    }

    <#
        Initialisation
    #>


    # Get menu
    Get-Menu $Entries

    # Get page
    Get-Page

    # Declare hashtable for nested entries
    $menuNested = [ordered]@{}

    <#
        User Input
    #>


    # Loop through user input until valid key has been pressed
    do { $inputLoop = $true

        # Move cursor to first entry and beginning of line
        [System.Console]::CursorTop = $lineTop
        [System.Console]::Write("`r")

        # Get pressed key
        $menuInput = [System.Console]::ReadKey($false)

        # Define selected entry
        $entrySelected = $menuEntries[($pageEntryFirst + $lineSelected)]

        # Check if key has function attached to it
        switch ($menuInput.Key) {
            # Exit / Return
            { $_ -in 'Escape', 'Backspace' } {
                # Return to parent if current menu is nested
                if ($menuNested.Count -ne 0) {
                    $pageCurrent = 0
                    $Title = $($menuNested.GetEnumerator())[$menuNested.Count - 1].Name
                    Get-Menu $($menuNested.GetEnumerator())[$menuNested.Count - 1].Value
                    Get-Page
                    $menuNested.RemoveAt($menuNested.Count - 1) | Out-Null
                # Otherwise exit and return $null
                } else {
                    Clear-Host
                    $inputLoop = $false
                    [System.Console]::CursorVisible = $true
                    return $null
                }; break
            }

            # Next entry
            'DownArrow' {
                if ($lineSelected -lt ($pageEntryTotal - 1)) { # Check if entry isn't last on page
                    Update-Entry ($lineSelected + 1)
                } elseif ($pageCurrent -ne $pageTotal) { # Switch if not on last page
                    $pageCurrent++
                    Get-Page
                }; break
            }

            # Previous entry
            'UpArrow' {
                if ($lineSelected -gt 0) { # Check if entry isn't first on page
                    Update-Entry ($lineSelected - 1)
                } elseif ($pageCurrent -ne 0) { # Switch if not on first page
                    $pageCurrent--
                    Get-Page
                    Update-Entry ($pageEntryTotal - 1)
                }; break
            }

            # Select top entry
            'Home' {
                if ($lineSelected -ne 0) { # Check if top entry isn't already selected
                    Update-Entry 0
                } elseif ($pageCurrent -ne 0) { # Switch if not on first page
                    $pageCurrent--
                    Get-Page
                    Update-Entry ($pageEntryTotal - 1)
                }; break
            }

            # Select bottom entry
            'End' {
                if ($lineSelected -ne ($pageEntryTotal - 1)) { # Check if bottom entry isn't already selected
                    Update-Entry ($pageEntryTotal - 1)
                } elseif ($pageCurrent -ne $pageTotal) { # Switch if not on last page
                    $pageCurrent++
                    Get-Page
                }; break
            }

            # Next page
            { $_ -in 'RightArrow','PageDown' } {
                if ($pageCurrent -lt $pageTotal) { # Check if already on last page
                    $pageCurrent++
                    Get-Page
                }; break
            }

            # Previous page
            { $_ -in 'LeftArrow','PageUp' } { # Check if already on first page
                if ($pageCurrent -gt 0) {
                    $pageCurrent--
                    Get-Page
                }; break
            }

            # Select/check entry if -MultiSelect is enabled
            'Spacebar' {
                if ($MultiSelect) {
                    switch ($entrySelected.Selected) {
                        $true { $entrySelected.Selected = $false }
                        $false { $entrySelected.Selected = $true }
                    }
                    Update-Entry ($lineSelected)
                }; break
            }

            # Select all if -MultiSelect has been enabled
            'Insert' {
                if ($MultiSelect) {
                    $menuEntries | ForEach-Object {
                        $_.Selected = $true
                    }
                    Get-Page
                }; break
            }

            # Select none if -MultiSelect has been enabled
            'Delete' {
                if ($MultiSelect) {
                    $menuEntries | ForEach-Object {
                        $_.Selected = $false
                    }
                    Get-Page
                }; break
            }

            # Confirm selection
            'Enter' {
                # Check if -MultiSelect has been enabled
                if ($MultiSelect) {
                    Clear-Host
                    # Process checked/selected entries
                    $menuEntries | ForEach-Object {
                        # Entry contains command, invoke it
                        if (($_.Selected) -and ($_.Command -notlike $null) -and ($entrySelected.Command.GetType().Name -ne 'Hashtable')) {
                            Invoke-Expression -Command $_.Command
                        # Return name, entry does not contain command
                        } elseif ($_.Selected) {
                            return $_.Name
                        }
                    }
                    # Exit and re-enable cursor
                    $inputLoop = $false
                    [System.Console]::CursorVisible = $true
                    break
                }

                # Use onConfirm to process entry
#write-host (" onConfirm =" + $entrySelected.onConfirm)
#read-host
                switch ($entrySelected.onConfirm) {
                    # Return hashtable as nested menu
                    'Hashtable' {
                        $menuNested.$Title = $inputEntries
                        $Title = $entrySelected.Name
                        Get-Menu $entrySelected.Command
                        Get-Page
                        break
                    }

                    # Invoke attached command and return as nested menu
                    'Invoke' {
                        $menuNested.$Title = $inputEntries
                        $Title = $entrySelected.Name
                        Get-Menu $(Invoke-Expression -Command $entrySelected.Command.Substring(1))
                        Get-Page
                        break
                    }

                    # Invoke attached command and exit
                    'Command' {
                        Clear-Host
                        Invoke-Expression -Command $entrySelected.Command
                        $inputLoop = $false
                        [System.Console]::CursorVisible = $true
                        break
                    }

                    # Return name and exit
                    'Name' {
                        Clear-Host
                        return $entrySelected.Name
                        $inputLoop = $false
                        [System.Console]::CursorVisible = $true
                    }
                }
            }
        }
    } while ($inputLoop)
}
#EndRegion '.\Public\Write-SRxMenu.ps1' 641