HP.Repo.psm1

#
# Copyright 2018-2022 HP Development Company, L.P.
# All Rights Reserved.
#
# NOTICE: All information contained herein is, and remains the property of HP Development Company, L.P.
#
# The intellectual and technical concepts contained herein are proprietary to HP Development Company, L.P
# and may be covered by U.S. and Foreign Patents, patents in process, and are protected by
# trade secret or copyright law. Dissemination of this information or reproduction of this material
# is strictly forbidden unless prior written permission is obtained from HP Development Company, L.P.

Set-StrictMode -Version 3.0
#requires -Modules "HP.Private","HP.Softpaq"

$RepositoryType = @"
public enum ErrorHandling {
  Fail = 0,
  LogAndContinue = 1
};
 
public class SoftpaqRepositoryFile {
  public class SoftpaqRepositoryFilter {
      public string platform;
      public string operatingSystem;
      public string category;
      public string releaseType;
      public string characteristic;
      public bool? preferLTSC;
  };
 
  public class NotificationConfiguration {
      public string server;
      public int port;
        public bool tls;
        public string[] addresses;
        public string username;
        public string password;
        public string from;
        public string fromname;
  };
   
  public class Configuration {
    public ErrorHandling OnRemoteFileNotFound;
    public int ExclusiveLockMaxRetries;
    public string OfflineCacheMode;
    public string RepositoryReport;
    }
 
 
    public string DateCreated;
    public string DateLastModified;
    public string CreatedBy;
    public string ModifiedBy;
  public SoftpaqRepositoryFilter[] Filters;
  public NotificationConfiguration Notifications;
  public Configuration Settings;
 
 
}
"@


$REPOFILE = ".repository/repository.json"
$LOGFILE = ".repository/activity.log"

Add-Type -TypeDefinition $RepositoryType

# print a bare error
function err
{
  [CmdletBinding()]
  param(
    [string]$str,
    [boolean]$withLog = $true
  )

  [console]::ForegroundColor = 'red'
  [console]::Error.WriteLine($str)
  [console]::ResetColor()

  if ($withLog) { Write-LogError -Message $str -Component "HP.Repo" -File $LOGFILE }
}

# convert a date object to an 8601 string
function ISO8601DateString
{
  [CmdletBinding()]
  param(
    [datetime]$Date
  )
  $Date.ToString("yyyy-MM-dd'T'HH:mm:ss.fffffff",[System.Globalization.CultureInfo]::InvariantCulture)
}

# get current user name
function GetUserName ()
{
  [CmdletBinding()]
  param()

  try {
    [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
  }
  catch {
    return $env:username
  }
}

# check if a file exists
function FileExists
{
  [CmdletBinding()]
  param(
    [string]$File
  )
  Test-Path $File -PathType Leaf
}

# load a json object
function LoadJson
{
  [CmdletBinding()]
  param(
    [string]$File
  )

  try {
    $PS7Mark = "PS7Mark"
    $rawData = (Get-Content -Raw -Path $File) -replace '("DateLastModified": ")([^"]+)(")',('$1' + $PS7Mark + '$2' + $PS7Mark + '$3')
    [SoftpaqRepositoryFile]$result = $rawData | ConvertFrom-Json
    $result.DateLastModified = $result.DateLastModified -replace $PS7Mark,""
    return $result
  }
  catch
  {
    err ("Could not parse '$File' $($_.Exception.Message)")
    return $Null
  }
}

# load a repository definition file
function LoadRepository
{
  [CmdletBinding()]
  param()

  Write-Verbose "loading $REPOFILE"
  $inRepo = FileExists -File $REPOFILE
  if (-not $inRepo) {
    throw [System.Management.Automation.ItemNotFoundException]"Directory '$(Get-Location)' is not a repository."
  }

  $repo = LoadJson -File $REPOFILE
  if (-not $repo -eq $null)
  {
    err ("Could not initialize the repository: $($_.Exception.Message)")
    return $false,$null
  }

  if (-not $repo.Filters) { $repo.Filters = @() }

  if (-not $repo.settings) {
    $repo.settings = New-Object SoftpaqRepositoryFile+Configuration
  }

  if (-not $repo.settings.OnRemoteFileNotFound) {
    $repo.settings.OnRemoteFileNotFound = [ErrorHandling]::Fail
  }

  if (-not $repo.settings.ExclusiveLockMaxRetries) {
    $repo.settings.ExclusiveLockMaxRetries = 10
  }

  if (-not $repo.settings.OfflineCacheMode) {
    $repo.settings.OfflineCacheMode = "Disable"
  }

  if (-not $repo.settings.RepositoryReport) {
    $repo.settings.RepositoryReport = "CSV"
  }

  foreach ($filter in $repo.Filters)
  {
    if (-not $filter.characteristic)
    {
      $filter.characteristic = "*"
    }
    if (-not $filter.preferLTSC)
    {
      $filter.preferLTSC = $false
    }
  }

  if (-not $repo.Notifications) {
    $repo.Notifications = New-Object SoftpaqRepositoryFile+NotificationConfiguration
    $repo.Notifications.port = 25
    $repo.Notifications.tls = $false
    $repo.Notifications.UserName = ""
    $repo.Notifications.Password = ""
    $repo.Notifications.from = "softpaq-repo-sync@$($env:userdnsdomain)"
    $repo.Notifications.fromname = "Softpaq Repository Notification"
  }

  Write-Verbose "load success"
  return $true,$repo
}

# download a SoftPaq, optionally checking existing SoftPaqs. Note that CVAs are always
# downloaded since there is no reliable way to check their consistency.

function DownloadSoftpaq
{
  [CmdletBinding()]
  param(
    $DownloadSoftpaqCmd,
    [int]$MaxRetries = 10
  )
  $download_file = $true
  $filename = "sp" + $DownloadSoftpaqCmd.number + ".exe"
  $CVAname = "sp" + $DownloadSoftpaqCmd.number + ".cva"

  # downloading the CVA
  Write-Verbose ("Downloading CVA $($DownloadSoftpaqCmd.number)")
  Log (" sp$($DownloadSoftpaqCmd.number).cva - Downloading CVA file.")
  Get-SoftpaqMetadataFile @DownloadSoftpaqCmd -MaxRetries $MaxRetries
  Log (" sp$($DownloadSoftpaqCmd.number).cva - Done downloading CVA file.")

  if (FileExists -File $filename) {
    Write-Verbose "Checking signature for existing file $filename"
    if (Get-HPPrivateCheckSignature -File $filename -CVAfile $CVAname -Verbose:$VerbosePreference -Progress:(-not $DownloadSoftpaqCmd.Quiet)) {

      if (-not $DownloadSoftpaqCmd.Quiet) {
        Write-Host -ForegroundColor Magenta "File $filename already exists and passes signature check. It will not be redownloaded."
      }
      Log (" sp$($DownloadSoftpaqCmd.number).exe - Already exists. Will not redownload.")
      $download_file = $false
    }
    else {
      Write-Verbose ("Need to redownload file '$filename'")
    }
  }
  else {
    Write-Verbose ("Need to download file '$filename'")
  }


  if ($download_file -eq $true) {
    try {
      Log (" sp$($DownloadSoftpaqCmd.number).exe - Downloading EXE file.")
      Get-Softpaq @DownloadSoftpaqCmd -MaxRetries $MaxRetries -Overwrite yes
      # check post-download integrity
      if (-not (Get-HPPrivateCheckSignature -File $filename -CVAfile $CVAname -Verbose:$VerbosePreference -Progress:(-not $DownloadSoftpaqCmd.Quiet))) {
        Remove-Item -Path $filename -Force -Verbose:$VerbosePreference
        Remove-Item -Path $CVAName -Force -Verbose:$VerbosePreference
        $msg = "File $filename failed integrity check and has been deleted, will retry download next sync"
        if (-not $DownloadSoftpaqCmd.Quiet) {
          Write-Host -ForegroundColor Magenta $msg
        }
        Write-LogWarning -Message $msg -Component "HP.Repo" -File $LOGFILE

      }
      Log (" sp$($DownloadSoftpaqCmd.number).exe - Done downloading EXE file.")
    }
    catch {
      Write-Host -ForegroundColor Magenta "File sp$($DownloadSoftpaqCmd.number).exe has invalid or missing signature and will be deleted."
      Log (" sp$($DownloadSoftpaqCmd.number).exe has invalid or missing signature and will be deleted.")
      Log (" sp$($DownloadSoftpaqCmd.number).exe - Redownloading EXE file.")
      Get-Softpaq @DownloadSoftpaqCmd -maxRetries $maxRetries
      Log (" sp$($DownloadSoftpaqCmd.number).exe - Done downloading EXE file.")
    }
  }
}

# write a repository definition file
function WriteRepositoryFile
{
  [CmdletBinding()]
  param($obj)

  $now = Get-Date
  $obj.DateLastModified = ISO8601DateString -Date $now
  $obj.ModifiedBy = GetUserName
  Write-Verbose "Writing repository file to $REPOFILE"
  $obj | ConvertTo-Json | Out-File -Force $REPOFILE
}

# check if a filter exists in a repo object
function FilterExists
{
  [CmdletBinding()]
  param($repo,$f)

  $c = getFilters $repo $f
  return ($null -ne $c)
}

# get a list of filters in a repo, matching exact parameters
function getFilters
{
  [CmdletBinding()]
  param($repo,$f)

  if ($repo.Filters.Count -eq 0) { return $null }
  $repo.Filters | Where-Object {
    $_.platform -eq $f.platform -and
    $_.OperatingSystem -eq $f.OperatingSystem -and
    $_.Category -eq $f.Category -and
    $_.ReleaseType -eq $f.ReleaseType -and
    $_.characteristic -eq $f.characteristic -and
    $_.preferLTSC -eq $f.preferLTSC
  }
}

# get a list of filters in a repo, considering empty parameters as wildcards
function GetFiltersWild
{
  [CmdletBinding()]
  param($repo,$f)

  if ($repo.Filters.Count -eq 0) { return $null }
  $repo.Filters | Where-Object {
    $_.platform -eq $f.platform -and
    (
      $_.OperatingSystem -eq $f.OperatingSystem -or
      $f.OperatingSystem -eq "*" -or
      ($f.OperatingSystem -eq "win10:*" -and $_.OperatingSystem.StartsWith("win10")) -or
      ($f.OperatingSystem -eq "win11:*" -and $_.OperatingSystem.StartsWith("win11"))
    ) -and
    ($_.Category -eq $f.Category -or $f.Category -eq "*") -and
    ($_.ReleaseType -eq $f.ReleaseType -or $f.ReleaseType -eq "*") -and
    ($_.characteristic -eq $f.characteristic -or $f.characteristic -eq "*") -and
    ($_.preferLTSC -eq $f.preferLTSC -or $null -eq $f.preferLTSC)
  }
}

# write a log entry to the .repository/activity.log
function Log
{
  [CmdletBinding()]
  param([string[]]$entryText)

  foreach ($line in $entryText)
  {
    if (-not $line) {
      $line = " "
    }
    Write-LogInfo -Message $line -Component "HP.Repo" -File $LOGFILE
  }

}

# touch a file (change its date if exists, or create it if it doesn't.
function TouchFile
{
  [CmdletBinding()]
  param([string]$File)

  if (Test-Path $File) { (Get-ChildItem $File).LastWriteTime = Get-Date }
  else { Write-Output $null > $File }
}


# remove all marks from the repository
function FlushMarks
{
  [CmdletBinding()]
  param()

  Write-Verbose "Removing all marks"
  Remove-Item ".repository\mark\*" -Include "*.mark"
}


# send a notification email
function Send
{
  [CmdletBinding()]
  param(
    $subject,
    $body,
    $html = $true
  )

  $n = Get-RepositoryNotificationConfiguration
  if ((-not $n) -or (-not $n.server)) {
    Write-Verbose ("Notifications are not configured")
    return
  }

  try {
    if ((-not $n.addresses) -or (-not $n.addresses.Count))
    {
      Write-Verbose ("Notifications have no recipients defined")
      return
    }
    Log ("Sending a notification email")

    $params = @{}
    $params.To = $n.addresses
    $params.SmtpServer = $n.server
    $params.port = $n.port
    $params.UseSsl = $n.tls
    $params.from = "$($n.fromname) <$($n.from)>"
    $params.Subject = $subject
    $params.Body = $body
    $params.BodyAsHtml = $html

    Write-Verbose ("server: $($params.SmtpServer)")
    Write-Verbose ("port: $($params.Port)")

    if ([string]::IsNullOrEmpty($n.UserName) -eq $false)
    {
      try {
        [SecureString]$read = $n.Password | ConvertTo-SecureString
        $params.Credential = New-Object System.Management.Automation.PSCredential ($n.UserName,$read)
        if (-not $params.Credential) {
          Log ("Could not build credential object from username and password")
          return;
        }
      }
      catch {
        err ("Failed to build credential object from username and password: $($_.Exception.Message)")
        return
      }
    }
    Send-MailMessage @params -ErrorAction Stop
  }
  catch {
    err ("Could not send email: $($_.Exception.Message)")
    return
  }
  Write-Verbose ("Send complete.")
}

<#
.SYNOPSIS
    Initialize a repository in the current directory.
 
.DESCRIPTION
  This command initializes a directory to be used as a repository. It creates a .repository folder in the current directory,
  which contains the definition of the .repository and all its settings.
 
  In order to un-initalize a directory, simple remove the .repository folder.
 
  After initializing a repository, you must add at least one filter to define the content that this repository will receive.
 
  If the directory already contains a repository, the command will fail.
 
.EXAMPLE
    Initialize-Repository
 
.LINK
  [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryConfiguration](Get-RepositoryConfiguration)
 
.LINK
  [Set-RepositoryConfiguration](Set-RepositoryConfiguration)
#>

function Initialize-Repository
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Initialize-Repository")]
  param()

  if (FileExists -File $REPOFILE) {
    err "This directory is already initialized as a repository."
    return
  }
  $now = Get-Date
  $newRepositoryFile = New-Object SoftpaqRepositoryFile

  $newRepositoryFile.settings = New-Object SoftpaqRepositoryFile+Configuration
  $newRepositoryFile.settings.OnRemoteFileNotFound = [ErrorHandling]::Fail
  $newRepositoryFile.settings.ExclusiveLockMaxRetries = 10
  $newRepositoryFile.settings.OfflineCacheMode = "Disable"
  $newRepositoryFile.settings.RepositoryReport = "CSV"

  $newRepositoryFile.DateCreated = ISO8601DateString -Date $now
  $newRepositoryFile.CreatedBy = GetUserName

  try {
    New-Item -ItemType directory -Path .repository | Out-Null
    WriteRepositoryFile -obj $newRepositoryFile
    New-Item -ItemType directory -Path ".repository/mark" | Out-Null
  }
  catch {
    err ("Could not initialize the repository: $($_.Exception.Message)")
    return
  }
  Log "Repository initialized successfully."
}

<#
.SYNOPSIS
    Add a filter to the repository.
 
.DESCRIPTION
  This function adds a filter to a repository, which was previously initialized by Initialize-Repository.
 
  The repository can contain one or more filters, and the effective filtering will be the sum of all filters defined.
 
.PARAMETER Platform
  Specifies the given platform as a platform to include in this repository. This is a platform ID, a 4-digit hexadecimal number, as obtained by Get?HPDeviceProductID.
 
.PARAMETER Os
  Specifies the operating system to be include in this repository. The field must be one of "win10" or "win11". If this parameter is not specified, all operating systems associated with the specified platform will be included.
 
.PARAMETER OsVer
  Specify the target OS Version (e.g. "1809", "1903", "1909", "2004", "2009", "21H1", "21H2", "22H2" etc). Starting from 21H1 release, "xxHx" format is expected. If the parameter is not specified, current operating system version will be assumed, which may not be what is intended.
 
.PARAMETER Category
  Specifies the SoftPaq category to be include in this repository. The category must be one (or more) of "Bios", "Firmware", "Driver", "Software", "OS", "Manageability", "Diagnostic", "Utility", "Driverpack", "Dock", "UWPPack".
  If this parameter is not specified, all categories are included.
 
.PARAMETER ReleaseType
  Specifies the SoftPaq release type to be include in this repository. The release type must be one (or more) of "critical", "recommended", "routine". If this parameter is not specified, all release types are included.
 
.PARAMETER Characteristic
  Specifies the SoftPaq characteristic to be include in this repository. The characteristic must be one of "ssm", "dpb", "uwp". If this parameter is not specified, all characteristics are included.
 
.PARAMETER PreferLTSC
  If specified, LTSB/LTSC reference file is preferred for the entered Platform. If the LTSB/LTSC reference file doesn't exist, falls back to regular reference file for that platform.
 
.EXAMPLE
  Add-RepositoryFilter -Platform 1234 -Os win10 -OsVer 2009
 
.EXAMPLE
  Add-RepositoryFilter -Platform 1234 -Os win10 -OsVer "21H1"
 
.EXAMPLE
  Add-RepositoryFilter -Platform 1234 -Os win10 -OsVer "21H1" -PreferLTSC
 
.LINK
  [Initialize-Repository](Initialize-Repository)
 
.LINK
  [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
  [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
  [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
.LINK
  [Get-HPDeviceProductID](Get-HPDeviceProductID)
#>

function Add-RepositoryFilter
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Add-RepositoryFilter")]
  param(
    [ValidatePattern("^[a-fA-F0-9]{4}$")]
    [Parameter(Position = 0,Mandatory = $true)]
    [string]$Platform,

    [ValidateSet("win7","win8","win8.1","win81","win10","win11","*")] # keep in sync with the SoftPaq module
    [Parameter(Position = 1)] $Os = "*",
    [string[]]

    [ValidateSet("1809","1903","1909","2004","2009","21H1","21H2","22H2")] # keep in sync with the SoftPaq module
    [Parameter(Position = 1)]
    [string]$OsVer,

    [ValidateSet("Bios","Firmware","Driver","Software","Os","Manageability","Diagnostic","Utility","Driverpack","Dock","UWPPack","*")] # keep in sync with the SoftPaq module
    [Parameter(Position = 2)]
    [string[]]$Category = "*",

    [ValidateSet("Critical","Recommended","Routine","*")] # keep in sync with the SoftPaq module
    [Parameter(Position = 3)]
    [string[]]$ReleaseType = "*",

    [ValidateSet("SSM","DPB","UWP","*")] # keep in sync with the SoftPaq module
    [Parameter(Position = 4)]
    [string[]]$Characteristic = "*",

    [Parameter(Position = 5, Mandatory = $false)]
    [switch]$PreferLTSC
  )

  $c = LoadRepository
  try {
    if ($c[0] -eq $false) { return }
    $repo = $c[1]

    $newFilter = New-Object SoftpaqRepositoryFile+SoftpaqRepositoryFilter
    $newFilter.platform = $Platform

    $newFilter.OperatingSystem = $Os
    if (-not $OsVer)
    {
      $OsVer = GetCurrentOSVer
    }
    if ($OsVer) { $OsVer = $OsVer.ToLower() }
    if ($Os -eq "win10") { $newFilter.OperatingSystem = "win10:$OsVer" }
    elseif ($Os -eq "win11") { $newFilter.OperatingSystem = "win11:$OsVer" }

    $newFilter.Category = $Category
    $newFilter.ReleaseType = $ReleaseType
    $newFilter.characteristic = $Characteristic
    $newFilter.preferLTSC = $PreferLTSC.IsPresent

    # silently ignore if the filter is already in the repo
    $exists = filterExists $repo $newFilter
    if (!$exists) {
      $repo.Filters += $newFilter
      WriteRepositoryFile -obj $repo
      if ($OsVer -and $Os -ne '*') { Log "Added filter $Platform {{ os='$Os', osver='$OsVer', category='$Category', release='$ReleaseType', characteristic='$Characteristic', preferLTSC='$($PreferLTSC.IsPresent)' }}" }
      else { Log "Added filter $Platform {{ os='$Os', category='$Category', release='$ReleaseType', characteristic='$Characteristic', preferLTSC='$($PreferLTSC.IsPresent)' }}" }
    }
    else
    {
      Write-Verbose "Silently ignoring this filter since exact match is already in the repository"
    }
    Write-Verbose "Repository filter added."
  }
  catch
  {
    err ("Could not add filter to the repository: $($_.Exception.Message)")
  }
}


<#
.SYNOPSIS
  Remove one or more filters from the repository.
 
.DESCRIPTION
  This function modifies the repository to remove filters from the repository definition.
 
  If an optional parameter is not specified, it will be considered a wildcard and match any value. Therefore this command may result in multiple filters being deleted.
 
.PARAMETER Platform
  The platform to remove. This is a 4-digit hex number, and can be obtained via Get-HPDeviceProductID
 
.PARAMETER Os
  An optional parameter to narrow down the filter to a specific OS for the specified platform. If not specified, all OS will be matched.
 
.PARAMETER OsVer
  Specify the target OS Version (e.g. "1809", "1903", "1909", "2004", "2009", "21H1", "21H2", "22H2" etc). Starting from 21H1 release, "xxHx" format is expected. If the parameter is not specified, current operating system version will be assumed, which may not be what is intended.
 
.PARAMETER Category
  An optional parameter to narrow down the filter to a specific category for the specified platform. If not specified, all categories will be matched.
 
.PARAMETER ReleaseType
  An optional parameter to narrow down the filter to a specific release type for the specified platform. If not specified, all release types will be matched.
 
.PARAMETER Characteristic
  An optional parameter to narrow down the filter to a specific characteristic for the specified platform. If not specified, all characteristics will be matched.
 
.PARAMETER PreferLTSC
  An optional boolean parameter to narrow down the filter to a specific LTSB/LTSC reference file preference for the specified platform. If not specified, all preferences will be matched.
 
.PARAMETER Yes
  An optional parameter to provide. If not specified, script will ask for confirmation before deleting a filter. If specified, script will go ahead and delete the filter without confirming.
 
.EXAMPLE
  Remove-RepositoryFilter -Platform 1234
 
.EXAMPLE
  Remove-RepositoryFilter -Platform 1234 -Os win10 -OsVer "21H1"
 
.EXAMPLE
  Remove-RepositoryFilter -Platform 1234 -Os win10 -OsVer "21H1" -PreferLTSC $True
 
.LINK
  [Initialize-Repository](Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
  [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
  [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Get-HPDeviceProductID](Get-HPDeviceProductID)
 
.LINK
  [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
#>

function Remove-RepositoryFilter
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Remove-RepositoryFilter")]
  param(
    [ValidatePattern("^[a-fA-F0-9]{4}$")]
    [Parameter(Position = 0,Mandatory = $true)]
    [string]$Platform,

    [ValidateSet("win7","win8","win8.1","win81","win10","win11","*")] # keep in sync with the SoftPaq module
    [string[]]
    [Parameter(Position = 1)]
    $Os = "*",

    [ValidateSet("1809","1903","1909","2004","2009","21H1","21H2","22H2")] # keep in sync with the SoftPaq module
    [Parameter(Position = 1)]
    [string]$OsVer,

    [ValidateSet("Bios","Firmware","Driver","Software","Os","Manageability","Diagnostic","Utility","Driverpack","Dock","UWPPack","*")] # keep in sync with the SoftPaq module
    [string[]]
    [Parameter(Position = 2)]
    $Category = "*",

    [ValidateSet("Critical","Recommended","Routine","*")] # keep in sync with the SoftPaq module
    [string[]]
    [Parameter(Position = 3)]
    $ReleaseType = "*",

    [Parameter(Position = 4,Mandatory = $false)]
    [switch]$Yes = $false,

    [ValidateSet("SSM","DPB","UWP","*")] # keep in sync with the SoftPaq module
    [string[]]
    [Parameter(Position = 5)]
    $Characteristic = "*",

    [Parameter(Position = 5, Mandatory = $false)]
    [nullable[boolean]]$PreferLTSC = $null
  )

  $c = LoadRepository
  try {
    if ($c[0] -eq $false) { return }

    $newFilter = New-Object SoftpaqRepositoryFile+SoftpaqRepositoryFilter
    $newFilter.platform = $Platform
    $newFilter.OperatingSystem = $Os

    if ($Os -eq "win10") {
      if ($OsVer) { $newFilter.OperatingSystem = "win10:$OsVer" }
      else { $newFilter.OperatingSystem = "win10:*" }
    }
    elseif ($Os -eq "win11") {
      if ($OsVer) { $newFilter.OperatingSystem = "win11:$OsVer" }
      else { $newFilter.OperatingSystem = "win11:*" }
    }

    $newFilter.Category = $Category
    $newFilter.ReleaseType = $ReleaseType
    $newFilter.characteristic = $Characteristic
    $newFilter.preferLTSC = $PreferLTSC

    $todelete = getFiltersWild $c[1] $newFilter
    if (-not $todelete) {
      Write-Verbose ("No matching filter to delete")
      return
    }

    if (-not $Yes.IsPresent) {
      Write-Host "The following filters will be deleted:" -ForegroundColor Cyan
      $todelete | ConvertTo-Json -Depth 2 | Write-Host -ForegroundColor Cyan
      $answer = Read-Host "Enter 'y' to continue: "
      if ($answer -ne "y") {
        Write-Host 'Aborted.'
        return }
    }

    $c[1].Filters = $c[1].Filters | Where-Object { $todelete -notcontains $_ }
    WriteRepositoryFile -obj $c[1]

    foreach ($f in $todelete) {
      Log "Removed filter $($f.platform) { os='$($f.operatingSystem)', category='$($f.category)', release='$($f.releaseType), characteristic='$($f.characteristic)' }"
    }
  }
  catch
  {
    err ("Could not remove filter from repository: $($_.Exception.Message)")
  }
}

<#
.SYNOPSIS
    Show the current repository definition.
 
.DESCRIPTION
  Get the repository definition as an object. This command must be run inside an initialized repository.
   
.EXAMPLE
    $myrepository = Get-RepositoryInfo
     
.LINK
  [Initialize-Repository](Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
  [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
#>

function Get-RepositoryInfo ()
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Get-RepositoryInfo")]
  param()

  $c = LoadRepository
  try {
    if (-not $c[0]) { return }
    $c[1]
  }
  catch
  {
    err ("Could not get repository info: $($_.Exception.Message)")
  }
}

<#
.SYNOPSIS
  Synchronize repository and generate a report with repository content information
 
.DESCRIPTION
  This command performs a synchronization of a repository, by downloading latest SoftPaqs associated with the repository filters. At the end of each sync, it creates a repository report in a format (default .CSV) set via Set-RepositoryConfiguration.
 
  The command may be scheduled via task manager to run on schedule. Define a notification email via Set-RepositoryNotificationConfiguration to receive any failure notifications during unattended operation.
 
  This command may be followed by Invoke-RepositoryCleanup to remove any obsolete SoftPaqs from the repository.
 
  Invoke-RepositorySync functionality is not supported in WinPE.
 
.PARAMETER Quiet
  Suppress progress messages during operation.
 
.PARAMETER ReferenceUrl
  Specify an alternate location for the HP Image Assistant (HPIA) Reference files. This URL must be http or https. The Reference files are expected to be at the location pointed to by this URL inside a directory named after the platform ID you want a SoftPaq list for. If not specified, ftp.hp.com is used via HTTPS protocol.
 
  For example, If you want to point to 83b2 Win10 OSVer 2009 reference files, Get-SoftpaqList will try to find them in this directory structure: $ReferenceUrl/83b2/83b2_64_10.0.2009.cab
 
.EXAMPLE
  Invoke-RepositorySync -Quiet
 
.LINK
  [Initialize-Repository](Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
#>

function Invoke-RepositorySync
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Invoke-RepositorySync")]
  param(
    [Parameter(Position = 0,Mandatory = $false)]
    [switch]$Quiet = $false,

    [Alias('Url')]
    [Parameter(Position = 1,Mandatory = $false)]
    [string]$ReferenceUrl = "https://hpia.hpcloud.hp.com/ref"
  )

  $repo = LoadRepository
  try {
    $cwd = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath((Get-Location))
    $cacheDir = Join-Path -Path $cwd -ChildPath ".repository"
    $cacheDirOffline = $cacheDir + "\cache\offline"
    $reportDir = $cacheDir

    # return if repository is not initialized
    if ($repo[0] -eq $false) { return }

    # return if repository is initialized but no filters added
    $filters = $repo[1].Filters
    if ($filters.Count -eq 0) {
      Write-Verbose "Repository has no filters defined - terminating."
      Write-Verbose ("Flushing the list of markers")
      FlushMarks
      return
    }

    $platformGroups = $filters | Group-Object -Property platform
    $normalized = @()

    foreach ($pobj in $platformGroups)
    {

      $items = $pobj.Group

      if ($items | Where-Object -Property operatingSystem -EQ -Value "*") {
        $items | ForEach-Object { $_.OperatingSystem = "*" }
      }

      if ($items | Where-Object -Property category -EQ -Value "*") {
        $items | ForEach-Object { $_.Category = "*" }
      }

      if ($items | Where-Object -Property releaseType -EQ -Value "*") {
        $items | ForEach-Object { $_.ReleaseType = "*" }
      }

      if ($items | Where-Object -Property characteristic -EQ -Value "*") {
        $items | ForEach-Object { $_.characteristic = "*" }
      }

      $normalized += $items | sort -Unique -Property operatingSystem,category,releaseType,characteristic
    }

    $softpaqlist = @()
    Log "Repository sync has started"
    $softpaqListCmd = @{}


    # build the list of SoftPaqs to download
    foreach ($c in $normalized) {
      Write-Verbose ($c | Format-List | Out-String)

      if (Get-HPDeviceDetails -Platform $c.platform -Url $ReferenceUrl)
      {
        $softpaqListCmd.platform = $c.platform.ToLower()
        $softpaqListCmd.Quiet = $Quiet
        $softpaqListCmd.verbose = $VerbosePreference

        Write-Verbose ("Working on a rule for platform $($softpaqListCmd.platform)")

        if ($c.OperatingSystem.StartsWith("win10:"))
        {
          $split = $c.OperatingSystem -split ':'
          $softpaqListCmd.OS = $split[0]
          $softpaqListCmd.osver = $split[1]
        }
        elseif ($c.OperatingSystem -eq "win10")
        {
          $softpaqListCmd.OS = "win10"
          $softpaqListCmd.osver = GetCurrentOSVer
        }
        elseif ($c.OperatingSystem.StartsWith("win11:"))
        {
          $split = $c.OperatingSystem -split ':'
          $softpaqListCmd.OS = $split[0]
          $softpaqListCmd.osver = $split[1]
        }
        elseif ($c.OperatingSystem -eq "win11")
        {
          $softpaqListCmd.OS = "win11"
          $softpaqListCmd.osver = GetCurrentOSVer
        }
        elseif ($c.OperatingSystem -ne "*")
        {
          $softpaqListCmd.OS = $c.OperatingSystem
          #$softpaqListCmd.osver = $null
        }

        if ($c.characteristic -ne "*")
        {
          $softpaqListCmd.characteristic = $c.characteristic.ToUpper().Split()
          Write-Verbose "Filter-characteristic:$($softpaqListCmd.characteristic)"
        }

        if ($c.ReleaseType -ne "*")
        {
          $softpaqListCmd.ReleaseType = $c.ReleaseType.Split()
          Write-Verbose "Filter-releaseType:$($softpaqListCmd.releaseType)"
        }
        if ($c.Category -ne "*")
        {
          $softpaqListCmd.Category = $c.Category.Split()
          Write-Verbose "Filter-category:$($softpaqListCmd.category)"
        }
        if ($c.preferLTSC -eq $true)
        {
          $softpaqListCmd.PreferLTSC = $true
          Write-Verbose "Filter-preferLTSC:$($softpaqListCmd.PreferLTSC)"
        }

        Log "Reading the softpaq list for platform $($softpaqListCmd.platform)"
        Write-Verbose "Trying to get SoftPaqs from $ReferenceUrl"
        $results = Get-SoftpaqList @softpaqListCmd -cacheDir $cacheDir -maxRetries $repo[1].settings.ExclusiveLockMaxRetries -ReferenceUrl $ReferenceUrl -AddHttps
        Log "softpaq list for platform $($softpaqListCmd.platform) created"
        $softpaqlist += $results


        $OfflineCacheMode = $repo[1].settings.OfflineCacheMode
        if ($OfflineCacheMode -eq "Enable") {

          # keep the download order of PlatformList, Advisory data and Knowledge Base as is to maintain unit tests
          if (-not $ReferenceUrl.EndsWith('/')) {
            $ReferenceUrl = $ReferenceUrl + "/"
          }
          $baseurl = $ReferenceUrl
          $url = $baseurl + "platformList.cab"
          $filename = "platformList.cab"
          Write-Verbose "Trying to download PlatformList... $url"
          try {
            $PlatformList = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirOffline -Expand
            Write-Verbose "Finish downloading PlatformList - $PlatformList"
          }
          catch {
            $url = "https://ftp.hp.com/pub/caps-softpaq/cmit/imagepal/ref/platformList.cab"
            Write-Verbose "Trying to download PlatformList from FTP... $url"
            $PlatformList = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirOffline -Expand
            if (-not $PlatformList) {
              $exception = $_.Exception
              switch ($repo[1].settings.OnRemoteFileNotFound) {
                "LogAndContinue" {
                  [string]$data = formatSyncErrorMessageAsHtml $exception
                  Log ($data -split "`n")
                  send "Softpaq repository synchronization error" $data
                }
                # "Fail"
                default {
                  throw $exception
                }
              }
            }
          }

          # download Advisory data
          $url = $baseurl + "$($softpaqListCmd.platform)/$($softpaqListCmd.platform)_cds.cab"
          $cacheDirAdvisory = $cacheDirOffline + "\$($softpaqListCmd.platform)"
          $filename = "$($softpaqListCmd.platform)_cds.cab"
          Write-Verbose "Trying to download Advisory Data Files... $url"
          try {
            $AdvisoryFile = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirAdvisory -Expand
            Write-Verbose "Finish downloading Advisory Data Files - $AdvisoryFile"
          }
          catch {
            $baseurl = "https://ftp.hp.com/pub/caps-softpaq/cmit/imagepal/ref/"
            $url = $baseurl + "$($softpaqListCmd.platform)/$($softpaqListCmd.platform)_cds.cab"
            Write-Verbose "Trying to download Advisory Data from FTP... $url"
            #$cacheDirAdvisory = $cacheDirOffline + "\$($softpaqListCmd.platform)"
            #$filename = "$($softpaqListCmd.platform)_cds.cab"
            $AdvisoryFile = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirAdvisory -Expand
            Write-Verbose "Finish downloading Advisory Data Files - $AdvisoryFile"
            if (-not $AdvisoryFile) {
              $exception = $_.Exception
              switch ($repo[1].settings.OnRemoteFileNotFound) {
                "LogAndContinue" {
                  [string]$data = formatSyncErrorMessageAsHtml $exception
                  Log ($data -split "`n")
                  send "Softpaq repository synchronization error" $data
                }
                # "Fail"
                default {
                  throw $exception
                }
              }
            }
          }

          # download Knowledge Base
          $url = $baseurl + "../kb/common/latest.cab"
          $cacheDirKb = $cacheDirOffline + "\kb\common"
          $filename = "latest.cab"
          Write-Verbose "Trying to download Knowledge Base... $url"
          try {
            $KnowledgeBase = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirKb
            Write-Verbose "Finish downloading Knowledge Base - $KnowledgeBase"
          }
          catch {
            $url = "https://ftp.hp.com/pub/caps-softpaq/cmit/imagepal/kb/common/latest.cab"
            Write-Verbose "Trying to download Knowledge Base from FTP... $url"
            #$cacheDirKb = $cacheDirOffline + "\kb\common"
            #$filename = "latest.cab"
            $KnowledgeBase = Get-HPPrivateOfflineCacheFiles -url $url -FileName $filename -cacheDirOffline $cacheDirKb
            Write-Verbose "Finish downloading Knowledge Base - $KnowledgeBase"
            if (-not $KnowledgeBase) {
              $exception = $_.Exception
              switch ($repo[1].settings.OnRemoteFileNotFound) {
                "LogAndContinue" {
                  [string]$data = formatSyncErrorMessageAsHtml $exception
                  Log ($data -split "`n")
                  send "Softpaq repository synchronization error" $data
                }
                # "Fail"
                default {
                  throw $exception
                }
              }
            }
          }
        }
      }
      else {
        Write-Host -ForegroundColor Cyan "Platform $($c.platform) doesn't exist. Please add a valid platform."
        Write-LogWarning "Platform $($c.platform) in not valid, and is was skipped."
      }
    }

    Write-Verbose ("Done with the list, repository is $($softpaqlist.Count) softpaqs.")
    [array]$softpaqlist = @($softpaqlist | Sort-Object -Unique -Property Id)
    Write-Verbose ("After trimming duplicates, we have $($softpaqlist.Count) softpaqs.")


    Write-Verbose ("Flushing the list of markers")
    FlushMarks
    Write-Verbose ("Writing new marks")

    # generate .mark file for each SoftPaq to be downloaded
    foreach ($sp in $softpaqList) {
      $number = $sp.id.ToLower().TrimStart("sp")
      TouchFile -File ".repository/mark/$number.mark"
    }

    Write-Verbose ("Starting download")
    $downloadCmd = @{}
    $downloadCmd.Quiet = $quiet
    $downloadCmd.Verbose = $VerbosePreference

    Log "Download has started for $($softpaqlist.Count) softpaqs."
    foreach ($sp in $softpaqlist)
    {
      $downloadCmd.Number = $sp.id.ToLower().TrimStart("sp")
      $downloadCmd.Url = $sp.url -Replace "/$($sp.id).exe$",''
      Write-Verbose "Working on data for softpaq $($downloadCmd.number)"
      try {
        Log "Start downloading files for sp$($downloadCmd.number)."
        DownloadSoftpaq -DownloadSoftpaqCmd $downloadCmd -MaxRetries $repo[1].settings.ExclusiveLockMaxRetries -Verbose:$VerbosePreference

        if ($OfflineCacheMode -eq "Enable") {
          Log (" sp$($downloadCmd.number).html - Downloading Release Notes.")
          $ReleaseNotesurl = Get-HPPrivateItemUrl $downloadCmd.number "html"
          $target = "sp$($downloadCmd.number).html"
          $targetfile = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($target)
          Invoke-HPPrivateDownloadFile -url $ReleaseNotesurl -Target $targetfile
          Log (" sp$($downloadCmd.number).html - Done Downloading Release Notes.")
        }
        Log "Finish downloading files for sp$($downloadCmd.number)."
      }
      catch {
        $exception = $_.Exception

        switch ($repo[1].settings.OnRemoteFileNotFound)
        {
          "LogAndContinue" {
            [string]$data = formatSyncErrorMessageAsHtml $exception
            Log ($data -split "`n")
            send "Softpaq repository synchronization error" $data
          }
          # "Fail"
          default {
            throw $exception
          }
        }
      }
    }

    Log "Repository sync has ended"
    Write-Verbose "Repository Sync has ended."

    Log "Repository Report creation started"
    Write-Verbose "Repository Report creation started."

    try {
      # get the configuration set for repository report if any
      $RepositoryReport = $repo[1].settings.RepositoryReport
      if ($RepositoryReport) {
        $Format = $RepositoryReport
        New-RepositoryReport -Format $Format -RepositoryPath "$cwd" -OutputFile "$cwd\.repository\Contents.$Format"
        Log "Repository Report created as Contents.$Format"
        Write-Verbose "Repository Report created as Content.$Format."
      }
    }
    catch [System.IO.FileNotFoundException]{
      Write-Verbose "No data available to create Repository Report as directory '$(Get-Location)' does not contain any CVA files."
      Log "No data available to create Repository Report as directory '$(Get-Location)' does not contain any CVA files."
    }
    catch {
      Write-Verbose "Error in creating Repository Report"
      Log "Error in creating Repository Report."
    }
  }
  catch
  {
    err "Repository synchronization failed: $($_.Exception.Message)" $true
    [string]$data = formatSyncErrorMessageAsHtml $_.Exception
    Log ($data -split "`n")
    send "Softpaq repository synchronization error" $data
  }
}

function formatSyncErrorMessageAsHtml ($exception)
{
  [string]$data = "An error occurred during softpaq synchronization.`n`n";
  $data += "The error was: <em>$($exception.Message)</em>`n"
  $data += "`nDetails:`n<pre>"
  $data += "<hr/>"
  $data += ($exception | Format-List -Force | Out-String)
  $data += "</pre>"
  $data += "<hr/>"
  $data
}

<#
.SYNOPSIS
    Cleanup repository
   
.DESCRIPTION
  Use Invoke-RepositoryCleanup to remove SoftPaqs from repository that are obsolete. These may be SoftPaqs that have been replaced
  by newer versions, or that no longer match the active repository filters.
 
.EXAMPLE
    Invoke-RepositoryCleanup
 
.LINK
  [Initialize-Repository](Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
  [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
#>

function Invoke-RepositoryCleanup
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Invoke-RepositoryCleanup")]
  param()
  $repo = LoadRepository
  Log ("Beginning repository cleanup")
  $deleted = 0

  try {
    Get-ChildItem "." -File | ForEach-Object {
      $name = $_.Name.ToLower().TrimStart("sp").Split('.')[0]
      if ($name -ne $null) {
        if (-not (Test-Path ".repository/mark/$name.mark" -PathType Leaf))
        {
          Write-Verbose "Deleting orphaned file $($_.Name)"
          Remove-Item $_.Name
          $deleted++
        }
        #else {
        # Write-Verbose "Softpaq $($_.Name) is still needed."
        #}
      }
    }
    Log ("Completed repository cleanup, deleted $deleted files.")
  }
  catch {
    err ("Could not clean repository: $($_.Exception.Message)")
  }
}

<#
.SYNOPSIS
  Set the repository notification configuration.
 
.DESCRIPTION
  This function defines a notification SMTP server (and optionally, port) for an email server to be used to send failure notifications during unattended synchronization via Invoke-RepositorySync.
 
  One or more recipients can then be added via Add-RepositorySyncFailureRecipient.
 
  The directory must have been initialized via Initialize-Repository.
 
  The function must be invoked inside a directory initialized as a repository.
 
 
.PARAMETER Server
  The server name (or IP) for the outgoing mail (SMTP) server
 
.PARAMETER Port
  Specifies a port for the SMTP server. If not provided, the default IANA-assigned port 25 will be used.
 
.PARAMETER Tls
  Specifies whether to use SSL/TLS. The value may be "true", "false", or "auto". "Auto" will automatically set SSL to true when the port is changed to a value different than 25. By default, TLS is false.
 
.PARAMETER UserName
  Specifies the SMTP server username for authenticated SMTP servers. If username is not specified, connection will be made without authentication.
 
.PARAMETER Password
  Specifies the SMTP server password for authenticated SMTP servers.
   
.PARAMETER From
    Specifies the email address from which the notification will appear to originate. Note that in servers may accept emails from specified
  domains only, or in some cases may require the email address to match the username.
 
.PARAMETER FromName
  Specifies the from address display name.
 
.PARAMETER RemoveCredentials
  Removes the SMTP server credentials without removing the entire mail server configuration.
 
.EXAMPLE
  Set-RepositoryNotificationConfiguration smtp.mycompany.com
 
.LINK
  [Initialize-Repository](Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
#>

function Set-RepositoryNotificationConfiguration
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Set-RepositoryNotificationConfiguration")]
  param(
    [Parameter(Position = 0,Mandatory = $false)]
    [string]
    [ValidatePattern("^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$")]
    $Server = $null,

    [Parameter(Position = 1,Mandatory = $false)]
    [ValidateRange(1,65535)]
    [int]
    $Port = 0,

    [Parameter(Position = 2,Mandatory = $false)]
    [string]
    [ValidateSet('true','false','auto')]
    $Tls = $null,

    [Parameter(Position = 3,Mandatory = $false)]
    [string]
    $Username = $null,

    [Parameter(Position = 4,Mandatory = $false)]
    [string]
    $Password = $null,

    [Parameter(Position = 5,Mandatory = $false)]
    [string]
    [ValidatePattern("^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")]
    $From = $null,

    [Parameter(Position = 6,Mandatory = $false)]
    [string]
    $FromName = $null,

    [Parameter(Position = 7,Mandatory = $false)]
    [switch]
    $RemoveCredentials
  )

  Write-Verbose "Beginning notification configuration update"

  if ($RemoveCredentials.IsPresent -and ([string]::IsNullOrEmpty($UserName) -eq $false -or [string]::IsNullOrEmpty($Password) -eq $false))
  {
    err ("-removeCredentials may not be specified with -username or -password")
    return
  }

  $c = LoadRepository
  try {
    if (-not $c[0]) { return }

    Write-Verbose "Applying configuration"
    if ([string]::IsNullOrEmpty($Server) -eq $false) {
      Write-Verbose ("Setting SMTP Server to: $Server")
      $c[1].Notifications.server = $Server
    }

    if ($Port) {
      Write-Verbose ("Setting SMTP Server port to: $Port")
      $c[1].Notifications.port = $Port
    }

    if (-not [string]::IsNullOrEmpty($UserName)) {
      Write-Verbose ("Setting SMTP server credential(username) to: $UserName")
      $c[1].Notifications.UserName = $UserName
    }

    if (-not [string]::IsNullOrEmpty($Password)) {
      Write-Verbose ("Setting SMTP server credential(password) to: (redacted)")
      $c[1].Notifications.Password = ConvertTo-SecureString $Password -Force -AsPlainText | ConvertFrom-SecureString
    }

    if ($RemoveCredentials.IsPresent)
    {
      Write-Verbose ("Clearing credentials from notification configuration")
      $c[1].Notifications.UserName = $null
      $c[1].Notifications.Password = $null
    }

    switch ($Tls)
    {
      "auto" {
        if ($Port -ne 25) { $c[1].Notifications.tls = $true }
        else { $c[1].Notifications.tls = $false }
        Write-Verbose ("SMTP server SSL auto-calculated to: $($c[1].Notifications.tls)")
      }

      "true" {
        $c[1].Notifications.tls = $true
        Write-Verbose ("Setting SMTP SSL to: $($c[1].Notifications.tls)")
      }
      "false" {
        $c[1].Notifications.tls = $false
        Write-Verbose ("Setting SMTP SSL to: $($c[1].Notifications.tls)")
      }
    }
    if (-not [string]::IsNullOrEmpty($From)) {
      Write-Verbose ("Setting Mail from address to: $From")
      $c[1].Notifications.from = $From }
    if (-not [string]::IsNullOrEmpty($FromName)) {
      Write-Verbose ("Setting Mail from displayname to: $FromName")
      $c[1].Notifications.fromname = $FromName }

    WriteRepositoryFile -obj $c[1]
    Log ("Updated notification configuration")
  }
  catch {
    err ("Failed to modify repository configuration: $($_.Exception.Message)")
  }
}

<#
.SYNOPSIS
    Clear the repository notification configuration
 
.DESCRIPTION
  This function removes notification configuration from repository, in effect turning off notifications.
 
  The directory must have been initialized via Initialize-Repository and notification configuration must have been defined via Set-RepositoryNotificationConfiguration
 
  The function must be invoked inside a directory initialized as a repository.
 
.LINK
  [Initialize-Repository](Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
.EXAMPLE
  Clear-RepositoryNotificationConfiguration
 
#>

function Clear-RepositoryNotificationConfiguration ()
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Clear-RepositoryNotificationConfiguration")]
  param()
  Log "Clearing notification configuration"

  $c = LoadRepository
  try {
    if (-not $c[0]) { return }
    $c[1].Notifications = $null
    WriteRepositoryFile -obj $c[1]
    Write-Verbose ("Ok.")
  }
  catch {
    err ("Failed to modify repository configuration: $($_.Exception.Message)")
  }
}

<#
.SYNOPSIS
    Get an object representing the current notification configuration
 
.DESCRIPTION
  This function retrieves the current notification configuration as an object.
 
  The directory must have been initialized via Initialize-Repository and notification configuration must have been defined via Set-RepositoryNotificationConfiguration
 
  The function must be invoked inside a directory initialized as a repository.
   
.LINK
  [Initialize-Repository](Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
.EXAMPLE
  $config = Get-RepositoryNotificationConfiguration
 
 
#>

function Get-RepositoryNotificationConfiguration ()
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Get-RepositoryNotificationConfiguration")]
  param()

  $c = LoadRepository
  if ((-not $c[0]) -or (-not $c[1].Notifications))
  {
    return $null
  }
  return $c[1].Notifications
}


<#
.SYNOPSIS
    Display the current notification configuration to screen
 
 
.DESCRIPTION
  This function retrieves the current notification configuration as user-friendly screen output.
 
  The directory must have been initialized via Initialize-Repository and notification configuration must have been defined via Set-RepositoryNotificationConfiguration
 
  The function must be invoked inside a directory initialized as a repository.
 
.LINK
  [Initialize-Repository](Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
  [Add-RepositorySyncFailureRecipient](Add-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
.EXAMPLE
  Show-RepositoryNotificationConfiguration
#>

function Show-RepositoryNotificationConfiguration ()
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Show-RepositoryNotificationConfiguration")]
  param()

  try {
    $c = Get-RepositoryNotificationConfiguration
    if (-not $c)
    {
      err ("Notifications are not configured.")
      return
    }

    if (-not [string]::IsNullOrEmpty($c.UserName)) {
      Write-Host "Notification server: smtp://$($c.username):<password-redacted>@$($c.server):$($c.port)"
    }
    else {
      Write-Host "Notification server: smtp://$($c.server):$($c.port)"
    }
    Write-Host "Email will arrive from $($c.from) with name `"$($c.fromname)`""

    if ((-not $c.addresses) -or (-not $c.addresses.Count))
    {
      Write-Host "There are no recipients configured"
      return
    }
    foreach ($r in $c.addresses)
    {
      Write-Host "Recipient: $r"
    }
  }
  catch {
    err ("Failed to read repository configuration: $($_.Exception.Message)")
  }

}

<#
.SYNOPSIS
  Add a recipient to be notified of failures
 
.DESCRIPTION
  This function adds an email address to the repository. On failures, notifications will be sent to this email address.
 
  The directory must have been initialized via Initialize-Repository and notification configured via Set-RepositoryNotificationConfiguration.
 
  The function must be invoked inside a directory initialized as a repository.
 
.PARAMETER To
  The email address to add
 
.LINK
  [Initialize-Repository](Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
.EXAMPLE
  Add-RepositorySyncFailureRecipient -to someone@mycompany.com
 
#>

function Add-RepositorySyncFailureRecipient ()
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Add-RepositorySyncFailureRecipient")]
  param(
    [Parameter(Position = 0,Mandatory = $true)]
    [ValidatePattern("^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")]
    [string]
    $To
  )

  Log "Adding '$To' as a recipient."
  $c = LoadRepository
  try {
    if (-not $c[0]) { return }

    if (-not $c[1].Notifications) {
      err ("Notifications are not configured")
      return
    }

    if (-not $c[1].Notifications.addresses) {
      $c[1].Notifications.addresses = $()
    }

    $c[1].Notifications.addresses += $To.trim()
    $c[1].Notifications.addresses = $c[1].Notifications.addresses | Sort-Object -Unique
    WriteRepositoryFile -obj ($c[1] | Sort-Object -Unique)
  }
  catch {
    err ("Failed to modify repository configuration: $($_.Exception.Message)")
  }

}

<#
.SYNOPSIS
    Remove a recipient from notification list for the current repository.
 
 
.DESCRIPTION
  This function removes an email address as a recipient for synchronization failure messages.
 
  The directory must have been initialized via Initialize-Repository and notification configured via Set-RepositoryNotificationConfiguration.
 
  The function must be invoked inside a directory initialized as a repository.
 
.PARAMETER To
  The email address to remove
 
.LINK
  [Initialize-Repository](Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.LINK
  [Test-RepositoryNotificationConfiguration](Test-RepositoryNotificationConfiguration)
 
.EXAMPLE
  Remove-RepositorySyncFailureRecipient -to someone@mycompany.com
 
#>

function Remove-RepositorySyncFailureRecipient
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Remove-RepositorySyncFailureRecipient")]
  param(
    [Parameter(Position = 0,Mandatory = $true)]
    [ValidatePattern("^\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*$")]
    [string]
    $To
  )
  Log "Removing '$To' as a recipient."
  $c = LoadRepository
  try {
    if ($c[0] -eq $false) { return }

    if (-not $c[1].Notifications) {
      err ("Notifications are not configured")
      return
    }


    if (-not $c[1].Notifications.addresses) {
      $c[1].Notifications.addresses = $()
    }

    $c[1].Notifications.addresses = $c[1].Notifications.addresses | Where-Object { $_ -ne $To.trim() } | Sort-Object -Unique
    WriteRepositoryFile -obj ($c[1] | Sort-Object -Unique)
  }
  catch {
    err ("Failed to modify repository configuration: $($_.Exception.Message)")
  }
}


<#
.SYNOPSIS
    Test the email notification configuration by sending a test email
 
.DESCRIPTION
  This function sends a test email using the current repository configuration and reports
  any errors associated with the send process. It is intended for debugging the email server configuration.
 
.LINK
  [Initialize-Repository](Initialize-Repository)
 
.LINK
  [Add-RepositoryFilter](Add-RepositoryFilter)
 
.LINK
  [Remove-RepositoryFilter](Remove-RepositoryFilter)
 
.LINK
  [Get-RepositoryInfo](Get-RepositoryInfo)
 
.LINK
  [Invoke-RepositorySync](Invoke-RepositorySync)
 
.LINK
  [Invoke-RepositoryCleanup](Invoke-RepositoryCleanup)
 
.LINK
  [Set-RepositoryNotificationConfiguration](Set-RepositoryNotificationConfiguration)
 
.LINK
  [Clear-RepositoryNotificationConfiguration](Clear-RepositoryNotificationConfiguration)
 
.LINK
  [Get-RepositoryNotificationConfiguration](Get-RepositoryNotificationConfiguration)
 
.LINK
  [Show-RepositoryNotificationConfiguration](Show-RepositoryNotificationConfiguration)
 
.LINK
  [Remove-RepositorySyncFailureRecipient](Remove-RepositorySyncFailureRecipient)
 
.EXAMPLE
  Test-RepositoryNotificationConfiguration
 
#>

function Test-RepositoryNotificationConfiguration
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Test-RepositoryNotificationConfiguration")]
  param()

  Log ("test email started")
  send "Repository Failure Notification (Test only)" "No content." -html $false
  Write-Verbose ("Ok.")
}

<#
.SYNOPSIS
  Set repository configuration values
 
.DESCRIPTION
  This function sets various configuration options that control synchronization behavior.
 
.PARAMETER setting
  The setting to configure from 'OnRemoteFileNotFound', 'OfflineCacheMode' and 'RepositoryReport'.
 
.PARAMETER value
  The new value of the setting for OnRemoteFileNotFound. It can be from 'Fail' (default) and 'LogAndContinue'.
 
.PARAMETER CacheValue
  The new CacheValue of the setting for OfflineCacheMode. It can be from 'Disable' (default) and 'Enable'.
 
.PARAMETER Format
  The new value of the setting for RepositoryReport. It can be from 'CSV' (Default) ,'JSon', 'XML' and 'ExcelCSV'.
 
.LINK
  [Initialize-Repository](Initialize-Repository)
 
.LINK
  [Get-RepositoryConfiguration](Get-RepositoryConfiguration)
 
.Example
  Set-RepositoryConfiguration -Setting OnRemoteFileNotFound -Value LogAndContinue
 
.Example
  Set-RepositoryConfiguration -Setting OfflineCacheMode -CacheValue Enable
 
.Example
  Set-RepositoryConfiguration -Setting RepositoryReport -Format CSV
 
.NOTES
  Current event handlers supported:
 
  - _OnRemoteFileNotFound_ - indicates what should happen if an expected SoftPaq is not found on the remote site. The default is 'Fail' in which case the process will stop. Setting to 'LogAndContinue' will log the error, but the repository synchronization will continue.
 
  Current Repository Report functionality:
 
  Invoke-RepositorySync will create a report containing contents of the Repository in a format provided by RepositoryFormat Setting.
  Use RepositoryReport Setting with -Format to set the format in which the you would like the report to be generated. Default format is CSV.
  Report will be generated with a name Contents.<format> e.g. Contents.CSV inside the .repository directory
 
  To create a report outside the repository use New-RepositoryReport function.
#>

function Set-RepositoryConfiguration
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Set-RepositoryConfiguration")]
  param(
    [ValidateSet('OnRemoteFileNotFound','OfflineCacheMode','RepositoryReport')]
    [Parameter(ParameterSetName = "ErrorHandler",Position = 0,Mandatory = $true)]
    [Parameter(ParameterSetName = "CacheMode",Position = 0,Mandatory = $true)]
    [Parameter(ParameterSetName = "ReportHandler",Position = 0,Mandatory = $true)]
    [string]$Setting,

    [Parameter(ParameterSetName = "ErrorHandler",Position = 1,Mandatory = $true)]
    [ErrorHandling]$Value,

    [ValidateSet('Enable','Disable')]
    [Parameter(ParameterSetName = "CacheMode",Position = 1,Mandatory = $true)]
    [string]$CacheValue,

    [ValidateSet('CSV','JSon','XML','ExcelCSV')]
    [Parameter(ParameterSetName = "ReportHandler",Position = 1,Mandatory = $true)]
    [string]$Format
  )
  $c = LoadRepository
  if (-not $c[0]) { return }
  if ($Setting -eq "OnRemoteFileNotFound") {
    if (($Value -eq "Fail") -or ($Value -eq "LogAndContinue")) {
      $c[1].settings. "${Setting}" = $Value
      WriteRepositoryFile -obj $c[1]
      Write-Verbose ("Ok.")
    }
    else {
      Write-Host -ForegroundColor Magenta "Enter valid Value for $Setting."
      Write-LogWarning "Enter valid Value for $Setting."
    }
  }
  elseif ($Setting -eq "OfflineCacheMode") {
    if ($CacheValue) {
      $c[1].settings. "${Setting}" = $CacheValue
      WriteRepositoryFile -obj $c[1]
      Write-Verbose ("Ok.")
    }
    else {
      Write-Host -ForegroundColor Magenta "Enter valid CacheValue for $Setting."
      Write-LogWarning "Enter valid CacheValue for $Setting."
    }
  }
  elseif ($Setting -eq "RepositoryReport") {
    if ($Format) {
      $c[1].settings. "${Setting}" = $Format
      WriteRepositoryFile -obj $c[1]
      Write-Verbose ("Ok.")
    }
    else {
      Write-Host -ForegroundColor Magenta "Enter valid Format for $Setting."
      Write-LogWarning "Enter valid Format for $Setting."
    }
  }
}

<#
.SYNOPSIS
    Get repository configuration values
 
.DESCRIPTION
  This function get various configuration options that control synchronization behavior.
 
.PARAMETER setting
  The setting to retrieve
 
.Example
  Get-RepositoryConfiguration -Setting OfflineCacheMode
 
.Example
  Get-RepositoryConfiguration -Setting OnRemoteFileNotFound
 
.Example
  Get-RepositoryConfiguration -Setting RepositoryReport
 
.LINK
  [Set-RepositoryConfiguration](Set-RepositoryConfiguration)
 
.LINK
  [Initialize-Repository](Initialize-Repository)
#>

function Get-RepositoryConfiguration
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/Get-RepositoryConfiguration")]
  param(
    [Parameter(Position = 0,Mandatory = $true)]
    [string]
    [ValidateSet('OnRemoteFileNotFound','OfflineCacheMode','RepositoryReport')]
    $Setting
  )
  $c = LoadRepository
  if (-not $c[0]) { return }
  $c[1].settings. "${Setting}"
}


<#
.SYNOPSIS
  Create a report from a repository directory
 
.DESCRIPTION
  This function scans a repository (or any directory containing CVAs and EXEs) and creates a report in one of the supported formats.
 
  Currently the supported formats are:
 
  - XML - Return an XML object
  - JSON - Return a JSON document
  - CSV - Return a CSV document
  - ExcelCSV - Return a CSV document containing an Excel hint that defines comma as a default separator. Use this only if you plan on opening the CSV file with Excel.
 
  If a format is not specified, the function will return PowerShell objects to the pipeline.
 
.PARAMETER Format
  Specify the output format (CSV, JSON, or XML). If not specified, the function will return PowerShell objects.
 
.PARAMETER RepositoryPath
  By default, the function assumes the repository is in the current directory. This parameter allows specifying a different location for the repository.
 
.PARAMETER OutputFile
  Specify an output file for the function. Can only be specified together with "Format".
 
.EXAMPLE
  New-RepositoryReport -Format JSON -RepositoryPath c:\myrepository\softpaqs -OutputFile c:\repository\today.json
 
.NOTES
  The function currently supports scenarios where the SoftPaq executable is stored under the format sp<softpaq-number>.exe.
#>

function New-RepositoryReport
{
  [CmdletBinding(HelpUri = "https://developers.hp.com/hp-client-management/doc/New-RepositoryReport")]
  param(
    [Parameter(Position = 0,Mandatory = $false)]
    [ValidateSet('CSV','JSon','XML','ExcelCSV')]
    [string]$Format,

    [Parameter(Position = 1,Mandatory = $false)]
    [System.IO.DirectoryInfo]$RepositoryPath = '.',

    [Parameter(Position = 2,Mandatory = $false)]
    [System.IO.FileInfo]$OutputFile
  )
  if ($OutputFile -and -not $format) { throw "OutputFile parameter requires a Format specifier" }
  $cvaList = Get-ChildItem -Path $RepositoryPath -Filter '*.cva'

  if (-not $cvaList -or -not $cvaList.Length)
  {
    throw [System.IO.FileNotFoundException]"Directory '$(Get-Location)' does not contain CVA files."
  }

  Write-Verbose "Processing $($cvaList.Length) CVAs"
  $results = $cvaList | ForEach-Object {
    $cva = Get-HPPrivateReadINI $_.FullName

    try {
      $exe = Get-ChildItem -Path ($cva.Softpaq.SoftpaqNumber.trim() + ".exe") -ErrorAction stop
    }
    catch [System.Management.Automation.ItemNotFoundException]{
      $exe = $null
    }

    [pscustomobject]@{
      Softpaq = $cva.Softpaq.SoftpaqNumber
      Vendor = $cva.General.VendorName
      Title = $cva. "Software Title".US
      type = if ($Cva.General.Category.contains("-")) { $Cva.General.Category.substring(0,$Cva.General.Category.IndexOf('-')).trim() } else { $Cva.General.Category }
      Version = "$($cva.General.Version) Rev.$($cva.General.Revision)"
      Downloaded = if ($exe) { $exe.CreationTime } else { "" }
      Size = if ($exe) { "$($exe.Length)" } else { "" }
    }
  }
  switch ($format)
  {
    "CSV" {
      $r = $results | ConvertTo-Csv -NoTypeInformation
    }
    "ExcelCSV" {

      $r = $results | ConvertTo-Csv -NoTypeInformation
      $r = [string[]]"sep=," + $r
    }
    "JSon" {
      $r = $results | ConvertTo-Json
    }
    "XML" {
      $r = $results | ConvertTo-Xml -NoTypeInformation
    }
    default {
      return $results
    }
  }

  if ($OutputFile) {
    if ($format -eq "xml") { $r = $r.OuterXml }
    $r | Out-File -FilePath $OutputFile -Encoding utf8
  }
  else { $r }
}






# SIG # Begin signature block
# MIIt/QYJKoZIhvcNAQcCoIIt7jCCLeoCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCIkDzop4BqeAiN
# kmlkf9ImOgPv5D3nUMzeLBLbypc0jKCCE2wwggXAMIIEqKADAgECAhAP0bvKeWvX
# +N1MguEKmpYxMA0GCSqGSIb3DQEBCwUAMGwxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xKzApBgNV
# BAMTIkRpZ2lDZXJ0IEhpZ2ggQXNzdXJhbmNlIEVWIFJvb3QgQ0EwHhcNMjIwMTEz
# MDAwMDAwWhcNMzExMTA5MjM1OTU5WjBiMQswCQYDVQQGEwJVUzEVMBMGA1UEChMM
# RGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSEwHwYDVQQD
# ExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwggIiMA0GCSqGSIb3DQEBAQUAA4IC
# DwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz7MKnJS7JIT3yithZwuEppz1Yq3aa
# za57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS5F/WBTxSD1Ifxp4VpX6+n6lXFllV
# cq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7bXHiLQwb7iDVySAdYyktzuxeTsiT
# +CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfISKhmV1efVFiODCu3T6cw2Vbuyntd
# 463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jHtrHEtWoYOAMQjdjUN6QuBX2I9YI+
# EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14Ztk6MUSaM0C/CNdaSaTC5qmgZ92k
# J7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2h4mXaXpI8OCiEhtmmnTK3kse5w5j
# rubU75KSOp493ADkRSWJtppEGSt+wJS00mFt6zPZxd9LBADMfRyVw4/3IbKyEbe7
# f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPRiQfhvbfmQ6QYuKZ3AeEPlAwhHbJU
# KSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ERElvlEFDrMcXKchYiCd98THU/Y+wh
# X8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4KJpn15GkvmB0t9dmpsh3lGwIDAQAB
# o4IBZjCCAWIwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wHwYDVR0jBBgwFoAUsT7DaQP4v0cB1JgmGggC72NkK8MwDgYDVR0P
# AQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMH8GCCsGAQUFBwEBBHMwcTAk
# BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEkGCCsGAQUFBzAC
# hj1odHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRIaWdoQXNzdXJh
# bmNlRVZSb290Q0EuY3J0MEsGA1UdHwREMEIwQKA+oDyGOmh0dHA6Ly9jcmwzLmRp
# Z2ljZXJ0LmNvbS9EaWdpQ2VydEhpZ2hBc3N1cmFuY2VFVlJvb3RDQS5jcmwwHAYD
# VR0gBBUwEzAHBgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQELBQADggEBAEHx
# qRH0DxNHecllao3A7pgEpMbjDPKisedfYk/ak1k2zfIe4R7sD+EbP5HU5A/C5pg0
# /xkPZigfT2IxpCrhKhO61z7H0ZL+q93fqpgzRh9Onr3g7QdG64AupP2uU7SkwaT1
# IY1rzAGt9Rnu15ClMlIr28xzDxj4+87eg3Gn77tRWwR2L62t0+od/P1Tk+WMieNg
# GbngLyOOLFxJy34riDkruQZhiPOuAnZ2dMFkkbiJUZflhX0901emWG4f7vtpYeJa
# 3Cgh6GO6Ps9W7Zrk9wXqyvPsEt84zdp7PiuTUy9cUQBY3pBIowrHC/Q7bVUx8ALM
# R3eWUaNetbxcyEMRoacwggawMIIEmKADAgECAhAIrUCyYNKcTJ9ezam9k67ZMA0G
# CSqGSIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ
# bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0
# IFRydXN0ZWQgUm9vdCBHNDAeFw0yMTA0MjkwMDAwMDBaFw0zNjA0MjgyMzU5NTla
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
# ODQgMjAyMSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDVtC9C
# 0CiteLdd1TlZG7GIQvUzjOs9gZdwxbvEhSYwn6SOaNhc9es0JAfhS0/TeEP0F9ce
# 2vnS1WcaUk8OoVf8iJnBkcyBAz5NcCRks43iCH00fUyAVxJrQ5qZ8sU7H/Lvy0da
# E6ZMswEgJfMQ04uy+wjwiuCdCcBlp/qYgEk1hz1RGeiQIXhFLqGfLOEYwhrMxe6T
# SXBCMo/7xuoc82VokaJNTIIRSFJo3hC9FFdd6BgTZcV/sk+FLEikVoQ11vkunKoA
# FdE3/hoGlMJ8yOobMubKwvSnowMOdKWvObarYBLj6Na59zHh3K3kGKDYwSNHR7Oh
# D26jq22YBoMbt2pnLdK9RBqSEIGPsDsJ18ebMlrC/2pgVItJwZPt4bRc4G/rJvmM
# 1bL5OBDm6s6R9b7T+2+TYTRcvJNFKIM2KmYoX7BzzosmJQayg9Rc9hUZTO1i4F4z
# 8ujo7AqnsAMrkbI2eb73rQgedaZlzLvjSFDzd5Ea/ttQokbIYViY9XwCFjyDKK05
# huzUtw1T0PhH5nUwjewwk3YUpltLXXRhTT8SkXbev1jLchApQfDVxW0mdmgRQRNY
# mtwmKwH0iU1Z23jPgUo+QEdfyYFQc4UQIyFZYIpkVMHMIRroOBl8ZhzNeDhFMJlP
# /2NPTLuqDQhTQXxYPUez+rbsjDIJAsxsPAxWEQIDAQABo4IBWTCCAVUwEgYDVR0T
# AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHwYD
# VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG
# A1UdJQQMMAoGCCsGAQUFBwMDMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV
# HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU
# cnVzdGVkUm9vdEc0LmNybDAcBgNVHSAEFTATMAcGBWeBDAEDMAgGBmeBDAEEATAN
# BgkqhkiG9w0BAQwFAAOCAgEAOiNEPY0Idu6PvDqZ01bgAhql+Eg08yy25nRm95Ry
# sQDKr2wwJxMSnpBEn0v9nqN8JtU3vDpdSG2V1T9J9Ce7FoFFUP2cvbaF4HZ+N3HL
# IvdaqpDP9ZNq4+sg0dVQeYiaiorBtr2hSBh+3NiAGhEZGM1hmYFW9snjdufE5Btf
# Q/g+lP92OT2e1JnPSt0o618moZVYSNUa/tcnP/2Q0XaG3RywYFzzDaju4ImhvTnh
# OE7abrs2nfvlIVNaw8rpavGiPttDuDPITzgUkpn13c5UbdldAhQfQDN8A+KVssIh
# dXNSy0bYxDQcoqVLjc1vdjcshT8azibpGL6QB7BDf5WIIIJw8MzK7/0pNVwfiThV
# 9zeKiwmhywvpMRr/LhlcOXHhvpynCgbWJme3kuZOX956rEnPLqR0kq3bPKSchh/j
# wVYbKyP/j7XqiHtwa+aguv06P0WmxOgWkVKLQcBIhEuWTatEQOON8BUozu3xGFYH
# Ki8QxAwIZDwzj64ojDzLj4gLDb879M4ee47vtevLt/B3E+bnKD+sEq6lLyJsQfmC
# XBVmzGwOysWGw/YmMwwHS6DTBwJqakAwSEs0qFEgu60bhQjiWQ1tygVQK+pKHJ6l
# /aCnHwZ05/LWUpD9r4VIIflXO7ScA+2GRfS0YW6/aOImYIbqyK+p/pQd52MbOoZW
# eE4wggbwMIIE2KADAgECAhAI+qTPsJ3byDJ7SsgX0LBUMA0GCSqGSIb3DQEBCwUA
# MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE
# AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBDb2RlIFNpZ25pbmcgUlNBNDA5NiBTSEEz
# ODQgMjAyMSBDQTEwHhcNMjIwMzA5MDAwMDAwWhcNMjMwMzA5MjM1OTU5WjB1MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJUGFsbyBB
# bHRvMRAwDgYDVQQKEwdIUCBJbmMuMRkwFwYDVQQLExBIUCBDeWJlcnNlY3VyaXR5
# MRAwDgYDVQQDEwdIUCBJbmMuMIIBojANBgkqhkiG9w0BAQEFAAOCAY8AMIIBigKC
# AYEA2KwFARbsSL8FnMdZ++xo7iVdqg+ZOY0S2KkvYQdNNcvrcfHTdNpNgf65RuIt
# VQxdJXzmZcAOXJUPjRQRduvFf/I8jqR4UwBLsNoy/sEuQIDCfezNSQz8TPredjUG
# Lr6Y9ie1vYryqJ110Mj6NtXZQidlytEneq3z73Ec7TRFKp8iiiwNpTcbhAq93pq6
# bjnc98ajFUBHJu9Gfk1Or3haR6m7YH0LRLVWm18I2OKrcPLk67hWRj6Aa7/heBkk
# F8TfGCUwGBHhblrprBVECR3M4zTnMygBfxVEzYsdyAytPy0DgqzZ7+rHY0yvgDUx
# Fi/d1SyqNDCf6FBBudNjzw7TULEBHlJjk96xhd1z4X5ctL1kW4duC7Mba6H8A1lI
# qM5qa+8Fr88IJhnl21PlkBp+XAk3lBaeJ/DVpORIv3bhUV8OLae6ElQBGvqQoEY/
# AaNerghhFjiqAhaUG3z3Y7ruhVaCmuw/SMVS79dxESj/J1qHWVnF1tn2a4liq/RY
# VeFTAgMBAAGjggIGMIICAjAfBgNVHSMEGDAWgBRoN+Drtjv4XxGG+/5hewiIZfRO
# QjAdBgNVHQ4EFgQUAjIiVx974XGZre7F5HqNCJiWZbowDgYDVR0PAQH/BAQDAgeA
# MBMGA1UdJQQMMAoGCCsGAQUFBwMDMIG1BgNVHR8Ega0wgaowU6BRoE+GTWh0dHA6
# Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNENvZGVTaWduaW5n
# UlNBNDA5NlNIQTM4NDIwMjFDQTEuY3JsMFOgUaBPhk1odHRwOi8vY3JsNC5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQwOTZTSEEz
# ODQyMDIxQ0ExLmNybDA+BgNVHSAENzA1MDMGBmeBDAEEATApMCcGCCsGAQUFBwIB
# FhtodHRwOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwgZQGCCsGAQUFBwEBBIGHMIGE
# MCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wXAYIKwYBBQUH
# MAKGUGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRH
# NENvZGVTaWduaW5nUlNBNDA5NlNIQTM4NDIwMjFDQTEuY3J0MAwGA1UdEwEB/wQC
# MAAwDQYJKoZIhvcNAQELBQADggIBAFrOPeL4ph8SmHwwcUQO7nPnapyOS0I50w70
# nVZ9CtrgyA7hiZmVm/CsC1JU8zg1dNyfH7wCDaoMAnqtybcdmhIXc4STwfcpiKOH
# nL3fRQcZ2zCCXmX5lkWYWni9Nqx603JQ8yiUSl1sMyv0Cd4RasOBHnjQuekDDKNT
# QvOiEA3NCZDGEjtIjE+TGqLW2kUEtjxzyr0mnhmidRaHry5C1GKu0mlKExwabOLW
# xGrXj4FPtKmWXZh00lMbbdeHm1Zqn9CTsO6xt8CQXSemcpb7lXY80um71wQO23ub
# tQGDe4QpShomqPmEIVxM5/B6Yih/0Lb8mt60SLfT5EOVS/Dhd86lSHcncL9JLxaq
# WwbQhIwpEa4b3MiZqyemqb0+YIBn5yG43M4oLzRPTo2mPwG19OtnMVZsrcjGEzLz
# EiBb9/YXsf8G5LAh86x2kRKDad35NNNojUJYVBtD7MGEsL37XF+6kWXsp92on2b2
# QLEL/5ZzJHmfrJ8m0TXMb4sMSI2KnHtCvEjG2MIAnjFEvNZ1ZFsKS78mwylDyHL0
# yTuv08JqDuommKgjmyvtLEeb6OYsOnSVQIcyV4XCY1kFA8mDuIsIlbWE3Nyv94Of
# N+4jNKcDzniYb5LmKlXraIM8PjPpYb34DlNpzCDN7/tJuMFsy/NwArj1SiL630mg
# Dm0fS5OgMYIZ5zCCGeMCAQEwfTBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT
# aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExAhAI+qTPsJ3byDJ7SsgX0LBU
# MA0GCWCGSAFlAwQCAQUAoHwwEAYKKwYBBAGCNwIBDDECMAAwGQYJKoZIhvcNAQkD
# MQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJ
# KoZIhvcNAQkEMSIEINidlPKY8fhDvMGUydtrlPKd4ygjkwm3XuWUPGfex+JIMA0G
# CSqGSIb3DQEBAQUABIIBgIXMRUmt/AsM1TFQcPHlRQrQSI/pBPXcKBuYarM5XxW5
# X8OB4OVsDOObuvD1r7+vf27hslUdjPlBSTF32AeuUHetJRGAwqJM05smxu9bfePV
# j+U8Q2RhmKdln0f/QJAEYaRm441Rp82cwCeYjkyzrMqn1QmxsCRLlQATZyP0Jhlp
# sn+RSWFCAzM+YY+o14bCts4SG+UWH9UVFqO/+z6IYV8Yx1Ur/mT3OdZ2iUPYQaxW
# zhyN0qn9l3oLJ88H/Rne8c2I9LOCm6m1pIoSsNdyf2Uz5WW/Qegw7+kOAKvz3jl7
# NyL4rB1WSGB5mrecuJdOGMzpanoT6xEGye0zw9O7TmBC/+zYH98o2usDxXOK1lQ9
# Pc5XD2PGA2REmu8OOxUaSq+lPg1OomYatuKlO++Z5LMspVOIZnkT4lrBgv73nz1Z
# fh3RahSA0nNOe3oSLzjtjIGa7aRudIx+199QTh+fvBQpQVEU06VI/9BH30KpO9L+
# fd+PEatNFBphYIXoNrRE+aGCFz0wghc5BgorBgEEAYI3AwMBMYIXKTCCFyUGCSqG
# SIb3DQEHAqCCFxYwghcSAgEDMQ8wDQYJYIZIAWUDBAIBBQAwdwYLKoZIhvcNAQkQ
# AQSgaARmMGQCAQEGCWCGSAGG/WwHATAxMA0GCWCGSAFlAwQCAQUABCDBlADyA7fB
# GszKOTzX28GXf/zS8uC1Yfrj7KrZTb/7oQIQTQ1E96ZxwxZCpNyBGxbKExgPMjAy
# MjEyMDIyMDI4NDRaoIITBzCCBsAwggSooAMCAQICEAxNaXJLlPo8Kko9KQeAPVow
# DQYJKoZIhvcNAQELBQAwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0
# LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hB
# MjU2IFRpbWVTdGFtcGluZyBDQTAeFw0yMjA5MjEwMDAwMDBaFw0zMzExMjEyMzU5
# NTlaMEYxCzAJBgNVBAYTAlVTMREwDwYDVQQKEwhEaWdpQ2VydDEkMCIGA1UEAxMb
# RGlnaUNlcnQgVGltZXN0YW1wIDIwMjIgLSAyMIICIjANBgkqhkiG9w0BAQEFAAOC
# Ag8AMIICCgKCAgEAz+ylJjrGqfJru43BDZrboegUhXQzGias0BxVHh42bbySVQxh
# 9J0Jdz0Vlggva2Sk/QaDFteRkjgcMQKW+3KxlzpVrzPsYYrppijbkGNcvYlT4Dot
# jIdCriak5Lt4eLl6FuFWxsC6ZFO7KhbnUEi7iGkMiMbxvuAvfTuxylONQIMe58ty
# SSgeTIAehVbnhe3yYbyqOgd99qtu5Wbd4lz1L+2N1E2VhGjjgMtqedHSEJFGKes+
# JvK0jM1MuWbIu6pQOA3ljJRdGVq/9XtAbm8WqJqclUeGhXk+DF5mjBoKJL6cqtKc
# tvdPbnjEKD+jHA9QBje6CNk1prUe2nhYHTno+EyREJZ+TeHdwq2lfvgtGx/sK0YY
# oxn2Off1wU9xLokDEaJLu5i/+k/kezbvBkTkVf826uV8MefzwlLE5hZ7Wn6lJXPb
# wGqZIS1j5Vn1TS+QHye30qsU5Thmh1EIa/tTQznQZPpWz+D0CuYUbWR4u5j9lMNz
# IfMvwi4g14Gs0/EH1OG92V1LbjGUKYvmQaRllMBY5eUuKZCmt2Fk+tkgbBhRYLqm
# gQ8JJVPxvzvpqwcOagc5YhnJ1oV/E9mNec9ixezhe7nMZxMHmsF47caIyLBuMnnH
# C1mDjcbu9Sx8e47LZInxscS451NeX1XSfRkpWQNO+l3qRXMchH7XzuLUOncCAwEA
# AaOCAYswggGHMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB
# /wQMMAoGCCsGAQUFBwMIMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwH
# ATAfBgNVHSMEGDAWgBS6FtltTYUvcyl2mi91jGogj57IbzAdBgNVHQ4EFgQUYore
# 0GH8jzEU7ZcLzT0qlBTfUpwwWgYDVR0fBFMwUTBPoE2gS4ZJaHR0cDovL2NybDMu
# ZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1NlRpbWVT
# dGFtcGluZ0NBLmNybDCBkAYIKwYBBQUHAQEEgYMwgYAwJAYIKwYBBQUHMAGGGGh0
# dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBYBggrBgEFBQcwAoZMaHR0cDovL2NhY2Vy
# dHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZEc0UlNBNDA5NlNIQTI1NlRp
# bWVTdGFtcGluZ0NBLmNydDANBgkqhkiG9w0BAQsFAAOCAgEAVaoqGvNG83hXNzD8
# deNP1oUj8fz5lTmbJeb3coqYw3fUZPwV+zbCSVEseIhjVQlGOQD8adTKmyn7oz/A
# yQCbEx2wmIncePLNfIXNU52vYuJhZqMUKkWHSphCK1D8G7WeCDAJ+uQt1wmJefkJ
# 5ojOfRu4aqKbwVNgCeijuJ3XrR8cuOyYQfD2DoD75P/fnRCn6wC6X0qPGjpStOq/
# CUkVNTZZmg9U0rIbf35eCa12VIp0bcrSBWcrduv/mLImlTgZiEQU5QpZomvnIj5E
# IdI/HMCb7XxIstiSDJFPPGaUr10CU+ue4p7k0x+GAWScAMLpWnR1DT3heYi/HAGX
# yRkjgNc2Wl+WFrFjDMZGQDvOXTXUWT5Dmhiuw8nLw/ubE19qtcfg8wXDWd8nYive
# QclTuf80EGf2JjKYe/5cQpSBlIKdrAqLxksVStOYkEVgM4DgI974A6T2RUflzrgD
# QkfoQTZxd639ouiXdE4u2h4djFrIHprVwvDGIqhPm73YHJpRxC+a9l+nJ5e6li6F
# V8Bg53hWf2rvwpWaSxECyIKcyRoFfLpxtU56mWz06J7UWpjIn7+NuxhcQ/XQKuji
# Yu54BNu90ftbCqhwfvCXhHjjCANdRyxjqCU4lwHSPzra5eX25pvcfizM/xdMTQCi
# 2NYBDriL7ubgclWJLCcZYfZ3AYwwggauMIIElqADAgECAhAHNje3JFR82Ees/Shm
# Kl5bMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdp
# Q2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERp
# Z2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIy
# MzU5NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7
# MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1l
# U3RhbXBpbmcgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUG
# SbPBPXJJUVXHJQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOc
# iQt/nR+eDzMfUBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkr
# PkLcZ47qUT3w1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rw
# N3mfXazL6IRktFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSm
# xR3NNg1c1eYbqMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu
# 9Yemj052FVUmcJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirH
# kr+g3uM+onP65x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506
# o9UD4L/wojzKQtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklN
# iyDSLFc1eSuo80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGT
# yYwMO1uKIqjBJgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgA
# DoRU7s7pXcheMBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0T
# AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYD
# VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG
# A1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV
# HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU
# cnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s
# BwEwDQYJKoZIhvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPP
# MFPOvxj7x1Bd4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKW
# b8RQTGIdDAiCqBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpP
# kWaeLJ7giqzl/Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8oVInw1YpxdmXa
# zPByoyP6wCeCRK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKv
# xMfBwWpx2cYTgAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl6
# 3f8lY5knLD0/a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YB
# T70/O3itTK37xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4n
# LCbbbxV7HhmLNriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvt
# lUG3OtUVmDG0YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm
# 2fBldkKmKYcJRyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqh
# K/bt1nz8MIIFjTCCBHWgAwIBAgIQDpsYjvnQLefv21DiCEAYWjANBgkqhkiG9w0B
# AQwFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD
# VQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtEaWdpQ2VydCBBc3N1cmVk
# IElEIFJvb3QgQ0EwHhcNMjIwODAxMDAwMDAwWhcNMzExMTA5MjM1OTU5WjBiMQsw
# CQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cu
# ZGlnaWNlcnQuY29tMSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQw
# ggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC/5pBzaN675F1KPDAiMGkz
# 7MKnJS7JIT3yithZwuEppz1Yq3aaza57G4QNxDAf8xukOBbrVsaXbR2rsnnyyhHS
# 5F/WBTxSD1Ifxp4VpX6+n6lXFllVcq9ok3DCsrp1mWpzMpTREEQQLt+C8weE5nQ7
# bXHiLQwb7iDVySAdYyktzuxeTsiT+CFhmzTrBcZe7FsavOvJz82sNEBfsXpm7nfI
# SKhmV1efVFiODCu3T6cw2Vbuyntd463JT17lNecxy9qTXtyOj4DatpGYQJB5w3jH
# trHEtWoYOAMQjdjUN6QuBX2I9YI+EJFwq1WCQTLX2wRzKm6RAXwhTNS8rhsDdV14
# Ztk6MUSaM0C/CNdaSaTC5qmgZ92kJ7yhTzm1EVgX9yRcRo9k98FpiHaYdj1ZXUJ2
# h4mXaXpI8OCiEhtmmnTK3kse5w5jrubU75KSOp493ADkRSWJtppEGSt+wJS00mFt
# 6zPZxd9LBADMfRyVw4/3IbKyEbe7f/LVjHAsQWCqsWMYRJUadmJ+9oCw++hkpjPR
# iQfhvbfmQ6QYuKZ3AeEPlAwhHbJUKSWJbOUOUlFHdL4mrLZBdd56rF+NP8m800ER
# ElvlEFDrMcXKchYiCd98THU/Y+whX8QgUWtvsauGi0/C1kVfnSD8oR7FwI+isX4K
# Jpn15GkvmB0t9dmpsh3lGwIDAQABo4IBOjCCATYwDwYDVR0TAQH/BAUwAwEB/zAd
# BgNVHQ4EFgQU7NfjgtJxXWRM3y5nP+e6mK4cD08wHwYDVR0jBBgwFoAUReuir/SS
# y4IxLVGLp6chnfNtyA8wDgYDVR0PAQH/BAQDAgGGMHkGCCsGAQUFBwEBBG0wazAk
# BggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAC
# hjdodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURS
# b290Q0EuY3J0MEUGA1UdHwQ+MDwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcmwwEQYDVR0gBAowCDAGBgRV
# HSAAMA0GCSqGSIb3DQEBDAUAA4IBAQBwoL9DXFXnOF+go3QbPbYW1/e/Vwe9mqyh
# hyzshV6pGrsi+IcaaVQi7aSId229GhT0E0p6Ly23OO/0/4C5+KH38nLeJLxSA8hO
# 0Cre+i1Wz/n096wwepqLsl7Uz9FDRJtDIeuWcqFItJnLnU+nBgMTdydE1Od/6Fmo
# 8L8vC6bp8jQ87PcDx4eo0kxAGTVGamlUsLihVo7spNU96LHc/RzY9HdaXFSMb++h
# UD38dglohJ9vytsgjTVgHAIDyyCwrFigDkBjxZgiwbJZ9VVrzyerbHbObyMt9H5x
# aiNrIv8SuFQtJ37YOtnwtoeW/VvRXKwYw02fc7cBqZ9Xql4o4rmUMYIDdjCCA3IC
# AQEwdzBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5
# BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0
# YW1waW5nIENBAhAMTWlyS5T6PCpKPSkHgD1aMA0GCWCGSAFlAwQCAQUAoIHRMBoG
# CSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjIxMjAy
# MjAyODQ0WjArBgsqhkiG9w0BCRACDDEcMBowGDAWBBTzhyJNhjOCkjWplLy9j5bp
# /hx8czAvBgkqhkiG9w0BCQQxIgQgu+Z3zvGJCgxSpyli/Or3HLmFE/c5btycfJb+
# n8d4NrQwNwYLKoZIhvcNAQkQAi8xKDAmMCQwIgQgx/ThvjIoiSCr4iY6vhrE/E/m
# eBwtZNBMgHVXoCO1tvowDQYJKoZIhvcNAQEBBQAEggIAtjMJvwlBxWfs8zeTVZfI
# HVi/ckqYt95jqUwVzwICDb1DCvxLxbcfdKj/vDCS9kF03sj1kmE3UIVT6GOSRIY0
# fJ3mZmhRrPgk72YSvpGI8z/77OnhGwiIvjb0TFu51fjG1OlGffMyJDb/dX2HxUHz
# UU58ACWRE9oKbEy8lAMomc3sxxRXrMKLkGa2WTvU9jamU2kPzFymSgHSlkasVM04
# XTj03NDbqYSuDizLGq5Hrd+uIAUNtkgLbk7PGYWusy3IC3dZBf2MRJj1JzP2isT9
# H77CuITXm7l9z7RhPBeqzamWroGflQeW6eRorlfF2PxUlzYzZLHaClMHzAayU6se
# smdNJfFCqngI37jCoy0msUf0L7n2yXDW4vv9w6IqXYA9blXhIr+5VAl0GOGlsAKz
# NV4LS9qjVPRwkpn17EYDB4UJFTUVDjMhQkeKRssbjPSHCqcWxoBvn1LHfjjkqUBx
# rH9t90mP+oLbjIiKg0z9k6BksBXL9OlFjlJy5/p3jU6Vi4L2qaAc3bPqyc6oobfM
# 7ThBhqWlCLIUjbDi3XusCvMfgzOVuFWkfRglWS0ypWbkDlITQlCzJYHJAz2Bn+1l
# +dVl0ljeFSoAo9motPV27zb/5Em+c1XJ626sUfaTNjjmsUl0DuJj5qaA7/8aUVXm
# T0mduBIiBCNeJWl+/mrkQXI=
# SIG # End signature block