DSCResources/cOfficeLicense/cOfficeLicense.psm1

DATA Const{
    ConvertFrom-StringData -stringdata @'
        ProductClass = SoftwareLicensingProduct
        ProductClassWin7 = OfficeSoftwareProtectionProduct
        OfficeAppId = 0ff1ce15-a989-479d-af46-f275c6370663
        OfficeAppId2010 = 59a52881-a989-479d-af46-f275c6370663
'@

}

# ////////////////////////////////////////////////////////////////////////////////////////
# ////////////////////////////////////////////////////////////////////////////////////////
Enum OfficeLicenseStatus
{
    Unlicensed       = 0
    Licensed         = 1
    OOBGrace         = 2
    OOTGrace         = 3
    NonGenuineGrace  = 4
    Notification     = 5
    ExtendedGrace    = 6
}

# ////////////////////////////////////////////////////////////////////////////////////////
# ////////////////////////////////////////////////////////////////////////////////////////
Enum OSVersion
{
    Win7    = 7     # Win7 or Server2008R2
    Win8    = 8     # Win8 or Server2012
    Win81   = 81    # Win8.1 or Server2012R2
    Win10   = 10    # Win10 or Server2016
    Unknwon = 0     # Unknwon or ~Vista
}

# ////////////////////////////////////////////////////////////////////////////////////////
# ////////////////////////////////////////////////////////////////////////////////////////
Enum OfficeVersion
{
    Office2010 = 14
    Office2013 = 15
    Office2016 = 16
}

# ////////////////////////////////////////////////////////////////////////////////////////
# ////////////////////////////////////////////////////////////////////////////////////////
function Get-TargetResource {
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [ValidateSet("Present", "Absent")]
        [string]
        $Ensure = 'Present',

        [parameter(Mandatory = $true)]
        [string]
        $ProductKey,

        [parameter()]
        [bool]
        $Activate = $true,

        [parameter()]
        [bool]
        $Force = $false,

        [parameter(Mandatory = $true)]
        #[ValidateSet("Office2010","Office2013","Office2016")]
        [ValidateSet("Office2013", "Office2016")]
        [string]
        $OfficeVersion
    )
    #$ErrorActionPreference = 'Stop'
    $GetRes = @{
        Ensure            = $Ensure
        ProductKey        = ''
        Activate          = $false
        OfficeVersion     = $OfficeVersion
        PartialProductKey = ''
        LicenseStatus     = ''
        Product           = ''
    }

    # Check specific version of Office is installed or not
    if (-not (Get-OSPPVBS $OfficeVersion)) {
        # Write-Verbose ('{0} is not installed.' -f $OfficeVersion)
        $GetRes.Ensure = 'Absent'
        return $GetRes
    }

    # Check the Product key was installed or not
    $Product = Get-PrimaryOfficeSKU $OfficeVersion -ea SilentlyContinue
    if (-not $Product) {
        #Write-Verbose ('Product key is not installed on this machine.')
        $GetRes.Ensure = 'Absent'
    }
    else {
        $GetRes.Ensure = 'Present'
        $GetRes.Product = $Product.Name

        #Check activation status
        $GetRes.LicenseStatus = [string]([OfficeLicenseStatus]$Product.LicenseStatus)
        if ($Product.LicenseStatus -ne [OfficeLicenseStatus]::Licensed) {
            #Write-Verbose ('Office is NOT Licensed. (License status: "{0}")' -f [OfficeLicenseStatus]$Product.LicenseStatus)
            $GetRes.Activate = $false
        }
        else {
            #Write-Verbose 'Office is Licensed.'
            $GetRes.Activate = $true
        }

        #Get partial product key (Last 5 chars)
        $GetRes.PartialProductKey = $Product.PartialProductKey
    }

    $GetRes
} # end of Get-TargetResource

# ////////////////////////////////////////////////////////////////////////////////////////
# ////////////////////////////////////////////////////////////////////////////////////////
function Set-TargetResource {
    [CmdletBinding()]
    param
    (
        [ValidateSet("Present", "Absent")]
        [string]
        $Ensure = 'Present',

        [parameter(Mandatory = $true)]
        [string]
        $ProductKey,

        [parameter()]
        [bool]
        $Activate = $true,

        [parameter()]
        [bool]
        $Force = $false,

        [parameter(Mandatory = $true)]
        #[ValidateSet("Office2010","Office2013","Office2016")]
        [ValidateSet("Office2013", "Office2016")]
        [string]
        $OfficeVersion
    )
    # $ErrorActionPreference = 'Stop'

    # Test the product key format (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)
    if ($ProductKey) {
        if ($ProductKey -notmatch '^[0-9A-Z]{5}(-[0-9A-Z]{5}){4}$') {
            Write-Error ('The product key is invalid')
            return
        }
    }

    $Ospp = Get-OSPPVBS $OfficeVersion
    if (-not $Ospp) {
        Write-Error ('OSPP.VBS not found')
    }
    $Ospp = ('"{0}"' -f $Ospp)

    $Cscript = 'C:\Windows\System32\cscript.exe'
    if (-not (Test-Path $Cscript)) {
        Write-Error ('"{0}" not found' -f $Cscript)
    }

    $cState = (Get-TargetResource @PSBoundParameters)

    if ($Ensure -eq 'Absent') {
        # Remove Product Key
        if ($cState.PartialProductKey.length -ne 5) {
            Write-Error 'Error happend when removing the product key (PartialProductKey not found)'
        }
        else {
            $Exec = (Start-Command -FilePath $Cscript -ArgumentList ($Ospp, ('/unpkey:{0}' -f $cState.PartialProductKey)))
            $Err = Assert-Error $Exec.StdOut
            if ($Err.ErrorCode) {Write-Error ('Error: {0} ({1})' -f $Err.ErrorMsg, $Err.ErrorCode)}
            else {Write-Verbose ('Remove the product key succeeded')}
        }
    }
    else {
        $isKeyChenged = $false

        $local:tmpPPKey = $ProductKey.Substring($ProductKey.Length - 5, 5)  # Last 5 chars
        if (($cState.Ensure -eq 'Absent') -or ($cState.PartialProductKey -ne $tmpPPKey) -or $Force) {
            # Install Product Key
            $Exec = (Start-Command -FilePath $Cscript -ArgumentList ($Ospp, ('/inpkey:{0}' -f $ProductKey)))
            $Err = Assert-Error $Exec.StdOut
            if ($Err.ErrorCode) {Write-Error ('Error: {0} ({1})' -f $Err.ErrorMsg, $Err.ErrorCode)}
            else {Write-Verbose ('Install the product key succeeded')}
            $isKeyChenged = $true
        }

        if ($Activate) {
            if ($isKeyChenged -or (!$cState.Activate)) {
                #Activation
                $Exec = (Start-Command -FilePath $Cscript -ArgumentList ($Ospp, '/act'))
                $Err = Assert-Error $Exec.StdOut
                if ($Err.ErrorCode) {Write-Error ('Error: {0} ({1})' -f $Err.ErrorMsg, $Err.ErrorCode)}
                else {Write-Verbose ('Activation succeeded')}
            }
        }
    }
} # end of Set-TargetResource

# ////////////////////////////////////////////////////////////////////////////////////////
# ////////////////////////////////////////////////////////////////////////////////////////
function Test-TargetResource {
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [ValidateSet("Present", "Absent")]
        [string]
        $Ensure = 'Present',

        [parameter(Mandatory = $true)]
        [string]
        $ProductKey,

        [parameter()]
        [bool]
        $Activate = $true,

        [parameter()]
        [bool]
        $Force = $false,

        [parameter(Mandatory = $true)]
        #[ValidateSet("Office2010","Office2013","Office2016")]
        [ValidateSet("Office2013", "Office2016")]
        [string]
        $OfficeVersion
    )

    # Test the product key format (XXXXX-XXXXX-XXXXX-XXXXX-XXXXX)
    if ($ProductKey) {
        if ($ProductKey -notmatch '^[0-9A-Z]{5}(-[0-9A-Z]{5}){4}$') {
            Write-Error ('The product key is invalid')
            return $true
        }
    }

    $cState = (Get-TargetResource @PSBoundParameters)

    if ($Ensure -eq 'Absent') {
        if ($cState.Ensure -eq 'Absent') {
            Write-Verbose ('Product key not installed. It is your desired state.')
            return $true
        }
        else {
            Write-Verbose ('Product key is installed. It is NOT your desired state.')
            return $false
        }
    }
    elseif ($Ensure -eq 'Present') {
        if ($Force) {
            Write-Verbose ("Specified 'Force' switch. Skip test and return False.")
            return $false
        }

        if ($cState.Ensure -eq 'Absent') {
            Write-Verbose ('Product key not installed. It is NOT your desired state.')
            return $false
        }

        $local:tmpPPKey = $ProductKey.Substring($ProductKey.Length - 5, 5)  # Last 5 chars
        if ($cState.PartialProductKey -ne $tmpPPKey) {
            Write-Verbose ('Partial product key is not matched. (current:"{0}" / desired: "{1}")' -f $cState.PartialProductKey, $tmpPPKey)
            return $false
        }

        if ($Activate) {
            if (!$cState.Activate) {
                Write-Verbose ('Office is NOT Licensed. (License status: "{0}")' -f [OfficeLicenseStatus]$cState.LicenseStatus)
                return $false
            }
            else {
                Write-Verbose 'Office is Licensed.'
            }
        }
    }

    return $true
} # end of Test-TargetResource

# ////////////////////////////////////////////////////////////////////////////////////////
# ////////////////////////////////////////////////////////////////////////////////////////
function Get-PrimaryOfficeSKU {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, Position = 0)]
        [OfficeVersion]$OfficeVersion
    )

    if ($OfficeVersion -eq [OfficeVersion]::Office2010) {
        $ApplicationId = $Const.OfficeAppId2010
    }
    else {
        $ApplicationId = $Const.OfficeAppId
    }

    if ((Get-OSVersion) -eq [OSVersion]::Win7) {
        $ProductClass = $Const.ProductClassWin7
    }
    else {
        $ProductClass = $Const.ProductClass
    }

    # This logic is ported from OSPP.VBS
    $QueryStr = ('SELECT * FROM {0} WHERE PartialProductKey IS NOT NULL' -f $ProductClass)
    $object = Get-WmiObject -Query $QueryStr -ErrorAction SilentlyContinue |
        where {($_.ApplicationId -eq $ApplicationId) -and (!$_.LicenseIsAddon)}

    if (@($object).Count -ne 1) {
        return $null
    }
    else {
        return $object
    }
}

# ////////////////////////////////////////////////////////////////////////////////////////
# ////////////////////////////////////////////////////////////////////////////////////////
function Get-OSVersion {
    [CmdletBinding()]
    Param()
    # https://msdn.microsoft.com/ja-jp/library/windows/desktop/ms724832(v=vs.85).aspx
    $ErrorActionPreference = 'SilentlyContinue'
    $OperatingSystem = Get-WmiObject Win32_OperatingSystem -ErrorAction SilentlyContinue
    ForEach ($OS in $OperatingSystem) {
        $Ver = $OS.Version -split '\.'
        switch (('{0}.{1}' -f $Ver[0], $Ver[1])) {
            '10.0' { return [OSVersion]::Win10 }
            '6.3' { return [OSVersion]::Win81 }
            '6.2' { return [OSVersion]::Win8  }
            '6.1' { return [OSVersion]::Win7  }
        }
    }
    return [OSVersion]::Unknwon
}

# ////////////////////////////////////////////////////////////////////////////////////////
# ////////////////////////////////////////////////////////////////////////////////////////
function Get-OSPPVBS {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, Position = 0)]
        [OfficeVersion]$OfficeVersion
    )
    $ErrorActionPreference = 'SilentlyContinue'
    $ProgramFiles = $env:ProgramFiles
    $ProgramFilesX86 = ${env:ProgramFiles(x86)}

    $Path = Join-Path $ProgramFilesX86 ('Microsoft Office\Office{0}\OSPP.VBS' -f [int]$OfficeVersion) -ErrorAction SilentlyContinue
    if (Test-Path $Path -PathType Leaf) {
        return $Path
    }

    $Path = Join-Path $ProgramFiles ('Microsoft Office\Office{0}\OSPP.VBS' -f [int]$OfficeVersion) -ErrorAction SilentlyContinue
    if (Test-Path $Path -PathType Leaf) {
        return $Path
    }
}

# ////////////////////////////////////////////////////////////////////////////////////////
# ////////////////////////////////////////////////////////////////////////////////////////
function Assert-Error {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory, Position = 0)]
        [string]$OsppOutput
    )

    if ($OsppOutput -match 'ERROR CODE: (0x.+)') {
        $ErrorCode = $Matches[1]
        if ($OsppOutput -match 'ERROR DESCRIPTION: (.+)') {
            $ErrorMsg = $Matches[1]
        }
    }

    return [pscustomobject]@{
        ErrorCode = $ErrorCode
        ErrorMsg  = $ErrorMsg
    }
}

# ////////////////////////////////////////////////////////////////////////////////////////
# ////////////////////////////////////////////////////////////////////////////////////////
function Start-Command {
    [CmdletBinding()]
    Param(
        [Parameter(Mandatory = $true, Position = 0)]
        [string] $FilePath,
        [Parameter(Mandatory = $false, Position = 1)]
        [string[]]$ArgumentList,
        [int]$Timeout = [int]::MaxValue
    )
    $ProcessInfo = New-Object System.Diagnostics.ProcessStartInfo
    $ProcessInfo.FileName = $FilePath
    $ProcessInfo.UseShellExecute = $false
    $ProcessInfo.Arguments = [string]$ArgumentList
    $ProcessInfo.RedirectStandardError = $true
    $ProcessInfo.RedirectStandardOutput = $true
    $Process = New-Object System.Diagnostics.Process
    $Process.StartInfo = $ProcessInfo
    $Process.Start() | Out-Null
    if (!$Process.WaitForExit($Timeout)) {
        $Process.Kill()
        Write-Error ('Process timeout. Terminated. (Timeout:{0}s, Process:{1})' -f ($Timeout * 0.001), $FilePath)
    }
    $stdout = $Process.StandardOutput.ReadToEnd()
    $stderr = $Process.StandardError.ReadToEnd()
    [pscustomobject]@{
        Process  = $Process
        StdOut   = $stdout
        StdErr   = $stderr
        ExitCode = $Process.ExitCode
    }
}

Export-ModuleMember -Function *-TargetResource