DSCResources/MSFT_SPProductUpdate/MSFT_SPProductUpdate.psm1

function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $SetupFile,

        [Parameter()]
        [System.Boolean]
        $ShutdownServices,

        [Parameter()]
        [ValidateSet("mon", "tue", "wed", "thu", "fri", "sat", "sun")]
        [System.String[]]
        $BinaryInstallDays,

        [Parameter()]
        [System.String]
        $BinaryInstallTime,

        [Parameter()]
        [ValidateSet("Present", "Absent")]
        [System.String]
        $Ensure = "Present"
    )

    if ($Ensure -eq "Absent")
    {
        $message = "SharePoint does not support uninstalling updates."
        Add-SPDscEvent -Message $message `
            -EntryType 'Error' `
            -EventID 100 `
            -Source $MyInvocation.MyCommand.Source
        throw $message
    }

    Write-Verbose -Message "Getting install status of SP binaries"

    $language = ""

    Write-Verbose -Message "Check if the setup file exists"
    if (-not(Test-Path -Path $SetupFile))
    {
        Write-Verbose -Message "ERROR: Setup files could not be found: $SetupFile"
        return @{
            SetupFile         = $SetupFile
            ShutdownServices  = $ShutdownServices
            BinaryInstallDays = $BinaryInstallDays
            BinaryInstallTime = $BinaryInstallTime
            Ensure            = "Absent"
        }
    }

    Write-Verbose -Message "Checking file status of $SetupFile"
    $checkBlockedFile = $true
    if (Split-Path -Path $SetupFile -IsAbsolute)
    {
        $driveLetter = (Split-Path -Path $SetupFile -Qualifier).TrimEnd(":")
        Write-Verbose -Message "SetupFile refers to drive $driveLetter"

        $volume = Get-Volume -DriveLetter $driveLetter -ErrorAction SilentlyContinue
        if ($null -ne $volume)
        {
            if ($volume.DriveType -ne "CD-ROM")
            {
                Write-Verbose -Message "Volume is a fixed drive: Perform Blocked File test"
            }
            else
            {
                Write-Verbose -Message "Volume is a CD-ROM drive: Skipping Blocked File test"
                $checkBlockedFile = $false
            }
        }
        else
        {
            Write-Verbose -Message "Volume not found. Unable to determine the type. Continuing."
        }
    }

    if ($checkBlockedFile -eq $true)
    {
        Write-Verbose -Message "Checking status now"
        try
        {
            $zone = Get-Item -Path $SetupFile -Stream "Zone.Identifier" -EA SilentlyContinue
        }
        catch
        {
            Write-Verbose -Message 'Encountered error while reading file stream. Ignoring file stream.'
        }

        if ($null -ne $zone)
        {
            $message = ("Setup file is blocked! Please use 'Unblock-File -Path $SetupFile' " + `
                    "to unblock the file before continuing.")
            Add-SPDscEvent -Message $message `
                -EntryType 'Error' `
                -EventID 100 `
                -Source $MyInvocation.MyCommand.Source
            throw $message
        }
        Write-Verbose -Message "File not blocked, continuing."
    }

    $nullVersion = New-Object -TypeName System.Version

    Write-Verbose -Message "Get file information from setup file"
    $setupFileInfo = Get-ItemProperty -Path $SetupFile
    $fileVersion = $setupFileInfo.VersionInfo.FileVersion
    Write-Verbose -Message "Update has version $fileVersion"

    $fileVersionInfo = New-Object -TypeName System.Version -ArgumentList $fileVersion
    if ($fileVersionInfo.Major -eq 15)
    {
        $sharePointVersion = '2013'
    }
    else
    {
        if ($fileVersionInfo.Build.ToString().Length -eq 4)
        {
            $sharePointVersion = '2016'
        }
        else
        {
            if ($fileVersionInfo.Build -lt 13000)
            {
                $sharePointVersion = '2019'
            }
            else
            {
                $sharePointVersion = 'SE'
            }
        }
    }

    if ($setupFileInfo.VersionInfo.FileDescription -match "Service Pack.*Language Pack")
    {
        Write-Verbose -Message "Update is a Language Pack Service Pack."
        # Retrieve language from file and check version for that language pack.

        # Extract language from filename
        if ($setupFileInfo.Name -match "\w*-(\w{2}-\w{2}).exe")
        {
            $language = $matches[1]
        }
        else
        {
            $message = "Update does not contain the language code in the correct format."
            Add-SPDscEvent -Message $message `
                -EntryType 'Error' `
                -EventID 100 `
                -Source $MyInvocation.MyCommand.Source
            throw $message
        }

        try
        {
            $cultureInfo = New-Object -TypeName System.Globalization.CultureInfo -ArgumentList $language
        }
        catch
        {
            $message = "Error while converting language information: $language"
            Add-SPDscEvent -Message $message `
                -EntryType 'Error' `
                -EventID 100 `
                -Source $MyInvocation.MyCommand.Source
            throw $message
        }

        # try/catch is required for some versions of Windows, other version use the LCID value of 4096
        if ($cultureInfo.LCID -eq 4096)
        {
            $message = "Error while converting language information: $language"
            Add-SPDscEvent -Message $message `
                -EntryType 'Error' `
                -EventID 100 `
                -Source $MyInvocation.MyCommand.Source
            throw $message
        }

        # Extract English name of the language code
        if ($cultureInfo.EnglishName -match "(\w*,*\s*\w*) \([a-zA-Z_0-9 ]*\)")
        {
            $languageEnglish = $matches[1]
            if ($languageEnglish.contains(","))
            {
                $languages = $languageEnglish.Split(",")
                $languageEnglish = $languages[0]
            }
        }

        # Extract Native name of the language code
        if ($cultureInfo.NativeName -match "(\w*,*\s*\w*) \([a-zA-Z_0-9 ]*\)")
        {
            $languageNative = $matches[1]
            if ($languageNative.contains(","))
            {
                $languages = $languageNative.Split(",")
                $languageNative = $languages[0]
            }
        }

        # Build language string used in Language Pack names
        $languageString = "$languageEnglish/$languageNative"
        Write-Verbose -Message "Update is for the $($languageString) language"

        $versionInfo = Get-SPDscLocalVersionInfo -ProductVersion $sharePointVersion -Lcid $($cultureInfo.LCID)

        if ($versionInfo -eq $nullVersion)
        {
            $message = "Error: Product for language $language is not found."
            Add-SPDscEvent -Message $message `
                -EntryType 'Error' `
                -EventID 100 `
                -Source $MyInvocation.MyCommand.Source
            throw $message
        }
        else
        {
            Write-Verbose -Message "Product found; Version: $versionInfo"
        }
    }
    elseif ($setupFileInfo.VersionInfo.FileDescription -match "Service Pack")
    {
        Write-Verbose -Message "Update is a Service Pack for SharePoint."
        # Check SharePoint version information.
        $versionInfo = Get-SPDscLocalVersionInfo -ProductVersion $sharePointVersion
    }
    else
    {
        Write-Verbose -Message "Update is a Cumulative Update."
        # For SP 2016 + 2019 Patches
        $setupFileInformation = New-Object -TypeName System.IO.FileInfo -ArgumentList  $SetupFile
        if ($setupFileInformation.Name.StartsWith("wssloc"))
        {
            Write-Verbose -Message "Cumulative Update is multilingual"
            $versionInfo = Get-SPDscLocalVersionInfo -ProductVersion $sharePointVersion -IsWssPackage
        }
        else
        {
            Write-Verbose -Message "Cumulative Update is generic"
            $versionInfo = Get-SPDscLocalVersionInfo -ProductVersion $sharePointVersion
        }
    }

    Write-Verbose -Message "The lowest version of any SharePoint component is $($versionInfo)"
    if ($versionInfo -lt $fileVersionInfo)
    {
        # Version of SharePoint is lower than the patch version. Patch is not installed.
        return @{
            SetupFile         = $SetupFile
            ShutdownServices  = $ShutdownServices
            BinaryInstallDays = $BinaryInstallDays
            BinaryInstallTime = $BinaryInstallTime
            Ensure            = "Absent"
        }
    }
    else
    {
        # Version of SharePoint is equal or greater than the patch version. Patch is installed.
        return @{
            SetupFile         = $SetupFile
            ShutdownServices  = $ShutdownServices
            BinaryInstallDays = $BinaryInstallDays
            BinaryInstallTime = $BinaryInstallTime
            Ensure            = "Present"
        }
    }
}

function Set-TargetResource
{
    # Supressing the global variable use to allow passing DSC the reboot message
    [CmdletBinding()]
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidGlobalVars", "")]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $SetupFile,

        [Parameter()]
        [System.Boolean]
        $ShutdownServices,

        [Parameter()]
        [ValidateSet("mon", "tue", "wed", "thu", "fri", "sat", "sun")]
        [System.String[]]
        $BinaryInstallDays,

        [Parameter()]
        [System.String]
        $BinaryInstallTime,

        [Parameter()]
        [ValidateSet("Present", "Absent")]
        [System.String]
        $Ensure = "Present"
    )

    Write-Verbose -Message "Setting install status of SP Update binaries"

    if ($Ensure -eq "Absent")
    {
        $message = "SharePoint does not support uninstalling updates."
        Add-SPDscEvent -Message $message `
            -EntryType 'Error' `
            -EventID 100 `
            -Source $MyInvocation.MyCommand.Source
        throw $message
    }

    Write-Verbose -Message "Check if the setup file exists"
    if (-not(Test-Path -Path $SetupFile))
    {
        $message = "Setup file cannot be found: {$SetupFile}"
        Add-SPDscEvent -Message $message `
            -EntryType 'Error' `
            -EventID 100 `
            -Source $MyInvocation.MyCommand.Source
        throw $message
    }

    Write-Verbose -Message "Checking file status of $SetupFile"
    $checkBlockedFile = $true
    if (Split-Path -Path $SetupFile -IsAbsolute)
    {
        $driveLetter = (Split-Path -Path $SetupFile -Qualifier).TrimEnd(":")
        Write-Verbose -Message "SetupFile refers to drive $driveLetter"

        $volume = Get-Volume -DriveLetter $driveLetter -ErrorAction SilentlyContinue
        if ($null -ne $volume)
        {
            if ($volume.DriveType -ne "CD-ROM")
            {
                Write-Verbose -Message "Volume is a fixed drive: Perform Blocked File test"
            }
            else
            {
                Write-Verbose -Message "Volume is a CD-ROM drive: Skipping Blocked File test"
                $checkBlockedFile = $false
            }
        }
        else
        {
            Write-Verbose -Message "Volume not found. Unable to determine the type. Continuing."
        }
    }

    if ($checkBlockedFile -eq $true)
    {
        Write-Verbose -Message "Checking status now"
        try
        {
            $zone = Get-Item -Path $SetupFile -Stream "Zone.Identifier" -EA SilentlyContinue
        }
        catch
        {
            Write-Verbose -Message 'Encountered error while reading file stream. Ignoring file stream.'
        }

        if ($null -ne $zone)
        {
            $message = ("Setup file is blocked! Please use 'Unblock-File -Path $SetupFile' " + `
                    "to unblock the file before continuing.")
            Add-SPDscEvent -Message $message `
                -EntryType 'Error' `
                -EventID 100 `
                -Source $MyInvocation.MyCommand.Source
            throw $message
        }
        Write-Verbose -Message "File not blocked, continuing."
    }

    $now = Get-Date
    Write-Verbose -Message "Check if BinaryInstallDays is specified"
    if ($BinaryInstallDays)
    {
        Write-Verbose -Message "BinaryInstallDays parameter exists, check if current day is specified"
        $currentDayOfWeek = $now.DayOfWeek.ToString().ToLower().Substring(0, 3)

        if ($BinaryInstallDays -contains $currentDayOfWeek)
        {
            Write-Verbose -Message ("Current day is present in the parameter BinaryInstallDays. " + `
                    "Update can be run today.")
        }
        else
        {
            Write-Verbose -Message ("Current day is not present in the parameter BinaryInstallDays, " + `
                    "skipping the update")
            return
        }
    }
    else
    {
        Write-Verbose -Message "No BinaryInstallDays specified, Update can be ran on any day."
    }

    Write-Verbose -Message "Check if BinaryInstallTime is specified"
    if ($BinaryInstallTime)
    {
        Write-Verbose -Message ("BinaryInstallTime parameter exists, check if current time is inside " + `
                "of time window")
        $upgradeTimes = $BinaryInstallTime.Split(" ")
        $starttime = 0
        $endtime = 0

        if ($upgradeTimes.Count -ne 3)
        {
            $message = "Time window incorrectly formatted."
            Add-SPDscEvent -Message $message `
                -EntryType 'Error' `
                -EventID 100 `
                -Source $MyInvocation.MyCommand.Source
            throw $message
        }
        else
        {
            if ([datetime]::TryParse($upgradeTimes[0], [ref]$starttime) -ne $true)
            {
                $message = "Error converting start time"
                Add-SPDscEvent -Message $message `
                    -EntryType 'Error' `
                    -EventID 100 `
                    -Source $MyInvocation.MyCommand.Source
                throw $message
            }

            if ([datetime]::TryParse($upgradeTimes[2], [ref]$endtime) -ne $true)
            {
                $message = "Error converting end time"
                Add-SPDscEvent -Message $message `
                    -EntryType 'Error' `
                    -EventID 100 `
                    -Source $MyInvocation.MyCommand.Source
                throw $message
            }

            if ($starttime -gt $endtime)
            {
                $message = "Error: Start time cannot be larger than end time"
                Add-SPDscEvent -Message $message `
                    -EntryType 'Error' `
                    -EventID 100 `
                    -Source $MyInvocation.MyCommand.Source
                throw $message
            }
        }

        if (($starttime -lt $now) -and ($endtime -gt $now))
        {
            Write-Verbose -Message ("Current time is inside of the window specified in " + `
                    "BinaryInstallTime. Starting update")
        }
        else
        {
            Write-Verbose -Message ("Current time is outside of the window specified in " + `
                    "BinaryInstallTime, skipping the update")
            return
        }
    }
    else
    {
        Write-Verbose -Message ("No BinaryInstallTime specified, Update can be ran at " + `
                "any time. Starting update.")
    }

    $installedVersion = Get-SPDscInstalledProductVersion

    Write-Verbose -Message "Try to load local Farm"

    $farmIsAvailable = Invoke-SPDscCommand -ScriptBlock {
        try
        {
            $null = Get-SPFarm
            return $true
        }
        catch
        {
            return $false
        }
    }

    if ($ShutdownServices -and $farmIsAvailable)
    {
        Write-Verbose -Message "Stopping services to speed up installation process"

        $searchPaused = $false
        $osearchStopped = $false
        $hostControllerStopped = $false

        if ($installedVersion.FileMajorPart -eq 15)
        {
            $searchServiceName = "OSearch15"
        }
        else
        {
            $searchServiceName = "OSearch16"
        }

        $osearchSvc = Get-Service -Name $searchServiceName
        $hostControllerSvc = Get-Service -Name "SPSearchHostController"

        Invoke-SPDscCommand -ScriptBlock {
            $searchSAs = Get-SPEnterpriseSearchServiceApplication
            foreach ($searchSA in $searchSAs)
            {
                Write-Verbose -Message "Pausing all search service applications"
                if ($searchSA.isPaused() -eq 0)
                {
                    $searchSA.Pause()
                }
            }
        }
        $searchPaused = $true

        if ($osearchSvc.Status -eq "Running")
        {
            $osearchStopped = $true
            Set-Service -Name $searchServiceName -StartupType Disabled
            $osearchSvc.Stop()
        }

        if ($hostControllerSvc.Status -eq "Running")
        {
            $hostControllerStopped = $true
            Set-Service "SPSearchHostController" -StartupType Disabled
            $hostControllerSvc.Stop()
        }

        $hostControllerSvc.WaitForStatus('Stopped', '00:01:00')

        Write-Verbose -Message "Search Services are stopped"

        Write-Verbose -Message "Stopping other services"

        if ($installedVersion.FileMajorPart -eq 15 -or $installedVersion.ProductBuildPart.ToString().Length -eq 4)
        {
            Write-Verbose -Message "SharePoint 2013 or 2016 used, reconfiguring IISAdmin service to Disabled startup."
            Set-Service -Name "IISADMIN" -StartupType Disabled
        }
        Set-Service -Name "SPTimerV4" -StartupType Disabled

        $null = Start-Process -FilePath "iisreset.exe" `
            -ArgumentList "-stop -noforce" `
            -Wait `
            -PassThru

        $timerSvc = Get-Service -Name "SPTimerV4"
        if ($timerSvc.Status -eq "Running")
        {
            $timerSvc.Stop()
        }
    }

    Write-Verbose -Message "Beginning installation of the SharePoint update"

    Invoke-SPDscCommand -Arguments @($SetupFile, $MyInvocation.MyCommand.Source) `
        -ScriptBlock {
        $setupFile = $args[0]
        $eventSource = $args[1]

        Write-Verbose -Message "Checking if SetupFile is an UNC path"
        $uncInstall = $false
        if ($setupFile.StartsWith("\\"))
        {
            Write-Verbose -Message "Specified BinaryDir is an UNC path. Adding path to Local Intranet Zone"

            $uncInstall = $true

            if ($setupFile -match "\\\\(.*?)\\.*")
            {
                $serverName = $Matches[1]
            }
            else
            {
                $message = "Cannot extract servername from UNC path. Check if it is in the correct format."
                Add-SPDscEvent -Message $message `
                    -EntryType 'Error' `
                    -EventID 100 `
                    -Source $eventSource
                throw $message
            }

            Set-SPDscZoneMap -Server $serverName
        }

        $setup = Start-Process -FilePath $setupFile `
            -ArgumentList "/quiet /passive" `
            -Wait `
            -PassThru

        if ($uncInstall -eq $true)
        {
            Write-Verbose -Message "Removing added path from the Local Intranet Zone"
            Remove-SPDscZoneMap -ServerName $serverName
        }

        # Error codes: https://aka.ms/installerrorcodes
        switch ($setup.ExitCode)
        {
            0
            {
                Write-Verbose -Message "SharePoint update binary installation complete."
            }
            17022
            {
                Write-Verbose -Message ("SharePoint update binary installation complete, " + `
                        "however a reboot is required.")
                $global:DSCMachineStatus = 1
            }
            17025
            {
                Write-Verbose -Message ("The SharePoint update was already installed on your system." + `
                        "Please report an issue about this behavior at https://github.com/dsccommunity/SharePointDsc")
            }
            Default
            {
                $message = ("SharePoint update install failed, exit code was $($setup.ExitCode). " + `
                        "Error codes can be found at https://aka.ms/installerrorcodes")
                Add-SPDscEvent -Message $message `
                    -EntryType 'Error' `
                    -EventID 100 `
                    -Source $eventSource
                throw $message
            }
        }
    }

    if ($ShutdownServices -and $farmIsAvailable)
    {
        Write-Verbose -Message "Restart stopped services"
        Set-Service -Name "SPTimerV4" -StartupType Automatic

        if ($installedVersion.FileMajorPart -eq 15 -or $installedVersion.ProductBuildPart.ToString().Length -eq 4)
        {
            Write-Verbose -Message "SharePoint 2013 or 2016 used, reconfiguring IISAdmin service to Automatic startup."
            Set-Service -Name "IISADMIN" -StartupType Automatic
        }

        $timerSvc = Get-Service -Name "SPTimerV4"
        $timerSvc.Start()

        Start-Process -FilePath "iisreset.exe" `
            -ArgumentList "-start" `
            -Wait `
            -PassThru

        $osearchSvc = Get-Service -Name $searchServiceName
        $hostControllerSvc = Get-Service -Name "SPSearchHostController"

        # Ensuring Search Services were stopped by script before Starting"
        if ($osearchStopped -eq $true)
        {
            Set-Service -Name $searchServiceName -StartupType Manual
            $osearchSvc.Start()
        }

        if ($hostControllerStopped -eq $true)
        {
            Set-Service "SPSearchHostController" -StartupType Automatic
            $hostControllerSvc.Start()
        }

        if ($searchPaused -eq $true)
        {
            # Resuming Search Service Application if paused###
            Invoke-SPDscCommand -ScriptBlock {
                $unpatchedServers = Get-SPDscAllServersPatchStatus | Where-Object { $_.Status -ne "UpgradeRequired" -and $_.Status -ne "UpgradeAvailable" }

                if ($unpatchedServers.Count -eq 0)
                {
                    Write-Verbose -Message "All servers are on the same patch level. Resuming the all search service applications!"
                    $searchSAs = Get-SPEnterpriseSearchServiceApplication
                    foreach ($searchSA in $searchSAs)
                    {
                        if (($searchSA.IsPaused() -band 0x80) -ne 0)
                        {
                            $searchSA.Resume()
                        }
                    }
                }
                else
                {
                    Write-Verbose -Message "There are still some unpatched servers. Skipping resuming the search!"
                    Write-Verbose -Message "The following servers aren't on the correct patch level: $($unpatchedServers -join ", ")"
                }
            }
        }

        Write-Verbose -Message "Services restarted."
    }
}

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $SetupFile,

        [Parameter()]
        [System.Boolean]
        $ShutdownServices,

        [Parameter()]
        [ValidateSet("mon", "tue", "wed", "thu", "fri", "sat", "sun")]
        [System.String[]]
        $BinaryInstallDays,

        [Parameter()]
        [System.String]
        $BinaryInstallTime,

        [Parameter()]
        [ValidateSet("Present", "Absent")]
        [System.String]
        $Ensure = "Present"
    )

    Write-Verbose -Message "Testing install status of SP Update binaries"

    $PSBoundParameters.Ensure = $Ensure

    if ($Ensure -eq "Absent")
    {
        $message = "SharePoint does not support uninstalling updates."
        Add-SPDscEvent -Message $message `
            -EntryType 'Error' `
            -EventID 100 `
            -Source $MyInvocation.MyCommand.Source
        throw $message
    }

    $CurrentValues = Get-TargetResource @PSBoundParameters

    Write-Verbose -Message "Current Values: $(Convert-SPDscHashtableToString -Hashtable $CurrentValues)"
    Write-Verbose -Message "Target Values: $(Convert-SPDscHashtableToString -Hashtable $PSBoundParameters)"

    $result = Test-SPDscParameterState -CurrentValues $CurrentValues `
        -Source $($MyInvocation.MyCommand.Source) `
        -DesiredValues $PSBoundParameters `
        -ValuesToCheck @("Ensure")

    Write-Verbose -Message "Test-TargetResource returned $result"

    return $result
}

function Get-SPDscLocalVersionInfo
{
    [OutputType([System.Version])]
    param
    (
        # Parameter help description
        [Parameter(Mandatory = $true)]
        [ValidateSet('2013', '2016', '2019', 'SE')]
        [System.String]
        $ProductVersion,

        [Parameter()]
        [System.Int32]
        $Lcid,

        [Parameter()]
        [Switch]
        $IsWssPackage
    )

    if ($ProductVersion -eq 'SE')
    {
        $spVersion = 'Subscription Edition'
    }
    else
    {
        $spVersion = $ProductVersion
    }

    $productNameRegEx = "Microsoft SharePoint (Foundation|Server) $($spVersion) Core"

    if (0 -ne $Lcid)
    {
        $productNameRegEx = "Microsoft SharePoint (Foundation|Server) $($spVersion) $($Lcid) (Lang|Language) Pack"
    }

    if ($IsWssPackage)
    {
        $productNameRegEx = "Microsoft SharePoint (Foundation|Server) $($spVersion) \d{4} (Lang|Language) Pack"
    }
    Write-Verbose "Product Name RegEx: $($productNameRegEx)"

    $installerRegistryPath = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products"

    $patchRegistryPath = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Patches"

    $installerEntries = Get-ChildItem -Path $installerRegistryPath -ErrorAction SilentlyContinue

    $nullVersion = New-Object -TypeName System.Version
    $versionInfoValue = New-Object -TypeName System.Version

    $officeProductKeys = $installerEntries | Where-Object -FilterScript { $_.PsPath -like "*00000000F01FEC" }

    if ($null -eq $installerEntries -or $null -eq $officeProductKeys )
    {
        return $nullVersion
    }

    # $null - one command returns an empty value
    $null = $officeProductKeys | ForEach-Object -Process {
        $officeProductKey = $_

        $productInfo = Get-ItemProperty "Registry::$($officeProductKey)\InstallProperties" -ErrorAction SilentlyContinue

        if ($null -eq $productInfo)
        {
            break
        }

        $prodName = $productInfo.DisplayName

        if ($prodName -match $productNameRegEx)
        {
            Write-Verbose "Gathering Information for $($prodName)"
            $patchInformationFolder = Get-ItemProperty "Registry::$($officeProductKey)\Patches"
            # SharePoint 2013 with SP 1 has a minimum of two Items in this key
            if ($patchInformationFolder.AllPatches.GetType().Name -eq "String[]" -and $patchInformationFolder.AllPatches.Length -gt 0)
            {
                $patchGuid = $patchInformationFolder.AllPatches[$patchInformationFolder.AllPatches.Length - 1]
            }
            else
            {
                $patchGuid = $patchInformationFolder.AllPatches
            }

            if ($null -ne $patchGuid)
            {
                $detailedPatchInformation = Get-ItemProperty "$($patchRegistryPath)\$($patchGuid)" -ErrorAction SilentlyContinue
                $localPackage = $detailedPatchInformation.LocalPackage

                if ($null -ne $localPackage)
                {
                    $patchFileInformation = New-Object -TypeName System.IO.FileInfo -ArgumentList $localPackage
                    if ($patchFileInformation.Extension -eq ".msp")
                    {
                        try
                        {
                            $windowsInstaller = New-Object -ComObject WindowsInstaller.Installer
                            $installerDatabase = $windowsInstaller.GetType().InvokeMember("OpenDatabase", "InvokeMethod", $null, $windowsInstaller, ($localPackage , 32))
                            $databaseQuery = "SELECT Value FROM MsiPatchMetadata WHERE Property = 'BuildNumber'"
                            $databaseView = $installerDatabase.GetType().InvokeMember("OpenView", "InvokeMethod", $null, $installerDatabase, ($databaseQuery))
                            $databaseView.GetType().InvokeMember("Execute", "InvokeMethod", $null, $databaseView, $null)
                            $value = $databaseView.GetType().InvokeMember("Fetch", "InvokeMethod", $null, $databaseView, $null)
                            $versionInfo = [System.Version]$value.GetType().InvokeMember("StringData", "GetProperty", $null, $value, 1)

                            # https://github.com/dsccommunity/DscResources/issues/383

                            Clear-ComObject -ComObject $databaseView
                            Clear-ComObject -ComObject $value
                            Clear-ComObject -ComObject $installerDatabase
                            Clear-ComObject -ComObject $windowsInstaller
                        }
                        catch [Exception]
                        {
                            $message = "An error occured during the collection of data about installed products in Get-SPDscLocalVersionInfo."
                            Add-SPDscEvent -Message $message `
                                -EntryType 'Error' `
                                -EventID 100 `
                                -Source $MyInvocation.MyCommand.Source
                            throw $message
                        }
                    }
                }
                else
                {
                    $versionInfo = New-Object -TypeName System.Version -ArgumentList $productInfo.DisplayVersion
                }
            }

            # Collect Information about language packs
            if ($IsWssPackage `
                    -and (  $versionInfoValue -eq $nullVersion `
                        -or $versionInfoValue -gt $versionInfo) `
            )
            {
                $versionInfoValue = $versionInfo
            }
            else
            {
                $versionInfoValue = $versionInfo
            }
            Write-Verbose "Version Information for $($prodName): $($versionInfoValue)"

        }
    }

    if ($nullVersion -ne $versionInfoValue)
    {
        return $versionInfoValue
    }

    return $nullVersion
}

# Function required for Mocking the static .Net call
function Clear-ComObject
{
    param
    (
        [Parameter(Mandatory = $true)]
        [System.Object]
        $ComObject
    )

    $null = [System.Runtime.InteropServices.Marshal]::ReleaseComObject($ComObject)
}

Export-ModuleMember -Function *-TargetResource