SPSWakeUP.ps1

<#PSScriptInfo
    .VERSION 5.0.0
 
    .GUID 1fc873b1-5854-46cb-8632-29cee879bb55
 
    .AUTHOR luigilink (Jean-Cyril DROUHIN)
 
    .COPYRIGHT
 
    .TAGS
    script powershell sharepoint warmup
 
    .LICENSEURI
    https://github.com/luigilink/SPSWakeUp/blob/main/LICENSE
 
    .PROJECTURI
    https://github.com/luigilink/SPSWakeUp
 
    .ICONURI
 
    .EXTERNALMODULEDEPENDENCIES
 
    .REQUIREDSCRIPTS
 
    .EXTERNALSCRIPTDEPENDENCIES
 
    .RELEASENOTES
 
    .PRIVATEDATA
#>


<#
    .SYNOPSIS
    SPSWakeUP script for SharePoint OnPremises
 
    .DESCRIPTION
    SPSWakeUp is a PowerShell script tool to warm up all site collection in your SharePoint environment.
    It's compatible with SharePoint Server Subscription Edition.
    Use WebRequest object in multi-thread to download JS, CSS and Pictures files.
    Log script results in log file, Configure automatically prerequisites for a best warm-up.
 
    .PARAMETER Action
    Use the Action parameter equal to Install if you want to add the warmup script in taskscheduler
    InstallAccount parameter need to be set
    PS D:\> E:\SCRIPT\SPSWakeUP.ps1 -Action Install -InstallAccount (Get-Credential)
 
    Use the Action parameter equal to Uninstall if you want to remove the warmup script from taskscheduler
    PS D:\> E:\SCRIPT\SPSWakeUP.ps1 -Action Uninstall
 
    Use the Action parameter equal to AdminSitesOnly if you want to warmup the Central Administration Site collection
    PS D:\> E:\SCRIPT\SPSWakeUP.ps1 -Action AdminSitesOnly
 
    .PARAMETER InstallAccount
    Need parameter InstallAccount whent you use the Action parameter equal to Install
    PS D:\> E:\SCRIPT\SPSWakeUP.ps1 -Install -InstallAccount (Get-Credential)
 
    .PARAMETER Transcript
    Use the boolean Transcript parameter if you want to start Transcrit PowerShell Feature.
    PS D:\> E:\SCRIPT\SPSWakeUP.ps1 -Transcript:$True
 
    .EXAMPLE
    SPSWakeUP.ps1 -Action Install -InstallAccount (Get-Credential)
    SPSWakeUP.ps1 -Action Uninstall
    SPSWakeUP.ps1 -Action AdminSitesOnly
    SPSWakeUP.ps1 -Transcript:$True
 
    .NOTES
    FileName: SPSWakeUP.ps1
    Authors: luigilink (Jean-Cyril DROUHIN)
                Nutsoft (Des Finkenzeller)
                bed428 (Brian D.)
 
    Date: September 01, 2026
    Version: 5.0.0
    Licence: MIT License
 
    .LINK
    https://spjc.fr/
    https://github.com/luigilink/spswakeup
#>

param
(
    [Parameter(Position = 1)]
    [validateSet('Install', 'Uninstall', 'Default', 'AdminSitesOnly', IgnoreCase = $true)]
    [System.String]
    $Action = 'Default',

    [Parameter(Position = 2)]
    [System.Management.Automation.PSCredential]
    $InstallAccount,

    [Parameter(Position = 3)]
    [System.Boolean]
    $Transcript = $false
)

#region Initialization
# Define variables
$spsWakeupVersion = '5.0.0'
$currentUser = ([Security.Principal.WindowsIdentity]::GetCurrent()).Name

# Clear the host console
Clear-Host

# Ensure the script is running with administrator privileges
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) {
    Throw "Administrator rights are required. Please re-run this script as an Administrator."
}

# Setting power management plan to High Performance"
Start-Process -FilePath "$env:SystemRoot\system32\powercfg.exe" -ArgumentList '/s 8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c' -NoNewWindow

# Start Transcript parameter is equal to True
if ($Transcript) {
    # Initialize the log file full path
    $pathLogFile = Join-Path -Path $PSScriptRoot -ChildPath ('SPSWakeUP_script_' + (Get-Date -Format yyyy-MM-dd_H-mm) + '.log')
    # Start Transcript with the log file
    Start-Transcript -Path $pathLogFile -IncludeInvocationHeader
}
#endregion

#region functions
function Add-SPSWakeUpEvent {
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Message,

        [Parameter(Mandatory = $true)]
        [System.String]
        $Source,

        [Parameter()]
        [ValidateSet('Error', 'Information', 'FailureAudit', 'SuccessAudit', 'Warning')]
        [System.String]
        $EntryType = 'Information',

        [Parameter()]
        [System.UInt32]
        $EventID = 1
    )

    $LogName = 'SPSWakeUp'

    if ([System.Diagnostics.EventLog]::SourceExists($Source)) {
        $sourceLogName = [System.Diagnostics.EventLog]::LogNameFromSourceName($Source, ".")
        if ($LogName -ne $sourceLogName) {
            Write-Verbose -Message "[ERROR] Specified source {$Source} already exists on log {$sourceLogName}"
            return
        }
    }
    else {
        if ([System.Diagnostics.EventLog]::Exists($LogName) -eq $false) {
            #Create event log
            $null = New-EventLog -LogName $LogName -Source $Source
        }
        else {
            [System.Diagnostics.EventLog]::CreateEventSource($Source, $LogName)
        }
    }

    try {
        $headerMessage = @"
SPSWakeUp Script Version: $spsWakeupVersion
User: $currentUser
ComputerName: $($env:COMPUTERNAME)
--------------------------------------------------------------
"@

        Write-EventLog -LogName $LogName -Source $Source -EventId $EventID -Message ($headerMessage + "`r`n" + $Message) -EntryType $EntryType
    }
    catch {
        Write-Error -Message @"
SPSWakeUp Script Version: $spsWakeupVersion
An error occurred while adding Event Log in Source: $Source
User: $currentUser
ComputerName: $($env:COMPUTERNAME)
Exception: $_
"@

    }
}
function Add-SPSSheduledTask {
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Management.Automation.PSCredential]
        $ExecuteAsCredential, # Credentials for Task Schedule

        [Parameter(Mandatory = $true)]
        [System.String]
        $ActionArguments, # Arguments for the task action

        [Parameter()]
        [System.String]
        $TaskName = 'SPSWakeUP', # Name of the scheduled task to be added

        [Parameter()]
        [System.String]
        $TaskPath = 'SharePoint' # Path of the task folder
    )

    # Initialize variables
    $TaskDate = Get-Date -Format yyyy-MM-dd # Current date in yyyy-MM-dd format
    $TaskCmd = 'C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe' # Path to PowerShell executable
    $UserName = $ExecuteAsCredential.UserName
    $Password = $ExecuteAsCredential.GetNetworkCredential().Password

    # Connect to the local TaskScheduler Service
    $TaskSvc = New-Object -ComObject ('Schedule.service')
    $TaskSvc.Connect($env:COMPUTERNAME)

    # Check if the folder exists, if not, create it
    try {
        $TaskFolder = $TaskSvc.GetFolder($TaskPath) # Attempt to get the task folder
    }
    catch {
        Write-Output "Task folder '$TaskPath' does not exist. Creating folder..."
        $RootFolder = $TaskSvc.GetFolder('\') # Get the root folder
        $RootFolder.CreateFolder($TaskPath) # Create the missing task folder
        $TaskFolder = $TaskSvc.GetFolder($TaskPath) # Get the newly created folder
        Write-Output "Successfully created task folder '$TaskPath'"
    }

    # Retrieve the scheduled task
    $getScheduledTask = $TaskFolder.GetTasks(0) | Where-Object -FilterScript {
        $_.Name -eq $TaskName
    }

    if ($getScheduledTask) {
        Write-Warning -Message 'Scheduled Task already exists - skipping.' # Task already exists
        [System.Runtime.InteropServices.Marshal]::ReleaseComObject($TaskSvc) | Out-Null
        Remove-Variable UserName, Password -ErrorAction SilentlyContinue
    }
    else {
        Write-Output '--------------------------------------------------------------'
        Write-Output "Adding '$TaskName' script in Task Scheduler Service ..."

        # Get credentials for Task Schedule
        $TaskAuthor = ([Security.Principal.WindowsIdentity]::GetCurrent()).Name # Author of the task
        $TaskUser = $UserName # Username for task registration
        $TaskUserPwd = $Password # Password for task registration

        # Add a new Task Schedule
        $TaskSchd = $TaskSvc.NewTask(0)
        $TaskSchd.RegistrationInfo.Description = "$($TaskName) Task - Start at 6:00 daily" # Task description
        $TaskSchd.RegistrationInfo.Author = $TaskAuthor # Task author
        $TaskSchd.Principal.RunLevel = 1 # Task run level (1 = Highest)

        # Task Schedule - Modify Settings Section
        $TaskSettings = $TaskSchd.Settings
        $TaskSettings.AllowDemandStart = $true
        $TaskSettings.Enabled = $true
        $TaskSettings.Hidden = $false
        $TaskSettings.StartWhenAvailable = $true

        # Task Schedule - Trigger Section
        $TaskTriggers = $TaskSchd.Triggers

        # Add Trigger Type 2 OnSchedule Daily Start at 6:00 AM
        $TaskTrigger1 = $TaskTriggers.Create(2)
        $TaskTrigger1.StartBoundary = $TaskDate + 'T06:00:00'
        $TaskTrigger1.DaysInterval = 1
        $TaskTrigger1.Enabled = $true

        # Add Trigger Type 8 At StartUp Delay 10M
        $TaskTrigger2 = $TaskTriggers.Create(8)
        $TaskTrigger2.Delay = 'PT10M'
        $TaskTrigger2.Enabled = $true

        # Add Trigger Type 0 OnEvent IISReset
        $TrigSubscription =
        @"
<QueryList><Query Id="0" Path="System"><Select Path="System">*[System[Provider[@Name='Microsoft-Windows-IIS-IISReset'] and EventID=3201]]</Select></Query></QueryList>
"@

        $TaskTrigger3 = $TaskTriggers.Create(0)
        $TaskTrigger3.Delay = 'PT20S'
        $TaskTrigger3.Subscription = $TrigSubscription
        $TaskTrigger3.Enabled = $true

        # Define the task action
        $TaskAction = $TaskSchd.Actions.Create(0) # 0 = Executable action
        $TaskAction.Path = $TaskCmd # Path to the executable
        $TaskAction.Arguments = $ActionArguments # Arguments for the executable

        try {
            # Register the task
            $TaskFolder.RegisterTaskDefinition($TaskName, $TaskSchd, 6, $TaskUser, $TaskUserPwd, 1)
            Write-Output "Successfully added '$TaskName' script in Task Scheduler Service"
            Add-SPSWakeUpEvent -Message "Successfully added '$TaskName' script in Task Scheduler Service" -Source 'Add-SPSSheduledTask' -EntryType 'Information'
        }
        catch {
            $catchMessage = @"
An error occurred while adding the script in scheduled task: $($TaskName)
ActionArguments: $($ActionArguments)
Exception: $($_.Exception.Message)
"@

            Write-Error -Message $catchMessage # Handle any errors during task registration
            Add-SPSWakeUpEvent -Message $catchMessage -Source 'Add-SPSSheduledTask' -EntryType 'Error'
        }
        finally {
            # Release COM objects
            [System.Runtime.InteropServices.Marshal]::ReleaseComObject($TaskSvc) | Out-Null
            # Remove variables
            Remove-Variable TaskSchd, TaskSettings, TaskTriggers, TaskTrigger1, TaskTrigger2, TaskTrigger3, TaskAction, TaskAuthor, TaskUser, TaskUserPwd -ErrorAction SilentlyContinue
        }
    }
}
function Remove-SPSSheduledTask {
    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory = $true)]
        [System.String]
        $TaskName, # Name of the scheduled task to be removed

        [Parameter()]
        [System.String]
        $TaskPath = 'SharePoint' # Path of the task folder
    )

    # Connect to the local TaskScheduler Service
    $TaskSvc = New-Object -ComObject ('Schedule.service')
    $TaskSvc.Connect($env:COMPUTERNAME)

    # Check if the folder exists
    try {
        $TaskFolder = $TaskSvc.GetFolder($TaskPath) # Attempt to get the task folder
    }
    catch {
        Write-Output "Task folder '$TaskPath' does not exist."
        return
    }

    # Retrieve the scheduled task
    $getScheduledTask = $TaskFolder.GetTasks(0) | Where-Object -FilterScript {
        $_.Name -eq $TaskName
    }

    if ($null -eq $getScheduledTask) {
        Write-Warning -Message 'Scheduled Task already removed - skipping.' # Task not found
    }
    else {
        Write-Output '--------------------------------------------------------------'
        Write-Output "Removing $($TaskName) script in Task Scheduler Service ..."
        if ($PSCmdlet.ShouldProcess($TaskName, 'Remove scheduled task')) {
            try {
                $TaskFolder.DeleteTask($TaskName, $null) # Remove the task
                Write-Output "Successfully removed $($TaskName) script from Task Scheduler Service"
                Add-SPSWakeUpEvent -Message "Successfully removed '$TaskName' script from Task Scheduler Service" -Source 'Remove-SPSSheduledTask' -EntryType 'Information'
            }
            catch {
                $catchMessage = @"
An error occurred while removing the script in scheduled task: $($TaskName)
Exception: $($_.Exception.Message)
"@

                Write-Error -Message $catchMessage # Handle any errors during task removal
                Add-SPSWakeUpEvent -Message $catchMessage -Source 'Remove-SPSSheduledTask' -EntryType 'Error'
            }
            finally {
                # Release COM objects
                [System.Runtime.InteropServices.Marshal]::ReleaseComObject($TaskSvc) | Out-Null
            }
        }
    }
}
function Install-SPSWakeUP {
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Path,

        [Parameter(Position = 2)]
        [System.Management.Automation.PSCredential]
        $InstallAccount
    )

    # Initialize variables
    # Quote script path to support scheduled task execution when path contains spaces.
    $escapedScriptPath = $Path.Replace('"', '""')
    $ActionArguments = "-ExecutionPolicy Bypass -File `"$escapedScriptPath`""
    $UserName = $InstallAccount.UserName
    $Password = $InstallAccount.GetNetworkCredential().Password
    $currentDomain = 'LDAP://' + ([ADSI]'').distinguishedName
    Write-Output "Checking Account `"$UserName`" ..."
    $dom = New-Object System.DirectoryServices.DirectoryEntry($currentDomain, $UserName, $Password)
    if ($null -eq $dom.Path) {
        Write-Warning -Message "Password Invalid for user:`"$UserName`""
        Add-SPSWakeUpEvent -Message "Password Invalid for user:`"$UserName`"" -Source 'SPSWakeUP' -EntryType 'Error'
        Exit
    }
    else {
        Write-Output "Account `"$UserName`" is valid. Adding SPSWakeUp script in Task Scheduler Service ..."
        # 1. Add SPSWakeup script in a new scheduled Task
        Add-SPSSheduledTask -ExecuteAsCredential $InstallAccount -ActionArguments $ActionArguments

        # 2. Get All Web Applications Urls
        $getSPWebApps = Get-SPSWebAppUrl

        # 3. Add read access for Warmup User account in User Policies settings
        if ($null -ne $getSPWebApps) {
            Add-SPSUserPolicy -Urls $getSPWebApps -UserName $UserName
        }
    }
    # Remove variables
    Remove-Variable ActionArguments, escapedScriptPath, UserName, Password, currentDomain, dom
}
function Clear-SPSLog {
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $path,

        [Parameter()]
        [System.UInt32]
        $days = 30
    )

    if (Test-Path $path) {
        # Define LastWriteTime parameter based on $days
        $LastWrite = (Get-Date).AddDays(-$days)
        # Get files based on lastwrite filter and specified folder
        $files = Get-ChildItem -Path $path -Filter '*.log' | Where-Object -FilterScript {
            $_.LastWriteTime -le "$LastWrite"
        }

        if ($files) {
            Write-Output "Cleaning log files in $path ..."
            foreach ($file in $files) {
                Write-Output "Deleting file $file ..."
                Remove-Item $file.FullName | Out-Null
            }
        }
    }
}
function Get-SPSThrottleLimit {
    # Get Number Of Throttle Limit
    process {
        try {
            $cimInstanceProc = @(Get-CimInstance -ClassName Win32_Processor)
            $cimInstanceSocket = $cimInstanceProc.count
            $numLogicalCpu = $cimInstanceProc[0].NumberOfLogicalProcessors * $cimInstanceSocket
            $NumThrottle = @{ $true = 10; $false = 2 * $numLogicalCpu }[$numLogicalCpu -ge 8]
            return $NumThrottle
        }
        catch {
            Write-Warning -Message $_
        }
    }
}
function Add-SPSHostEntry {
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $url
    )

    $url = $url -replace 'https://', ''
    $url = $url -replace 'http://', ''
    $hostNameEntry = $url.split('/')[0]
    [void]$hostEntries.Add($hostNameEntry)
}
function Get-SPSAdminUrl {
    try {
        # Initialize ArrayList Object
        $tbCASitesURL = New-Object -TypeName System.Collections.ArrayList
        # Get Central Administration Url and add it in ArrayList Object
        $webAppADM = Get-SPWebApplication -IncludeCentralAdministration | Where-Object -FilterScript {
            $_.IsAdministrationWebApplication
        }
        [void]$tbCASitesURL.Add("$($webAppADM.Url)")
        # List of the most useful administration pages and Quick launch top links
        $urlsAdmin = @('Lists/HealthReports/AllItems.aspx', `
                '_admin/FarmServers.aspx', `
                '_admin/Server.aspx', `
                '_admin/WebApplicationList.aspx', `
                '_admin/ServiceApplications.aspx', `
                'applications.aspx', `
                'systemsettings.aspx', `
                'monitoring.aspx', `
                'backups.aspx', `
                'security.aspx', `
                'upgradeandmigration.aspx', `
                'apps.aspx', `
                'generalapplicationsettings.aspx')
        foreach ($urlAdmin in $urlsAdmin) {
            [void]$tbCASitesURL.Add("$($webAppADM.Url)$($urlAdmin)")
        }
        # Get Service Application Urls and then in ArrayList Object
        $sa = Get-SPServiceApplication
        $linkUrls = $sa | ForEach-Object { $_.ManageLink.Url } | Select-Object -Unique
        foreach ($linkUrl in $linkUrls) {
            $siteADMSA = $linkUrl.TrimStart('/')
            [void]$tbCASitesURL.Add("$($webAppADM.Url)$($siteADMSA)")
        }
        return $tbCASitesURL
    }
    catch {
        Write-Warning -Message $_
    }
}
function Get-SPSPreferredUrl {
    <#
    .SYNOPSIS
        Returns the URL to use for local warm-up, choosing the scheme from the
        zone's real IIS bindings instead of the (possibly offloaded) public URL.
 
    .DESCRIPTION
        In an SSL offloading architecture, the Web Front End listens in HTTP only
        (no 443 binding) while the Default zone public URL is HTTPS. SPSWakeUp warms
        up the site collections locally (HOSTS entries pointing to 127.0.0.1), so it
        must target the scheme that IIS actually listens on locally, not the public
        (offloaded) scheme.
 
        Rule: if the zone exposes an HTTP ServerBindings entry, warm up in HTTP;
        otherwise (SecureBindings only) use HTTPS. The host header and path of the
        original URL are preserved, which also handles web applications exposing
        several bindings / host headers on the same zone.
 
        When the IIS bindings cannot be read (e.g. object model unavailable), the
        original URL is returned unchanged, preserving the previous behaviour.
 
    .PARAMETER WebApplication
        The SPWebApplication object owning the zone to inspect.
 
    .PARAMETER Zone
        The SPUrlZone to inspect. Defaults to 'Default'.
 
    .PARAMETER OriginalUrl
        The public/AAM URL whose host header and path must be preserved. When omitted,
        the zone response URI of the web application is used.
    #>

    [OutputType([System.String])]
    param
    (
        [Parameter(Mandatory = $true)]
        $WebApplication,

        [Parameter(Mandatory = $false)]
        $Zone = 'Default',

        [Parameter(Mandatory = $false)]
        [System.String]
        $OriginalUrl
    )

    # Determine the reference URL whose host header + path must be preserved.
    $referenceUrl = $OriginalUrl
    if ([string]::IsNullOrEmpty($referenceUrl)) {
        try {
            $referenceUrl = $WebApplication.GetResponseUri($Zone).AbsoluteUri
        }
        catch {
            $referenceUrl = $null
        }
    }
    if ([string]::IsNullOrEmpty($referenceUrl)) {
        return $referenceUrl
    }

    try {
        $refUri = [System.Uri]$referenceUrl
    }
    catch {
        return $referenceUrl
    }

    # Read the real IIS bindings for the zone. Be defensive: the object model may be
    # unavailable (unit tests) or the zone may not be extended.
    $httpBindings = @()
    $httpsBindings = @()
    try {
        if ($null -ne $WebApplication -and $null -ne $WebApplication.IisSettings) {
            $iis = $WebApplication.IisSettings[$Zone]
            if ($null -ne $iis) {
                $httpBindings = @($iis.ServerBindings | Where-Object { $null -ne $_ })
                $httpsBindings = @($iis.SecureBindings | Where-Object { $null -ne $_ })
            }
        }
    }
    catch {
        return $referenceUrl
    }

    # No binding information available -> keep the original URL untouched.
    if ($httpBindings.Count -eq 0 -and $httpsBindings.Count -eq 0) {
        return $referenceUrl
    }

    # Prefer HTTP when the zone exposes an HTTP ServerBinding (covers SSL offloading);
    # otherwise use HTTPS (SecureBindings only).
    if ($httpBindings.Count -gt 0) {
        $scheme = 'http'
        $defaultPort = 80
        $selectedBindings = $httpBindings
    }
    else {
        $scheme = 'https'
        $defaultPort = 443
        $selectedBindings = $httpsBindings
    }

    # Preserve the host header from the reference URL (handles multiple host headers).
    $hostHeader = $refUri.Host

    # Keep a non-standard port when the matching binding uses one.
    $port = $defaultPort
    foreach ($binding in $selectedBindings) {
        $bindingHost = $binding.HostHeader
        $bindingPort = $binding.Port
        if (-not [string]::IsNullOrEmpty($bindingHost)) {
            if ($bindingHost -eq $hostHeader -and $null -ne $bindingPort) {
                $port = [int]$bindingPort
                break
            }
        }
        elseif ($null -ne $bindingPort) {
            # Catch-all binding (no host header) -> use its port.
            $port = [int]$bindingPort
        }
    }

    $pathAndQuery = $refUri.PathAndQuery
    if ([string]::IsNullOrEmpty($pathAndQuery)) {
        $pathAndQuery = '/'
    }

    if ($port -eq $defaultPort) {
        return ('{0}://{1}{2}' -f $scheme, $hostHeader, $pathAndQuery)
    }
    else {
        return ('{0}://{1}:{2}{3}' -f $scheme, $hostHeader, $port, $pathAndQuery)
    }
}
function Get-SPSSitesUrl {
    try {
        # Initialize ArrayList Object
        $tbSitesURL = New-Object -TypeName System.Collections.ArrayList
        # Get Url of all site collection
        $webApps = Get-SPWebApplication -ErrorAction SilentlyContinue
        if ($null -ne $webApps) {
            foreach ($webApp in $webApps) {
                foreach ($site in $webApp.sites) {
                    if ($site.RootWeb.Url -notmatch 'sitemaster-') {
                        # Use the scheme that IIS actually listens on locally (SSL offloading aware)
                        $preferredUrl = Get-SPSPreferredUrl -WebApplication $webApp -Zone 'Default' -OriginalUrl $site.RootWeb.Url
                        [void]$tbSitesURL.Add("$($preferredUrl)")
                    }
                    $site.Dispose()
                }
            }
        }
        return $tbSitesURL
    }
    catch {
        Write-Warning -Message $_
    }
}
function Get-SPSWebAppUrl {
    try {
        # Initialize ArrayList Object
        $webAppURL = New-Object -TypeName System.Collections.ArrayList
        # hostEntries is provided by Invoke-SPSAllSite; initialize locally when
        # Get-SPSWebAppUrl is called from other flows (e.g. Install).
        if (-not (Get-Variable -Name hostEntries -Scope 1 -ErrorAction SilentlyContinue)) {
            Set-Variable -Name hostEntries -Scope Local -Value (New-Object -TypeName System.Collections.Generic.List[string])
        }
        # Get SPwebApplication Object
        $webApps = Get-SPWebApplication -ErrorAction SilentlyContinue
        if ($null -ne $webApps) {
            foreach ($webapp in $webApps) {
                # Use the scheme that IIS actually listens on locally (SSL offloading aware)
                $responseUri = Get-SPSPreferredUrl -WebApplication $webapp -Zone 'Default' -OriginalUrl $webapp.GetResponseUri('Default').AbsoluteUri
                [void]$webAppURL.Add($responseUri)
                if ($null -eq (Get-SPServer | Where-Object { $responseUri -match $_.Name })) {
                    Add-SPSHostEntry -Url $responseUri
                }
            }
            $sites = $webApps | ForEach-Object -Process {
                $_.sites
            }
            $HSNCs = $sites | Where-Object -FilterScript {
                $_.HostHeaderIsSiteName -eq $true
            }
            foreach ($HSNC in $HSNCs) {
                if ($HSNC.Url -notmatch 'sitemaster-') {
                    # Resolve the preferred scheme from the HSNC's own web application
                    $preferredHSNCUrl = Get-SPSPreferredUrl -WebApplication $HSNC.WebApplication -Zone 'Default' -OriginalUrl $HSNC.Url
                    [void]$webAppURL.Add($preferredHSNCUrl)
                    Add-SPSHostEntry -Url $preferredHSNCUrl
                }
                $HSNC.Dispose()
            }
        }
        else {
            $webAppURL = $null
        }
        return $webAppURL
    }
    catch {
        Write-Warning -Message $_
    }
}
function Invoke-SPSWebRequest {
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String[]]
        $Urls,

        [Parameter(Mandatory = $true)]
        $throttleLimit
    )

    $ScriptBlock =
    {
        param
        (
            [Parameter(Mandatory = $true)]
            [ValidateNotNullOrEmpty()]
            [System.String]
            $Uri,

            [Parameter()]
            $Useragent = [Microsoft.PowerShell.Commands.PSUserAgent]::Chrome,

            [Parameter()]
            $SessionWeb
        )

        Process {
            try {
                $startProcess = Get-Date
                if ($null -ne $SessionWeb) {
                    $webResponse = Invoke-WebRequest -Uri $Uri `
                        -WebSession $SessionWeb `
                        -TimeoutSec 90 `
                        -UserAgent $Useragent `
                        -UseBasicParsing
                }
                else {
                    $webResponse = Invoke-WebRequest -Uri $Uri `
                        -UseDefaultCredentials `
                        -TimeoutSec 90 `
                        -UserAgent $Useragent `
                        -UseBasicParsing
                }
                $timeExec = '{0:N2}' -f (((Get-Date) - $startProcess).TotalSeconds)
                $Response = "$([System.int32]$webResponse.StatusCode) - $($webResponse.StatusDescription)"
            }
            catch {
                $Response = $_.Exception.Message
            }
            finally {
                if ($webResponse) {
                    $webResponse.Close()
                    Remove-Variable webResponse
                }
            }
            $RunResult = New-Object PSObject
            $RunResult | Add-Member -MemberType NoteProperty -Name Url -Value $Uri
            $RunResult | Add-Member -MemberType NoteProperty -Name 'Time(s)' -Value $TimeExec
            $RunResult | Add-Member -MemberType NoteProperty -Name Status -Value $Response
            $RunResult
        }
    }

    try {
        # Initialize variables and runpsace for Multi-Threading Request
        $psUserAgent = [Microsoft.PowerShell.Commands.PSUserAgent]::Chrome
        $Jobs = [System.Collections.Generic.List[object]]::new()
        $iss = [system.management.automation.runspaces.initialsessionstate]::CreateDefault()
        $Pool = [runspacefactory]::CreateRunspacePool(1, $throttleLimit, $iss, $Host)
        $Pool.Open()
    }
    catch {
        $catchMessage = @"
An error occurred invoking CreateRunspacePool Method
Exception: $($_.Exception.Message)
"@

        Write-Error -Message $catchMessage # Handle any errors during task removal
        Add-SPSWakeUpEvent -Message $catchMessage -Source 'CreateRunspacePool' -EntryType 'Error'
    }
    try {
        # Initialize WebSession from First SPWebApplication object
        $webApp = Get-SpWebApplication | Select-Object -first 1
        # Use the scheme that IIS actually listens on locally (SSL offloading aware)
        $authentBaseUrl = Get-SPSPreferredUrl -WebApplication $webApp -Zone 'Default' -OriginalUrl $webApp.GetResponseUri('Default').AbsoluteUri
        $authentUrl = ("$($authentBaseUrl)" + '_windows/default.aspx?ReturnUrl=/_layouts/15/Authenticate.aspx?Source=%2f')
        Write-Output "Getting webSession by opening $($authentUrl) with Invoke-WebRequest"
        Invoke-WebRequest -Uri $authentUrl `
            -SessionVariable webSession `
            -UseDefaultCredentials `
            -TimeoutSec 90 `
            -UserAgent $psUserAgent `
            -UseBasicParsing
    }
    catch {
        $catchMessage = @"
An error occurred invoking Invoke-WebRequest Function
Exception: $($_.Exception.Message)
"@

        Write-Error -Message $catchMessage # Handle any errors during task removal
        Add-SPSWakeUpEvent -Message $catchMessage -Source 'Invoke-WebRequest' -EntryType 'Error'
    }
    foreach ($Url in $Urls) {
        try {
            if (-not([string]::IsNullOrEmpty($Url))) {
                $Job = [powershell]::Create().AddScript($ScriptBlock).AddParameter('Uri', $Url).AddParameter('UserAgent', $psUserAgent).AddParameter('SessionWeb', $webSession)
                $Job.RunspacePool = $Pool
                $Jobs.Add([PSCustomObject]@{
                    Url    = $Url
                    Pipe   = $Job
                    Result = $Job.BeginInvoke()
                })
            }
        }
        catch {
            $catchMessage = @"
An error occurred invoking Create Method for Url: $($Url)
Exception: $($_.Exception.Message)
"@

            Write-Error -Message $catchMessage # Handle any errors during task removal
            Add-SPSWakeUpEvent -Message $catchMessage -Source 'Invoke-SPSWebRequest' -EntryType 'Error'
        }

    }

    While ($Jobs.Result.IsCompleted -contains $false) {
        Start-Sleep -S 1
    }

    $Results = [System.Collections.Generic.List[object]]::new()
    foreach ($Job in $Jobs) {
        try {
            $Results.Add($Job.Pipe.EndInvoke($Job.Result))
        }
        catch {
            $catchMessage = @"
An error occurred invoking Job Pipe EndInvoke Method for Url: $($Job.Url)
Exception: $($_.Exception.Message)
"@

            Write-Error -Message $catchMessage # Handle any errors during task removal
            Add-SPSWakeUpEvent -Message $catchMessage -Source 'Invoke-SPSWebRequest' -EntryType 'Error'
        }
        finally {
            $Job.Pipe.Dispose()
        }
    }
    $Pool.Dispose()
    $Results
}
function Invoke-SPSAdminSite {
    # Disable LoopBack Check
    Disable-LoopbackCheck
    # Disable IE First Run
    Disable-IEFirstRun
    # Get Central Administration Service Instance
    $serviceInstance = Get-SPServiceInstance -Server $env:COMPUTERNAME
    if ($null -eq $serviceInstance) {
        $domain = (Get-CimInstance -ClassName Win32_ComputerSystem).Domain
        $fqdn = "$($env:COMPUTERNAME).$domain"
        $serviceInstance = Get-SPServiceInstance -Server $fqdn
    }
    if ($null -ne $serviceInstance) {
        $serviceInstance = $serviceInstance | Where-Object -FilterScript {
            $_.GetType().Name -eq "SPWebServiceInstance" -and
            $_.Name -eq "WSS_Administration"
        }
    }
    if ($null -ne $serviceInstance) {
        Write-Output 'Opening All Central Admin Urls with Invoke-WebRequest, Please Wait...'
        $getSPADMSites = Get-SPSAdminUrl
        foreach ($spADMUrl in $getSPADMSites) {
            try {
                $startInvoke = Get-Date
                $argsInvokeWebReq = @{
                    Uri                   = $spADMUrl
                    UseDefaultCredentials = $true
                    TimeoutSec            = 90
                    UserAgent             = [Microsoft.PowerShell.Commands.PSUserAgent]::Chrome
                    UseBasicParsing       = $true
                }
                $webResponse = Invoke-WebRequest @argsInvokeWebReq
                $TimeExec = '{0:N2}' -f (((Get-Date) - $startInvoke).TotalSeconds)
                Write-Output '-----------------------------------'
                Write-Output "| Url : $spADMUrl"
                Write-Output "| Time : $TimeExec"
                Write-Output "| Status : $($webResponse.StatusCode) - $($webResponse.StatusDescription)"
            }
            catch {
                $catchMessage = @"
An error occurred with Invoke-WebRequest CMDLet
Url: $($spADMUrl)
Exception: $($_.Exception.Message)
"@

                Write-Error -Message $catchMessage
                Add-SPSWakeUpEvent -Message $catchMessage -Source 'Invoke-SPSAdminSite' -EntryType 'Error'
            }
        }
    }
    else {
        Write-Warning -Message "No Central Admin Service Instance running on $env:COMPUTERNAME"
        Add-SPSWakeUpEvent -Message "No Central Admin Service Instance running on $env:COMPUTERNAME" -Source 'Invoke-SPSAdminSite' -EntryType 'Warning'
    }
}
function Invoke-SPSAllSite {
    # Initialize variables
    $DateStarted = Get-Date
    $hostEntries = New-Object -TypeName System.Collections.Generic.List[string]
    $hostsFile = "$env:windir\System32\drivers\etc\HOSTS"
    $hostsFileCopy = $hostsFile + '.' + (Get-Date -UFormat "%y%m%d%H%M%S").ToString() + '.copy'
    # Get All Web Applications Urls, Host Named Site Collection and Site Collections
    Write-Output '--------------------------------------------------------------'
    Write-Output 'Get URLs of All Web Applications ...'
    $getSPWebApps = Get-SPSWebAppUrl
    # Filter out null or empty URLs (orphaned objects)
    if ($null -ne $getSPWebApps) {
        $originalWebAppCount = $getSPWebApps.Count
        $getSPWebApps = $getSPWebApps | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        $filteredWebAppCount = $getSPWebApps.Count
        if ($filteredWebAppCount -lt $originalWebAppCount) {
            Write-Warning "Filtered out $($originalWebAppCount - $filteredWebAppCount) null or empty Web Application URL(s)"
        }
    }

    Write-Output '--------------------------------------------------------------'
    Write-Output 'Get URLs of All Site Collection ...'
    $getSPSites = Get-SPSSitesUrl
    # Filter out null or empty URLs (orphaned SPSite objects)
    if ($null -ne $getSPSites) {
        $originalSiteCount = $getSPSites.Count
        $getSPSites = $getSPSites | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }
        $filteredSiteCount = $getSPSites.Count
        if ($filteredSiteCount -lt $originalSiteCount) {
            Write-Warning "Filtered out $($originalSiteCount - $filteredSiteCount) null or empty Site Collection URL(s) - possible orphaned SPSite objects"
        }
    }

    if (($null -ne $getSPWebApps -and $getSPWebApps.Count -gt 0) -and ($null -ne $getSPSites -and $getSPSites.Count -gt 0)) {
        if ($hostEntries) {
            # Disable LoopBack Check
            Disable-LoopbackCheck
            # Disable IE First Run
            Disable-IEFirstRun
            # Remove Duplicate Entries
            $hostEntries = $hostEntries | Get-Unique
            # Initialize variables
            $hostFileNeedsBackup = $true
            $hostIPV4Addr = '127.0.0.1'
            # Make backup copy of the Hosts file with today's date Add Web Application and Host Named Site Collection Urls in HOSTS system File
            Write-Output '--------------------------------------------------------------'
            Write-Output 'Add Urls of All Web Applications or HSNC in HOSTS File ...'
            foreach ($hostEntry in $hostEntries) {
                $hostEntryIsPresent = Select-String -Path $hostsFile -Pattern $hostEntry
                if ($null -eq $hostEntryIsPresent) {
                    if ($hostFileNeedsBackup) {
                        Write-Verbose -Message "Backing up $hostsFile file to: $hostsFileCopy"
                        Copy-Item -Path $hostsFile -Destination $hostsFileCopy -Force
                        $hostFileNeedsBackup = $false
                    }
                    # Remove http or https information to keep only HostName or FQDN
                    if ($hostEntry.Contains(':')) {
                        Write-Warning -Message "$hostEntry cannot be added in HOSTS File, only web applications with 80 or 443 port are added."
                    }
                    else {
                        Write-Output "Adding $($hostEntry) in HOSTS file"
                        Add-Content -Path $hostsFile -value "$hostIPV4Addr `t $hostEntry"
                    }
                }
            }
        }
        # Request Url with Invoke-WebRequest CmdLet for All Urls
        Write-Output '--------------------------------------------------------------'
        Write-Output 'Opening All sites Urls with Invoke-WebRequest, Please Wait...'
        
        # Additional validation before calling Invoke-SPSWebRequest
        if ($null -ne $getSPSites -and $getSPSites.Count -gt 0) {
            $InvokeResults = Invoke-SPSWebRequest -Urls $getSPSites -throttleLimit (Get-SPSThrottleLimit)
            # Show the results
            foreach ($InvokeResult in $InvokeResults) {
                if ($null -ne $InvokeResult.Url) {
                    Write-Output '-----------------------------------'
                    Write-Output "| Url : $($InvokeResult.Url)"
                    Write-Output "| Time : $($InvokeResult.'Time(s)') seconds"
                    Write-Output "| Status : $($InvokeResult.Status)"
                }
            }
            $DateEnded = Get-Date
            $totalUrls = $getSPSites.Count
            $totalDuration = ($DateEnded - $DateStarted).TotalSeconds

            $outputMessage = @"
-------------------------------------
| SPSWakeUp Script - Invoke-SPSAllSite
| PowerShell Version : $($PSVersionTable.PSVersion)
| Started on : $DateStarted
| Completed on : $DateEnded
| SPSWakeUp waked up $totalUrls urls in $totalDuration seconds
--------------------------------------------------------------
| REPORTING: Memory Usage for each worker process (W3WP.EXE)
| Process Creation Date | Memory | Application Pool Name
--------------------------------------------------------------
"@


            $w3wpProcess = Get-CimInstance Win32_Process -Filter "name = 'w3wp.exe'" | Select-Object WorkingSetSize, CommandLine, CreationDate | Sort-Object CommandLine
            foreach ($w3wpProc in $w3wpProcess) {
                $w3wpProcCmdLine = $w3wpProc.CommandLine.Replace('c:\windows\system32\inetsrv\w3wp.exe -ap "', '')
                $pos = $w3wpProcCmdLine.IndexOf('"')
                $appPoolName = $w3wpProcCmdLine.Substring(0, $pos)
                $w3wpMemoryUsage = [Math]::Round($w3wpProc.WorkingSetSize / 1MB)
                $outputMessage += ("`r`n" + "| $($w3wpProc.CreationDate) | $($w3wpMemoryUsage) MB | $($appPoolName)")
            }
            Write-Output $outputMessage
            Add-SPSWakeUpEvent -Message $outputMessage -Source 'Invoke-SPSAllSite'-EntryType Information
        }
        else {
            Write-Warning 'No site URLs found to process. $getSPSites is null or empty.'
            $warningMessage = "SPSWakeUp Script - No site URLs found to wake up. Please verify that Get-SPSSitesUrl returned valid URLs."
            Write-Output $warningMessage
            Add-SPSWakeUpEvent -Message $warningMessage -Source 'Invoke-SPSAllSite' -EntryType Warning
        }

        # Clean the copy files of system HOSTS folder
        Clear-HostsFileCopy -hostsFilePath $hostsFile
    }
    else {
        Write-Warning 'Cannot proceed: Web Applications or Site Collections not found or are empty.'
        if ($null -eq $getSPWebApps -or $getSPWebApps.Count -eq 0) {
            Write-Warning 'No Web Applications found. Please verify that Get-SPWebApplication returned valid URLs.'
        }
        if ($null -eq $getSPSites -or $getSPSites.Count -eq 0) {
            Write-Warning 'No Site Collections found. Please verify that Get-SPSite returned valid URLs.'
        }
        $errorMessage = "SPSWakeUp Script - Cannot proceed: insufficient data (Web Applications count: $($getSPWebApps.Count), Site Collections count: $($getSPSites.Count))"
        Add-SPSWakeUpEvent -Message $errorMessage -Source 'Invoke-SPSAllSite' -EntryType Warning
    }
}
function Disable-LoopbackCheck {
    $lsaPath = 'HKLM:\System\CurrentControlSet\Control\Lsa'
    $lsaPathValue = Get-ItemProperty -path $lsaPath
    if (-not ($lsaPathValue.DisableLoopbackCheck -eq '1')) {
        Write-Output 'Disabling Loopback Check...'
        New-ItemProperty -Path HKLM:\System\CurrentControlSet\Control\Lsa -Name 'DisableLoopbackCheck' -value '1' -PropertyType dword -Force | Out-Null
    }
    else {
        Write-Output 'Loopback Check already Disabled - skipping.'
    }
}
function Disable-IEFirstRun {
    $iefirstrunPath = 'HKCU:\Software\Microsoft\Internet Explorer\Main'
    $iefirstrunPathValue = Get-ItemProperty -path $iefirstrunPath
    if (-not ($iefirstrunPathValue.DisableFirstRunCustomize -eq '1')) {
        Write-Output 'Disabling IE First Run...'
        New-ItemProperty -Path $iefirstrunPath -Name 'DisableFirstRunCustomize' -value '1' -PropertyType dword -Force | Out-Null
    }
    else {
        Write-Output 'IE First Run already Disabled - skipping.'
    }
}
function Clear-HostsFileCopy {
    Param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $hostsFilePath,

        [Parameter()]
        [System.UInt32]
        $numberFiles = 10
    )

    $hostsFolderPath = Split-Path $hostsFilePath
    if (Test-Path $hostsFolderPath) {
        # Get files with .copy extension, sort them by name, from most recent to oldest and skip the first numberFiles variable
        $copyFiles = Get-ChildItem -Path $hostsFolderPath -Filter '*.copy' | Sort-Object -Descending -Property Name | Select-Object -Skip $numberFiles
        if ($copyFiles) {
            Write-Output '--------------------------------------------------------------'
            Write-Output "Cleaning backup HOSTS files in $hostsFolderPath ..."
            foreach ($copyFile in $copyFiles) {
                if ($null -ne $copyFile) {
                    Write-Output " * Deleting File $copyFile ..."
                    Remove-Item $copyFile.FullName | Out-Null
                }
            }
        }
    }
}
function Add-SPSUserPolicy {
    param
    (
        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        $Urls,

        [Parameter(Mandatory = $true)]
        [ValidateNotNullOrEmpty()]
        [System.String]
        $UserName
    )

    Write-Output '--------------------------------------------------------------'
    Write-Output "Add Read Access to $UserName for All Web Applications ..."
    foreach ($url in $Urls) {
        try {
            $webapp = [Microsoft.SharePoint.Administration.SPWebApplication]::Lookup("$url")
            $displayName = 'SPSWakeUP Account'

            # If the web app is not Central Administration
            if ($webapp.IsAdministrationWebApplication -eq $false) {
                # If the web app is using Claims auth, change the user accounts to the proper syntax
                if ($webapp.UseClaimsAuthentication -eq $true) {
                    $user = (New-SPClaimsPrincipal -identity $UserName -identitytype 1).ToEncodedString()
                }
                else {
                    $user = $UserName
                }
                Write-Output "Checking Read access for $user account to $url..."
                [Microsoft.SharePoint.Administration.SPPolicyCollection]$policies = $webapp.Policies
                $policyExist = $policies | Where-Object -FilterScript {
                    $_.Displayname -eq 'SPSWakeUP Account'
                }

                if (-not ($policyExist)) {
                    Write-Output "Applying Read access for $user account to $url..."
                    [Microsoft.SharePoint.Administration.SPPolicy]$policy = $policies.Add($user, $displayName)
                    $policyRole = $webApp.PolicyRoles.GetSpecialRole([Microsoft.SharePoint.Administration.SPPolicyRoleType]::FullRead)
                    if ($null -ne $policyRole) {
                        $policy.PolicyRoleBindings.Add($policyRole)
                    }
                    $webapp.Update()
                    Write-Output "Done Applying Read access for `"$user`" account to `"$url`""
                }
            }
        }
        catch {
            $catchMessage = @"
An error occurred while adding SPWebApp Policy for UserName: $UserName
Url: $($url)
Exception: $($_.Exception.Message)
"@

            Write-Error -Message $catchMessage
            Add-SPSWakeUpEvent -Message $catchMessage -Source 'SPSWakeUP' -EntryType 'Error'
        }
    }
}
function Set-SPSProxySetting {
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory = $true, Position = 0)]
        [ValidateSet('Backup', 'Disable', 'Restore', IgnoreCase = $true)]
        [System.String]
        $Action,

        [Parameter(Position = 1)]
        [System.String]
        $BackupFile = "$PSScriptRoot\SPSWakeUP_proxy_backup.json"
    )

    $regPath = 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Internet Settings'

    switch ($Action) {
        'Backup' {
            if ($PSCmdlet.ShouldProcess($BackupFile, 'Back up proxy settings')) {
                try {
                    $proxySettings = Get-ItemProperty -Path $regPath |
                    Select-Object AutoConfigURL, ProxyEnable, ProxyServer, ProxyOverride

                    $proxySettings | ConvertTo-Json | Out-File -FilePath $BackupFile -Encoding UTF8
                    Write-Output "Proxy settings backed up to $BackupFile"
                }
                catch {
                    $catchMessage = @"
An error occurred while saving proxy settings
File : $BackupFile
Exception: $($_.Exception.Message)
"@

                    Write-Error -Message $catchMessage
                }
            }
        }
        'Disable' {
            if ($PSCmdlet.ShouldProcess('Proxy settings', 'Disable')) {
                try {
                    $itemProperties = @('AutoConfigURL', 'ProxyServer', 'ProxyOverride')
                    Set-ItemProperty -Path $regPath -Name ProxyEnable -Value 0
                    foreach ($itemProperty in $itemProperties) {
                        if (Get-ItemProperty -Path $regPath -Name $itemProperty -ErrorAction SilentlyContinue) {
                            Remove-ItemProperty -Path $regPath -Name $itemProperty -ErrorAction SilentlyContinue
                        }
                    }
                    Write-Output 'All proxy settings disabled.'
                }
                catch {
                    $catchMessage = @"
An error occurred while disabling proxy settings
Exception: $($_.Exception.Message)
"@

                    Write-Error -Message $catchMessage
                }
            }
        }
        'Restore' {
            if (Test-Path $BackupFile) {
                if ($PSCmdlet.ShouldProcess($BackupFile, 'Restore proxy settings')) {
                    try {
                        $proxySettings = Get-Content $BackupFile | ConvertFrom-Json
                        Set-ItemProperty -Path $regPath -Name ProxyEnable -Value $proxySettings.ProxyEnable
                        if ($proxySettings.ProxyServer) {
                            Set-ItemProperty -Path $regPath -Name ProxyServer -Value $proxySettings.ProxyServer
                        }
                        else {
                            Remove-ItemProperty -Path $regPath -Name ProxyServer -ErrorAction SilentlyContinue
                        }
                        if ($proxySettings.AutoConfigURL) {
                            Set-ItemProperty -Path $regPath -Name AutoConfigURL -Value $proxySettings.AutoConfigURL
                        }
                        else {
                            Remove-ItemProperty -Path $regPath -Name AutoConfigURL -ErrorAction SilentlyContinue
                        }
                        if ($proxySettings.ProxyOverride) {
                            Set-ItemProperty -Path $regPath -Name ProxyOverride -Value $proxySettings.ProxyOverride
                        }
                        else {
                            Remove-ItemProperty -Path $regPath -Name ProxyOverride -ErrorAction SilentlyContinue
                        }
                        Write-Output "Proxy settings restored from $BackupFile"
                    }
                    catch {
                        $catchMessage = @"
An error occurred while restoring proxy settings
File : $BackupFile
Exception: $($_.Exception.Message)
"@

                        Write-Error -Message $catchMessage
                    }
                }
            }
            else {
                Write-Output "Backup file not found at $BackupFile"
            }
        }
    }
}
#endregion

#region initialize SharePoint Context
# Load the SharePointServer module for all actions that need SharePoint cmdlets.
# SharePoint Server Subscription Edition is the only supported version.
if ($Action -in @('Install', 'Uninstall', 'Default', 'AdminSitesOnly')) {
    try {
        if (-not (Get-Module -ListAvailable -Name SharePointServer)) {
            throw 'The SharePointServer module is not available on this server. SPSWakeUp requires SharePoint Server Subscription Edition.'
        }
        if (-not (Get-Module SharePointServer)) {
            Import-Module SharePointServer -Verbose:$false -WarningAction SilentlyContinue -DisableNameChecking
        }
    }
    catch {
        $catchMessage = @"
Failed to load SharePoint module on $($env:COMPUTERNAME)
Exception: $($_.Exception.Message)
"@

        Write-Error -Message $catchMessage
        Add-SPSWakeUpEvent -Message $catchMessage -Source 'Initialize Module' -EntryType 'Error'
    }
    try {
        $currentSPServer = Get-SPServer | Where-Object -FilterScript { $_.Address -eq $env:COMPUTERNAME }
        if ($null -ne $currentSPServer) {
            if ($currentSPServer.Role -eq 'Search') {
                Write-Warning -Message 'You run this script on server with Search MinRole'
                Add-SPSWakeUpEvent -Message 'Search MinRole is not supported in SPSWakeUp' -Source 'Server MinRole' -EntryType 'Warning'
                Exit
            }
        }
    }
    catch {
        Write-Error -Message @"
An error occurred while checking the SharePoint Server Role
Exception: $($_.Exception.Message)
"@

    }
}
#endregion

#region main
switch ($Action) {
    'Uninstall' {
        # Remove SPSWakeup script from scheduled Task
        Remove-SPSSheduledTask -TaskName 'SPSWakeUP'
    }
    'Install' {
        if ($null -eq $InstallAccount) {
            Write-Warning -Message ('SPSWakeUp: Install parameter is set. Please set also InstallAccount ' + `
                    "parameter. `nSee https://github.com/luigilink/SPSWakeUp/wiki for details.")
            Exit
        }
        else {
            # Initialize variables
            $scriptFullPath = Join-Path -Path $PSScriptRoot -ChildPath 'SPSWakeUP.ps1'
            # Add SPSWakeup script in a new scheduled Task
            Install-SPSWakeUP -Path $scriptFullPath -InstallAccount $InstallAccount
        }
    }
    'AdminSitesOnly' {
        # Phase 1: collect SharePoint URLs in current process (PS 5.1 compatible)
        $jsonPath      = Join-Path -Path $PSScriptRoot -ChildPath 'SPSWakeUp_urls.json'
        $warmupScript  = Join-Path -Path $PSScriptRoot -ChildPath 'SPSWakeUp-pwsh.ps1'

        Write-Output '--------------------------------------------------------------'
        Write-Output 'Phase 1: Collecting URLs in current PowerShell process ...'

        $adminUrls = Get-SPSAdminUrl
        $payload = [ordered]@{
            Action        = 'AdminSitesOnly'
            GeneratedAt   = (Get-Date -Format 'o')
            ThrottleLimit = (Get-SPSThrottleLimit)
            AdminUrls     = @($adminUrls | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
            SiteUrls      = @()
            WebAppUrls    = @()
            HostEntries   = @()
            AuthUrl       = ''
        }

        $payload | ConvertTo-Json -Depth 5 | Out-File -FilePath $jsonPath -Encoding UTF8 -Force

        # Phase 2: warm up via PS 7.x if available, otherwise fallback to PS 5.1.
        if (Test-Path -Path $jsonPath) {
            $pwshCmd = Get-Command pwsh -ErrorAction SilentlyContinue
            $pwshPath = if ($null -ne $pwshCmd) { $pwshCmd.Source } else { $null }
            if ([string]::IsNullOrEmpty($pwshPath)) { $pwshPath = 'C:\Program Files\PowerShell\7\pwsh.exe' }

            if ((Test-Path -Path $pwshPath) -and (Test-Path -Path $warmupScript)) {
                Write-Output '--------------------------------------------------------------'
                Write-Output 'Phase 2: Warming up with PowerShell 7.x ...'
                $warmupArgs = "-ExecutionPolicy Bypass -File `"$warmupScript`" -Action AdminSitesOnly -InputJsonPath `"$jsonPath`""
                Start-Process -FilePath $pwshPath -ArgumentList $warmupArgs -Wait -NoNewWindow
            }
            else {
                Write-Warning 'PowerShell 7.x not found. Falling back to Windows PowerShell 5.1 warm-up flow.'
                Set-SPSProxySetting -Action 'Backup' -BackupFile "$PSScriptRoot\SPSWakeUP_proxy_backup.json"
                Set-SPSProxySetting -Action 'Disable'
                Invoke-SPSAdminSite
                Set-SPSProxySetting -Action 'Restore' -BackupFile "$PSScriptRoot\SPSWakeUP_proxy_backup.json"
            }
        }
        else {
            Write-Warning "JSON file not found after collect phase: $jsonPath"
        }
    }
    Default {
        # Phase 1: collect SharePoint URLs in current process (PS 5.1 compatible)
        $jsonPath      = Join-Path -Path $PSScriptRoot -ChildPath 'SPSWakeUp_urls.json'
        $warmupScript  = Join-Path -Path $PSScriptRoot -ChildPath 'SPSWakeUp-pwsh.ps1'

        Write-Output '--------------------------------------------------------------'
        Write-Output 'Phase 1: Collecting URLs in current PowerShell process ...'

        $hostEntries = New-Object -TypeName System.Collections.Generic.List[string]
        $adminUrls = Get-SPSAdminUrl
        $webAppUrls = Get-SPSWebAppUrl
        $siteUrls = Get-SPSSitesUrl

        $authUrl = ''
        try {
            $firstWebApp = Get-SPWebApplication | Select-Object -First 1
            if ($null -ne $firstWebApp) {
                # Use the scheme that IIS actually listens on locally (SSL offloading aware)
                $authBaseUrl = Get-SPSPreferredUrl -WebApplication $firstWebApp -Zone 'Default' -OriginalUrl $firstWebApp.GetResponseUri('Default').AbsoluteUri
                $authUrl = "$($authBaseUrl)_windows/default.aspx?ReturnUrl=/_layouts/15/Authenticate.aspx?Source=%2f"
            }
        }
        catch {
            Write-Warning "Could not determine authentication URL: $($_.Exception.Message)"
        }

        $payload = [ordered]@{
            Action        = 'Default'
            GeneratedAt   = (Get-Date -Format 'o')
            ThrottleLimit = (Get-SPSThrottleLimit)
            AdminUrls     = @($adminUrls | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
            SiteUrls      = @($siteUrls | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
            WebAppUrls    = @($webAppUrls | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
            HostEntries   = @($hostEntries | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } | Select-Object -Unique)
            AuthUrl       = $authUrl
        }

        $payload | ConvertTo-Json -Depth 5 | Out-File -FilePath $jsonPath -Encoding UTF8 -Force

        # Phase 2: warm up via PS 7.x if available, otherwise fallback to PS 5.1.
        if (Test-Path -Path $jsonPath) {
            $pwshCmd = Get-Command pwsh -ErrorAction SilentlyContinue
            $pwshPath = if ($null -ne $pwshCmd) { $pwshCmd.Source } else { $null }
            if ([string]::IsNullOrEmpty($pwshPath)) { $pwshPath = 'C:\Program Files\PowerShell\7\pwsh.exe' }

            if ((Test-Path -Path $pwshPath) -and (Test-Path -Path $warmupScript)) {
                Write-Output '--------------------------------------------------------------'
                Write-Output 'Phase 2: Warming up with PowerShell 7.x ...'
                $warmupArgs = "-ExecutionPolicy Bypass -File `"$warmupScript`" -Action Default -InputJsonPath `"$jsonPath`""
                Start-Process -FilePath $pwshPath -ArgumentList $warmupArgs -Wait -NoNewWindow
            }
            else {
                Write-Warning 'PowerShell 7.x not found. Falling back to Windows PowerShell 5.1 warm-up flow.'
                Set-SPSProxySetting -Action 'Backup' -BackupFile "$PSScriptRoot\SPSWakeUP_proxy_backup.json"
                Set-SPSProxySetting -Action 'Disable'
                Invoke-SPSAdminSite
                Invoke-SPSAllSite
                Set-SPSProxySetting -Action 'Restore' -BackupFile "$PSScriptRoot\SPSWakeUP_proxy_backup.json"
            }
        }
        else {
            Write-Warning "JSON file not found after collect phase: $jsonPath"
        }
    }
}

# Stop Transcript parameter is equal to True
if ($Transcript) {
    Stop-Transcript
    # Clean the folder of log files
    Clear-SPSLog -path $PSScriptRoot
}
Exit
#endregion