NTS.Tools.Application.psm1

function Start-C2RSetup {
    <#
        .Description
        this function can be used to install Microsoft 365 Apps
 
        .Parameter ConfigXMLPath
        defines the path to xml config file, cannot be used with UseDefaultConfigXML or UseSilentDefaultConfigXML
 
        .Parameter WorkingDir
        defines to folder where installation files should be saved
 
        .Parameter CleanUpInstallFiles
        if specified, the folder used in the parameter workdir will be deleted afterwards
 
        .Parameter UseDefaultConfigXML
        if specified, it will create config with "Outlook", "Word", "Excel", "Teams" and install those apps
 
        .Parameter UseSilentDefaultConfigXML
        same as parameter UseDefaultConfigXML, but silent
 
        .Parameter OfficeEdition
        define the office edition which should be installed with the default config
 
        .Example
        # this will install Microsoft 365 Apps for Business and deletes the setup files afterwards
        Install-M365Apps -UseDefaultConfigXML -OfficeEdition O365BusinessRetail -CleanUpInstallFiles
 
        .NOTES
        https://github.com/mallockey/Install-Office365Suite/blob/master/Install-Office365Suite.ps1
    #>


    [CmdletBinding(DefaultParameterSetName = 'XMLFile')]
    [Alias("Install-M365Apps", "Install-OfficeLTSC")]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = "XMLFile")]
        [string]
        $ConfigXMLPath,

        [Parameter(Mandatory = $false)]
        [string]
        $WorkingDir = "$($env:ProgramData)\NTS\Office",

        [Parameter(Mandatory = $false)]
        [switch]
        $CleanUpInstallFiles,

        [Parameter(Mandatory = $false, ParameterSetName = "DefaultXML")]
        [switch]
        $UseDefaultConfigXML,

        [Parameter(Mandatory = $false, ParameterSetName = "SilentDefaultXML")]
        [switch]
        $UseSilentDefaultConfigXML,

        [Parameter(Mandatory = $false, ParameterSetName = "DefaultXML")]
        [Parameter(Mandatory = $false, ParameterSetName = "SilentDefaultXML")]
        [ValidateSet(
            "O365ProPlusRetail",
            "O365BusinessRetail",
            "ProPlus2021Volume",
            "ProPlus2019Volume"
        )]
        [string]
        $OfficeEdition
    )
    
    $ErrorActionPreference = "Stop"

    # check admin access
    $CurrentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
    if (!($CurrentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator))) {
        Write-Warning "$($env:COMPUTERNAME): Script is not running as Administrator"
        Write-Warning "$($env:COMPUTERNAME): Please rerun this script as Administrator"
        exit
    }

    if (!(Test-Path $WorkingDir)) {
        New-Item -Path $WorkingDir -ItemType Directory | Out-Null
    }

    if ($UseDefaultConfigXML) {
        Write-Output "$($env:COMPUTERNAME): creating xml config file for $($OfficeEdition)"
        New-C2RConfigXML -ConfigXMLPath $WorkingDir -ConfigXMLFileName "config.xml" -OfficeEdition $OfficeEdition -IncludeApps ("Outlook", "Word", "Excel", "Teams")
        $ConfigXMLPath = "$($WorkingDir)\config.xml"
    }
    elseif ($UseSilentDefaultConfigXML) {
        Write-Output "$($env:COMPUTERNAME): creating xml config file for $($OfficeEdition)"
        New-C2RConfigXML -ConfigXMLPath $WorkingDir -ConfigXMLFileName "config.xml" -OfficeEdition $OfficeEdition -IncludeApps ("Outlook", "Word", "Excel", "Teams") -DisplayLevel None
        $ConfigXMLPath = "$($WorkingDir)\config.xml"
    }

    # verify config xml
    Test-OfficeConfiguration -ConfigurationXMLFilePath $ConfigXMLPath

    # Get Setup.exe
    Get-C2RSetup -OutPath $WorkingDir -FileName "Setup.exe"

    # Run the setup
    [xml]$OfficeConfig = Get-Content -Path $ConfigXMLPath
    Write-Output "$($env:COMPUTERNAME): start installation of $($OfficeConfig.Configuration.Add.Product.ID)"
    $Process = Start-Process "$($WorkingDir)\Setup.exe" -ArgumentList "/configure `"$ConfigXMLPath`"" -Wait -PassThru -WindowStyle Hidden
    if ($Process.ExitCode -ne 0) {
        throw "there was an error installing office - $($PSItem.Exception.Message)"
    }

    #Check if suite was installed correctly

    $RegLocations = @(
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall",
        "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
    )
    $ItemsFound = ((Get-ChildItem -Path $RegLocations).Name -match "$($OfficeConfig.Configuration.Add.Product.ID)")[0].ToString().Replace("HKEY_LOCAL_MACHINE\","")
    if($null -ne $ItemsFound) {
        $OfficeVersionInstalled = (Get-ItemProperty -Path "HKLM:\$($ItemsFound)").Displayname
        Write-Output "$($env:COMPUTERNAME): $($OfficeVersionInstalled) was successfully installed"
    }
    else {
        Write-Warning "$($env:COMPUTERNAME): $($OfficeConfig.Configuration.Add.Product.ID) was not detected after the install ran"
    }

    if ($CleanUpInstallFiles) {
        Start-FolderCleanUp -FolderToRemove $WorkingDir
    }
}

function New-C2RConfigXML {
    <#
        .Description
        this function can be used generate a microsoft 365 apps config file
 
        .Parameter ConfigXMLPath
        defines the folder where the config file will be saved
 
        .Parameter ConfigXMLFileName
        name of the config file
 
        .Parameter OfficeEdition
        define the office edtion proplus or business for example
 
        .Parameter ExcludeApps
        apps that should not be installed
 
        .Parameter IncludeApps
        apps that should be installed
 
        .Parameter LanguageIDs
        languages that should be installed, defaults to matchos
 
        .Parameter DisplayLevel
        installation display level
 
        .Parameter OfficeArch
        architecture of the microsoft 365 suite
 
        .Parameter AcceptEULA
        should the eula be accepted or not
 
        .Parameter EnableUpdates
        enabled updates for the suite
 
        .Parameter Channel
        update channel
 
        .Parameter IncludeProject
        include project in the installation
 
        .Parameter IncludeVisio
        include visio in the installation
 
        .Parameter SharedComputerLicensing
        activate shared computer licensing
 
        .Parameter RemoveMSI
        unininstall msi version of office before starting the installation
 
        .Parameter SetFileFormat
        sets the default file format for word, excel, powerpoint
         
 
        .Example
        # this will create the config xml under $WorkDir to install onyl the specified apps
        New-C2RConfigXML -ConfigXMLPath $WorkingDir -ConfigXMLFileName "config.xml" -IncludeApps "Outlook", "Word", "Excel", "Teams"
 
        .NOTES
        https://github.com/mallockey/Install-Office365Suite/blob/master/Install-Office365Suite.ps1
    #>


    [CmdletBinding(DefaultParameterSetName = 'ExcludeApps')]
    param(
        [Parameter(Mandatory = $false)]
        [string]
        $ConfigXMLPath = "$($env:ProgramData)\NTS\Office\",

        [Parameter(Mandatory = $false)]
        [string]
        $ConfigXMLFileName = "config.xml",

        [Parameter(Mandatory = $false)]
        [ValidateSet(
            "O365ProPlusRetail",
            "O365BusinessRetail",
            "ProPlus2021Volume",
            "ProPlus2019Volume"
        )]
        [string]
        $OfficeEdition = 'O365ProPlusRetail',

        [Parameter(Mandatory = $false, ParameterSetName = "ExcludeApps")]
        [ValidateSet(
            'Groove', 
            'Outlook', 
            'OneNote', 
            'Access', 
            'OneDrive', 
            'Publisher', 
            'Word', 
            'Excel', 
            'PowerPoint', 
            'Teams', 
            'Lync'
        )]
        [Array]
        $ExcludeApps,

        [Parameter(Mandatory = $false, ParameterSetName = "IncludeApps")]
        [ValidateSet(
            'Groove', 
            'Outlook', 
            'OneNote', 
            'Access', 
            'OneDrive', 
            'Publisher', 
            'Word', 
            'Excel', 
            'PowerPoint', 
            'Teams', 
            'Lync'
        )]
        [Array]
        $IncludeApps,

        [Parameter(Mandatory = $false)]
        [Array]
        $LanguageIDs,

        [Parameter(Mandatory = $false)]
        [ValidateSet("None", "Full")]
        [string]
        $DisplayLevel = "Full",

        [Parameter(Mandatory = $false)]
        [ValidateSet('64', '32')]
        [string]
        $OfficeArch = '64',

        [Parameter(Mandatory = $false)]
        [ValidateSet("TRUE", "FALSE")]
        [string]
        $AcceptEULA = "TRUE",

        [Parameter(Mandatory = $false)]
        [ValidateSet("TRUE", "FALSE")]
        [string]
        $EnableUpdates = "TRUE",

        [Parameter(Mandatory = $false)]
        [ValidateSet('SemiAnnualPreview', 'SemiAnnual', 'MonthlyEnterprise', 'CurrentPreview', 'Current')]
        [string]
        $Channel = 'Current',

        [Parameter(Mandatory = $false)]
        [Switch]
        $IncludeProject,

        [Parameter(Mandatory = $false)]
        [Switch]
        $IncludeVisio,

        [Parameter(Mandatory = $false)]
        [ValidateSet(0, 1)]
        [string]
        $SharedComputerLicensing = '0',

        [Parameter(Mandatory = $false)]
        [bool]
        $RemoveMSI = $true,

        [Parameter(Mandatory = $false)]
        [bool]
        $SetFileFormat = $true
    )

    $ErrorActionPreference = 'Stop'
    $ValidApps = (
        'Groove', 
        'Outlook', 
        'OneNote', 
        'Access', 
        'OneDrive', 
        'Publisher', 
        'Word', 
        'Excel', 
        'PowerPoint', 
        'Teams', 
        'Lync'
    )
  
    if ($ExcludeApps -and $IncludeApps) {
        throw "you can use 'ExcludeApps' or 'IncludeApps' not both"
    }
    elseif ($ExcludeApps) {
        $ExcludeApps | ForEach-Object {
            $ExcludeAppsString += "<ExcludeApp ID =`"$_`" />"
        }
    }
    elseif ($IncludeApps) {
        $ValidApps | Where-Object { $PSItem -notin $IncludeApps } | ForEach-Object {
            $ExcludeAppsString += "<ExcludeApp ID =`"$_`" />"
        }
    }
  
    if ($LanguageIDs) {
        $LanguageIDs | ForEach-Object {
            $LanguageString += "<Language ID =`"$_`" />"
        }
    }
    else {
        $LanguageString = "<Language ID=`"MatchOS`" />"
    }
  
    if ($OfficeArch) {
        $OfficeArchString = "`"$OfficeArch`""
    }
  
    if ($RemoveMSI) {
        $RemoveMSIString = '<RemoveMSI />'
    }
    else {
        $RemoveMSIString = $Null
    }
  
    if ($SetFileFormat) {
        $AppSettingsString = '<AppSettings>
        <User Key="software\microsoft\office\16.0\excel\options" Name="defaultformat" Value="51" Type="REG_DWORD" App="excel16" Id="L_SaveExcelfilesas" />
        <User Key="software\microsoft\office\16.0\powerpoint\options" Name="defaultformat" Value="27" Type="REG_DWORD" App="ppt16" Id="L_SavePowerPointfilesas" />
        <User Key="software\microsoft\office\16.0\word\options" Name="defaultformat" Value="" Type="REG_SZ" App="word16" Id="L_SaveWordfilesas" />
      </AppSettings>'

    }
    else {
        $AppSettingsString = $Null
    }
  
    if ($OfficeEdition -eq "ProPlus2021Volume") {
        $ChannelString = "Channel=`"PerpetualVL2021`""
    }
    elseif ($OfficeEdition -eq "ProPlus2019Volume") {
        $ChannelString = "Channel=`"PerpetualVL2019`""
    }
    elseif ($Channel) {
        $ChannelString = "Channel=`"$Channel`""
    }
    else {
        $ChannelString = $Null
    }
  
    if ($IncludeProject) {
        $ProjectString = "<Product ID=`"ProjectProRetail`"`>$ExcludeAppsString $LanguageString</Product>"
    }
    else {
        $ProjectString = $Null
    }
  
    if ($IncludeVisio) {
        $VisioString = "<Product ID=`"VisioProRetail`"`>$ExcludeAppsString $LanguageString</Product>"
    }
    else {
        $VisioString = $Null
    }
  
    $OfficeXML = [XML]@"
<Configuration>
    <Add OfficeClientEdition=$($OfficeArchString) $($ChannelString) $($SourcePathString) >
    <Product ID="$($OfficeEdition)">
        $($LanguageString)
        $($ExcludeAppsString)
    </Product>
    $($ProjectString)
    $($VisioString)
    </Add>
    <Property Name="SharedComputerLicensing" Value="$($SharedComputerlicensing)" />
    <Display Level="$($DisplayLevel)" AcceptEULA="$($AcceptEULA)" />
    <Updates Enabled="$($EnableUpdates)" />
    $($AppSettingsString)
    $($RemoveMSIString)
</Configuration>
"@

  
    try {
        if (!(Test-Path $ConfigXMLPath)) {
            New-Item -Path $ConfigXMLPath -ItemType Directory | Out-Null
        }

        Write-Output "$($env:COMPUTERNAME): XML Config file will be saved to $($ConfigXMLPath)\$($ConfigXMLFileName)"
        $OfficeXML.Save("$($ConfigXMLPath)\$($ConfigXMLFileName)")
    }
    catch {
        throw "could not create config xml at $($ConfigXMLPath) - $($PSItem.Exception.Message)"
    }
}

function Get-C2RSetup {
        <#
        .Description
        this function downloads office deployment tool kit and extract it, the setup.exe file be keept
 
        .Parameter OutPath
        folder pat where the setup file should be placed
 
        .Parameter FileName
        name of the setup.exe file
 
        .Parameter KeepODTFiles
        if specified, the folder containing the office deployment tool kit files will not be removed
 
        .Example
        # this will install download the setup.exe file to the folder in $Workdir
        Get-C2RSetup -OutPath $WorkingDir -FileName "Setup.exe"
 
        .NOTES
        https://github.com/mallockey/Install-Office365Suite/blob/master/Install-Office365Suite.ps1
    #>


    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)]
        [string]
        $OutPath = "$($env:ProgramData)\NTS\Office\Setup",

        [Parameter(Mandatory = $false)]
        [string]
        $FileName = "setup.exe",

        [Parameter(Mandatory = $false)]
        [switch]
        $KeepODTFiles
    )

    $ODTFolder = "$($OutPath)\ODT"
    $ODTSetupFilePath = "$($ODTFolder)\ODTSetup.exe"

    # get odt
    try {
        if (!(Test-Path $OutPath)) {
            New-Item -Path $OutPath -ItemType Directory | Out-Null
        }
        if (!(Test-Path $ODTFolder)) {
            New-Item -Path $ODTFolder -ItemType Directory | Out-Null
        }
        if (Test-Path -Path $ODTSetupFilePath) {
            throw "$($ODTSetupFilePath) already exits"
        }
        else {
            try {                
                [String]$MSWebPage = Invoke-RestMethod "https://www.microsoft.com/en-us/download/confirmation.aspx?id=49117"
                    $ODTDownloadURL = $MSWebPage | ForEach-Object {
                    if ($PSItem -match 'url=(https://.*officedeploymenttool.*\.exe)') {
                        $matches[1]
                    }
                }
            }
            catch {
                throw "could not find odt download url - $($PSItem.Exception.Message)"
            }

            Invoke-WebRequest -Uri $ODTDownloadURL -OutFile $ODTSetupFilePath
        }
    }
    catch {
        throw "could not download ODTSetup - $($PSItem.Exception.Message)"
    }

    # get setup
    try {
        Write-Output "$($env:COMPUTERNAME): extracting office deployment tool kit"
        Start-Process -FilePath $ODTSetupFilePath -ArgumentList "/quiet /extract:`"$($ODTFolder)`"" -Wait

        Write-Output "$($env:COMPUTERNAME): Copy Setup.exe to $($OutPath)"
        Copy-Item -Path "$($ODTFolder)\setup.exe" -Destination "$($OutPath)\$($FileName)" -Force

        if (!($KeepODTFiles)) {
            Start-FolderCleanUp -FolderToRemove $ODTFolder
        }
    }
    catch {
        Write-Warning "$($env:COMPUTERNAME): Error running the Office Deployment Tool - $($PSItem.Exception.Message)"
    }
}

function Test-OfficeConfiguration {
        <#
        .Description
        this function verifies a office config xml file against microsofts service
 
        .Parameter ConfigurationXMLFilePath
        path to the xml file
 
        .Example
        # this will verify the xml file at $ConfigXMLPath
        Test-OfficeConfiguration -ConfigurationXMLFilePath $ConfigXMLPath
 
        .NOTES
        https://github.com/mallockey/Install-Office365Suite/blob/master/Install-Office365Suite.ps1
    #>


    param(
        [Parameter(Mandatory = $true)]
        [String]
        $ConfigurationXMLFilePath
    )

    $ErrorActionPreference = 'Stop'
      
    try {
        $OfficeXML = Get-Content -Path $ConfigurationXMLFilePath
    }
    catch {
        throw "There was an error generating the XML config file"
    }

    try {
        Write-Output "$($env:COMPUTERNAME): Uploading XML config file to clients.config.office.net to verify syntax"
      
        Invoke-RestMethod -Uri 'https://clients.config.office.net/intents/v1.0/DeploymentSettings/ImportConfiguration' `
            -Method Post  `
            -Body $OfficeXML `
            -ContentType 'text/xml' | Out-Null
      
        Write-Output "$($env:COMPUTERNAME): XML config file was successfully verified"
    }
    catch {
        throw "The XML is not formatted correctly"
    }
}