Import-VcCmApp.ps1

Function Import-VcCmApp {
    <#
    .SYNOPSIS
        Installs and/or downloads the Visual C++ Redistributables listed in an external XML file.

    .DESCRIPTION
        This script will download the Visual C++ Redistributables listed in an external XML file into a folder structure that represents release and processor architecture. If the redistributable exists in the specified path, it will not be re-downloaded.

        In addition the script can optionally perform one of 3 tasks:
         1. Install the redistributables on the local machine
         2. Create a single application in MDT to install the redistributables
         3. Create an application for each redistributable in ConfigMgr.
        
        You can filter on redistributable releases and processor architectures with the -Release and -Architecture parameters.

        A complete XML file listing the redistributables is included. The basic structure of the XML file should be:

        <Redistributables>
            <Platform Architecture="x64" Release="" Install="">
                <Redistributable>
                    <Name></Name>
                    <ShortName></ShortName>
                    <URL></URL>
                    <ProductCode></ProductCode>
                    <Download></Download>
            </Platform>
            <Platform Architecture="x86" Release="" Install="">
                <Redistributable>
                    <Name></Name>
                    <ShortName></ShortName>
                    <URL></URL>
                    <ProductCode></ProductCode>
                    <Download></Download>
                </Redistributable>
            </Platform>
        </Redistributables>

    .NOTES
        Name: Install-VisualCRedistributables.ps1
        Author: Aaron Parker
        Twitter: @stealthpuppy

    .LINK
        http://stealthpuppy.com

    .PARAMETER Xml
        The XML file that contains the details about the Visual C++ Redistributables. This must be in the expected format.

    .EXAMPLE
        .\Install-VisualCRedistributables.ps1 -Xml ".\VisualCRedistributables.xml"

        Description:
        Downloads the Visual C++ Redistributables listed in VisualCRedistributables.xml.

    .PARAMETER Path
        Specify a target folder to download the Redistributables to, otherwise use the current folder.

    .EXAMPLE
        .\Install-VisualCRedistributables.ps1 -Xml ".\VisualCRedistributables.xml" -Path C:\Redist

        Description:
        Downloads the Visual C++ Redistributables listed in VisualCRedistributables.xml to C:\Redist.

    .PARAMETER Release
        Specifies the release (or version) of the redistributables to download or install.

    .EXAMPLE
        .\Install-VisualCRedistributables.ps1 -Xml ".\VisualCRedistributables.xml" -Release "2012","2013",2017"

        Description:
        Downloads only the 2012, 2013 & 2017 releases of the Visual C++ Redistributables listed in VisualCRedistributables.xml

    .PARAMETER Architecture
        Specifies the processor architecture to download or install.

    .EXAMPLE
        .\Install-VisualCRedistributables.ps1 -Xml ".\VisualCRedistributables.xml" -Architecture "x64"

        Description:
        Downloads only the 64-bit versions of the Visual C++ Redistributables listed in VisualCRedistributables.xml.

    .PARAMETER Install
        By default the script will only download the Redistributables. Add -Install to install each of the Redistributables as well.

    .EXAMPLE
        .\Install-VisualCRedistributables.ps1 -Xml ".\VisualCRedistributables.xml" -Install

        Description:
        Downloads and installs the Visual C++ Redistributables listed in VisualCRedistributables.xml.

    .PARAMETER CreateCMApp
        Switch parameter to create ConfigMgr apps from downloaded redistributables.

    .Parameter SMSSiteCode
        Specify the Site Code for ConfigMgr app creation.

    .EXAMPLE
        .\Install-VisualCRedistributables.ps1 -Xml ".\VisualCRedistributables.xml" -Path \\server1\Sources\Apps\VSRedist -CreateCMApp -SMSSiteCode S01

        Description:
        Downloads Visual C++ Redistributables listed in VisualCRedistributables.xml and creates ConfigMgr Applications for the selected Site.

    .PARAMETER CreateMDTApp
        Switch parameter to create a single MDT apps for the downloaded redistributables.

    .Parameter MDTPath
        Specify the root path to the MDT deployment share.

    .EXAMPLE
        .\Install-VisualCRedistributables.ps1 -Xml ".\VisualCRedistributables.xml" -CreateMDTApp -MDTPath \\server1\deploymentshare

        Description:
        Downloads Visual C++ Redistributables listed in VisualCRedistributables.xml and creates a single MDT application in the deployment share \\server1\deploymentshare.
#>


    # Parameter sets here means that Install, MDT and ConfigMgr actions are mutually exclusive
    [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = "Low", DefaultParameterSetName = 'Base')]
    PARAM (
        [Parameter(ParameterSetName = 'Base', Mandatory = $True, HelpMessage = "The path to the XML document describing the Redistributables.")]
        [Parameter(ParameterSetName = 'Install')]
        [Parameter(ParameterSetName = 'ConfigMgr')]
        [Parameter(ParameterSetName = 'MDT')]
        [ValidateScript( { If (Test-Path $_ -PathType 'Leaf') { $True } Else { Throw "Cannot find file $_" } })]
        [string]$Xml,

        [Parameter(ParameterSetName = 'Base', Mandatory = $False, HelpMessage = "Specify a target path to download the Redistributables to.")]
        [Parameter(ParameterSetName = 'Install')]
        [Parameter(ParameterSetName = 'ConfigMgr')]
        [ValidateScript( { If (Test-Path $_ -PathType 'Container') { $True } Else { Throw "Cannot find path $_" } })]
        [string]$Path = ".\",

        [Parameter(ParameterSetName = 'Base', Mandatory = $False, HelpMessage = "Specify the version of the Redistributables to install.")]
        [Parameter(ParameterSetName = 'Install')]
        [Parameter(ParameterSetName = 'ConfigMgr')]
        [ValidateSet('2005', '2008', '2010', '2012', '2013', '2015', '2017')]
        [string[]]$Release = @("2008", "2010", "2012", "2013", "2015", "2017"),

        [Parameter(ParameterSetName = 'Base', Mandatory = $False, HelpMessage = "Specify the processor architecture/s to install.")]
        [Parameter(ParameterSetName = 'Install')]
        [Parameter(ParameterSetName = 'ConfigMgr')]
        [ValidateSet('x86', 'x64')]
        [string[]]$Architecture = @("x86", "x64"),

        [Parameter(ParameterSetName = 'Install', Mandatory = $True, HelpMessage = "Enable the installation of the Redistributables after download.")]
        [switch]$Install,

        [Parameter(ParameterSetName = 'ConfigMgr', Mandatory = $True, HelpMessage = "Create Redistributable applications in ConfigMgr.")]
        [switch]$CreateCMApp,

        [Parameter(ParameterSetName = 'ConfigMgr', Mandatory = $True, HelpMessage = "Specify ConfigMgr Site Code.")]
        [ValidateScript( { If ($_ -match "^[a-zA-Z0-9]{3}$") { $True } Else { Throw "$_ is not a valid ConfigMgr site code." } })]
        [string]$SMSSiteCode,

        [Parameter(ParameterSetName = 'MDT', Mandatory = $True, HelpMessage = "Create Redistributable applications in the Microsoft Deployment Toolkit.")]
        [switch]$CreateMDTApp,

        [Parameter(ParameterSetName = 'MDT', Mandatory = $True, HelpMessage = "The path to the MDT deployment share root.")]
        [ValidateScript( { If (Test-Path $_ -PathType 'Container') { $True } Else { Throw "Cannot find path $_" } })]
        [string]$MDTPath
    )

    BEGIN {
        # Get script elevation status
        # [bool]$Elevated = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")

        # Test $Path and throw an error if we can't find it
        # Have an issue with ValidateScript when we change $Path when creating an MDT application
        # If (!(Test-Path $Path -PathType 'Container')) { Throw "Cannot find path $Path." }

        ##### If CreateCMApp parameter specified, load the Configuration Manager module
        If ($CreateCMApp) {
        
            # If import apps into ConfigMgr, the download location will have to be a UNC path
            If (!([bool]([System.Uri]$Path).IsUnc)) { Throw "$Path must be a valid UNC path." }
            If (!(Test-Path $Path)) { Throw "Unable to confirm $Path exists. Please check that $Path is valid." }

            # If the ConfigMgr console is installed, load the PowerShell module
            # Requires PowerShell module to be installed
            If (Test-Path env:SMS_ADMIN_UI_PATH) {
                Try {            
                    Import-Module "$($env:SMS_ADMIN_UI_PATH)\..\ConfigurationManager.psd1" # Import the ConfigurationManager.psd1 module
                }
                Catch {
                    Throw "Could not load ConfigMgr Module. Please make sure that the ConfigMgr Console is installed."
                }
            }
            Else {
                Throw "Cannot find environment variable SMS_ADMIN_UI_PATH. Is the ConfigMgr Console and PowerShell module installed?"
            }
        }

        ##### If CreateMDTApp parameter specified, load the MDT module
        If ($CreateMDTApp) {
            $mdtDrive = "DS001"
            $tempFolder = "$env:Temp\Install-VisualCRedistributables"
            $publisher = "Microsoft"
            $shortName = "Visual C++ Redistributables"
        
            # If we can find the MDT PowerShell module, import it. Requires MDT console to be installed
            $mdtModule = "$((Get-ItemProperty "HKLM:SOFTWARE\Microsoft\Deployment 4").Install_Dir)bin\MicrosoftDeploymentToolkit.psd1"
            If (Test-Path -Path $mdtModule) {
                Try {            
                    Import-Module -Name $mdtModule
                }
                Catch {
                    Throw "Could not load MDT PowerShell Module. Please make sure that the MDT console is installed correctly."
                }
            }
            Else {
                Throw "Cannot find the MDT PowerShell module. Is the MDT console installed?"
            }

            If ($pscmdlet.ShouldProcess("Script and XML for import.", "Copy")) {
                # Copy the script and XML file to a temporary folder for importing
                Write-Verbose "Creating folder: $tempFolder"
                New-Item "$tempFolder" -Type Directory | Out-Null
                Copy-Item $MyInvocation.MyCommand.Definition $tempFolder
                Copy-Item $Xml $tempFolder
            }
        
            # Create the PSDrive for MDT
            If ($pscmdlet.ShouldProcess("Mapping PSDrive to MDT deployment share $MDTPath", "Mapping")) {
                If (Test-Path "$($mdtDrive):") {
                    Write-Verbose "Found existing MDT drive $mdtDrive. Removing."
                    Remove-PSDrive -Name $mdtDrive -Force
                }
                New-PSDrive -Name $mdtDrive -PSProvider MDTProvider -Root $MDTPath
            }

            If ($pscmdlet.ShouldProcess("$shortName in $MDTPath", "Import MDT app")) {

                If (!(Test-Path("$($mdtDrive):.\Applications\$publisher $shortName"))) {

                    # Convert arrays to strings to pass to the MDT command line
                    # $cRelease = [system.String]::Join(",", $Release)
                    # $cArchitecture = [system.String]::Join(",", $Architecture)

                    $filename = $xml.Substring($xml.LastIndexOf("\") + 1)
                    $CommandLine = "powershell.exe -ExecutionPolicy Bypass -NonInteractive -WindowStyle Minimized -File .\$($MyInvocation.MyCommand.Name) -Xml '.\$filename' -Install" 

                    # Import as an application into MDT
                    Import-MDTApplication -path "$($mdtDrive):\Applications" -enable "True" `
                        -Name "$publisher $shortName" `
                        -ShortName $shortName `
                        -Version "" -Publisher $publisher -Language "en-US" `
                        -CommandLine $CommandLine `
                        -WorkingDirectory ".\Applications\$publisher $shortName" `
                        -ApplicationSourcePath $tempFolder `
                        -DestinationFolder "$publisher $shortName"
                }
                Else {
                    Throw "Application: '$publisher $shortName' already exists in the specified MDT deployment share."
                }
            }

            # Remove the temporary folder
            If ($pscmdlet.ShouldProcess($tempFolder, "Remove")) {
                Remove-Item "$tempFolder" -Recurse -Force
            }

            # Update Path to point to the MDT application location
            # Script will then download the redistributables there for install at deployment time
            Remove-Variable Path
            $Path = "$MDTPath\Applications\$publisher $shortName"
            If ($pscmdlet.ShouldProcess($Path, "Create")) {
                If (!(Test-Path $Path)) { New-Item -Path $Path -Type 'Directory' -Force | Out-Null }
            }
        }
    }

    PROCESS {

        # Read the specifed XML document
        Try {
            [xml]$xmlDocument = Get-Content -Path $Xml -ErrorVariable xmlReadError
        }
        Catch {
            Throw "Unable to read $Xml. $xmlReadError"
        }

        ##### If -Release and -Architecture are specified, filter the XML content
        If ($PSBoundParameters.ContainsKey('Release') -or $PSBoundParameters.ContainsKey('Architecture')) {

            # Create an array that we'll add the filtered XML content to
            $xmlContent = @()

            # If -Release alone is specified, filter on platform
            If ($PSBoundParameters.ContainsKey('Release') -and (!($PSBoundParameters.ContainsKey('Architecture')))) {
                ForEach ($rel in $Release) {
                    $xmlContent += (Select-Xml -XPath "/Redistributables/Platform[@Release='$rel']" -Xml $xmlDocument).Node
                }
            }
            # If -Architecture alone is specified, filter on architecture
            If ($PSBoundParameters.ContainsKey('Architecture') -and (!($PSBoundParameters.ContainsKey('Release')))) {
                ForEach ($arch in $Architecture) {
                    $xmlContent += (Select-Xml -XPath "/Redistributables/Platform[@Architecture='$arch']" -Xml $xmlDocument).Node
                }
            }
            # If -Architecture and -Release are specified, filter on both
            If ($PSBoundParameters.ContainsKey('Architecture') -and $PSBoundParameters.ContainsKey('Release')) {
                ForEach ($rel in $Release) {
                    ForEach ($arch in $Architecture) {
                        $xmlContent += (Select-Xml -XPath "/Redistributables/Platform[@Release='$rel'][@Architecture='$arch']" -Xml $xmlDocument).Node
                    }
                }
            }
        }
        Else {
        
            # Pass the XML document contents to $xmlContent, so that we don't need to provide
            # different logic if -Platform and -Architectures are not supplied
            $xmlContent = @()
            $xmlContent += (Select-Xml -XPath "/Redistributables/Platform" -Xml $xmlDocument).Node
        }

        ##### Loop through each setting in the XML structure to process each redistributable
        ForEach ($platform in $xmlContent) {

            # Create variables from the Platform content to simplify references below
            $plat = $platform | Select-Object -ExpandProperty Architecture
            $rel = $platform | Select-Object -ExpandProperty Release
            $arg = $platform | Select-Object -ExpandProperty Install

            # Step through each redistributable defined in the XML
            ForEach ($redistributable in $platform.Redistributable) {
            
                # Create variables from the Redistributable content to simplify references below
                $uri = $redistributable.Download
                $filename = $uri.Substring($uri.LastIndexOf("/") + 1)
                If ([bool]([System.Uri]$Path).IsUnc) { 
                    $target = "$Path\$rel\$plat\$($redistributable.ShortName)"
                }
                Else {
                    $target = "$((Get-Item $Path).FullName)\$rel\$plat\$($redistributable.ShortName)"
                }
            
                # Create the folder to store the downloaded file. Skip if it exists
                If (!(Test-Path -Path $target)) {
                    If ($pscmdlet.ShouldProcess($target, "Create")) {
                        New-Item -Path $target -Type Directory -Force | Out-Null
                    }
                }
                Else {
                    Write-Verbose "Folder '$($redistributable.ShortName)' exists. Skipping."
                }


                ##### Download the Redistributable to the target path. Skip if it exists
                If (!(Test-Path -Path "$target\$filename" -PathType 'Leaf')) {
                    If ($pscmdlet.ShouldProcess($uri, "Download")) {
                        Invoke-WebRequest -Uri $uri -OutFile "$target\$filename"
                    }
                }
                Else {
                    Write-Verbose "Redistributable exists. Skipping."
                }

                # Install the Redistributable if the -Install switch is specified
                If ($Install) {
                    If ($pscmdlet.ShouldProcess("'$target\$filename $arg'", "Installing")) {
                        Start-Process -FilePath "$target\$filename" -ArgumentList $arg -Wait
                    }
                }


                ##### Create an application for the redistributable in ConfigMgr
                If ($CreateCMApp) {
                
                    # Ensure the current folder is saved
                    Push-Location -StackName FileSystem
                    Try {
                        If ($pscmdlet.ShouldProcess($SMSSiteCode + ":", "Set location")) {
                        
                            # Set location to the PSDrive for the ConfigMgr site
                            Set-Location ($SMSSiteCode + ":") -ErrorVariable ConnectionError
                        }
                    }
                    Catch {
                        $ConnectionError
                    }
                    Try {

                        # Create the ConfigMgr application with properties from the XML file
                        If ($pscmdlet.ShouldProcess($redistributable.Name + " $plat", "Creating ConfigMgr application")) {
                            $app = New-CMApplication -Name ($redistributable.Name + " $plat") -ErrorVariable CMError -Publisher "Microsoft"
                        
                            Add-CMScriptDeploymentType -InputObject $app -InstallCommand "$filename $arg" -ContentLocation $target `
                                -ProductCode $redistributable.ProductCode -DeploymentTypeName ("SCRIPT_" + $redistributable.Name) `
                                -UserInteractionMode Hidden -UninstallCommand "msiexec /x $($redistributable.ProductCode) /qn-" `
                                -LogonRequirementType WhetherOrNotUserLoggedOn -InstallationBehaviorType InstallForSystem -ErrorVariable CMError `
                                -Comment "Generated by $($MyInvocation.MyCommand.Name)"
                        }

                    }
                    Catch {
                        $CMError
                    }

                    # Go back to the original folder
                    Pop-Location -StackName FileSystem
                }
            }
        }
    }
}