MSFTLinkDownloader.psm1

Function Get-HrefMatches {
    [CmdletBinding()]
    param(
        ## The filename to parse
        [Parameter(Mandatory = $true)]
        [string] $content,

        ## The Regular Expression pattern with which to filter
        ## the returned URLs
        [string] $Pattern = "<\s*a\s*[^>]*?href\s*=\s*[`"']*([^`"'>]+)[^>]*?>"
    )

    $returnMatches = new-object System.Collections.ArrayList

    ## Match the regular expression against the content, and
    ## add all trimmed matches to our return list
    $resultingMatches = [Regex]::Matches($content, $Pattern, "IgnoreCase")
    foreach($match in $resultingMatches)
    {
        $cleanedMatch = $match.Groups[1].Value.Trim()
        [void] $returnMatches.Add($cleanedMatch)
    }

    $returnMatches
}

Function Get-Hyperlinks {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)]
        [string] $content,
        [string] $Pattern = "<A[^>]*?HREF\s*=\s*""([^""]+)""[^>]*?>([\s\S]*?)<\/A>"
    )
    $resultingMatches = [Regex]::Matches($content, $Pattern, "IgnoreCase")

    $returnMatches = @()
    foreach($match in $resultingMatches){
        $LinkObjects = New-Object -TypeName PSObject
        $LinkObjects | Add-Member -Type NoteProperty `
            -Name Text -Value $match.Groups[2].Value.Trim()
        $LinkObjects | Add-Member -Type NoteProperty `
            -Name Href -Value $match.Groups[1].Value.Trim()

        $returnMatches += $LinkObjects
    }
    $returnMatches
}

Function Get-WebContentHeader{
    #https://stackoverflow.com/questions/41602754/get-website-metadata-such-as-title-description-from-given-url-using-powershell
    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true,Position=1,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        #[Microsoft.PowerShell.Commands.HtmlWebResponseObject]$WebContent,
        $WebContent,

        [Parameter(Mandatory=$false)]
        [ValidateSet('Keywords','Description','Title')]
        [string]$Property
    )

    ## -------- PARSE TITLE, DESCRIPTION AND KEYWORDS ----------
    $resultTable = @{}
    # Get the title
    $resultTable.title = $WebContent.ParsedHtml.title
    # Get the HTML Tag
    $HtmlTag = $WebContent.ParsedHtml.childNodes | Where-Object {$_.nodename -eq 'HTML'}
    # Get the HEAD Tag
    $HeadTag = $HtmlTag.childNodes | Where-Object {$_.nodename -eq 'HEAD'}
    # Get the Meta Tags
    $MetaTags = $HeadTag.childNodes| Where-Object {$_.nodename -eq 'META'}
    # You can view these using $metaTags | select outerhtml | fl
    # Get the value on content from the meta tag having the attribute with the name keywords
    $resultTable.keywords = $metaTags  | Where-Object {$_.name -eq 'keywords'} | Select-Object -ExpandProperty content
    # Do the same for description
    $resultTable.description = $metaTags  | Where-Object {$_.name -eq 'description'} | Select-Object -ExpandProperty content
    # Return the table we have built as an object

    switch($Property){
        'Keywords'       {Return $resultTable.keywords}
        'Description'    {Return $resultTable.description}
        'Title'          {Return $resultTable.title}
        default          {Return $resultTable}
    }
}

Function Initialize-FileDownload {
    [CmdletBinding()]
    param(
         [Parameter(Mandatory=$false)]
         [Alias("Title")]
         [string]$Name,

         [Parameter(Mandatory=$true,Position=1)]
         [string]$Url,

         [Parameter(Mandatory=$true,Position=2)]
         [Alias("TargetDest")]
         [string]$TargetFile
     )
     Begin{
         ## Get the name of this function
         [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name

         ## Check running account
         [Security.Principal.WindowsIdentity]$CurrentProcessToken = [Security.Principal.WindowsIdentity]::GetCurrent()
         [Security.Principal.SecurityIdentifier]$CurrentProcessSID = $CurrentProcessToken.User
         [boolean]$IsLocalSystemAccount = $CurrentProcessSID.IsWellKnown([Security.Principal.WellKnownSidType]'LocalSystemSid')
         [boolean]$IsLocalServiceAccount = $CurrentProcessSID.IsWellKnown([Security.Principal.WellKnownSidType]'LocalServiceSid')
         [boolean]$IsNetworkServiceAccount = $CurrentProcessSID.IsWellKnown([Security.Principal.WellKnownSidType]'NetworkServiceSid')
         [boolean]$IsServiceAccount = [boolean]($CurrentProcessToken.Groups -contains [Security.Principal.SecurityIdentifier]'S-1-5-6')
         [boolean]$IsProcessUserInteractive = [Environment]::UserInteractive
     }
     Process
     {
         $ChildURLPath = $($url.split('/') | Select-Object -Last 1)

         $uri = New-Object "System.Uri" "$url"
         $request = [System.Net.HttpWebRequest]::Create($uri)
         $request.set_Timeout(15000) #15 second timeout
         $response = $request.GetResponse()
         $totalLength = [System.Math]::Floor($response.get_ContentLength()/1024)
         $responseStream = $response.GetResponseStream()
         $targetStream = New-Object -TypeName System.IO.FileStream -ArgumentList $targetFile, Create

         $buffer = new-object byte[] 10KB
         $count = $responseStream.Read($buffer,0,$buffer.length)
         $downloadedBytes = $count

         If($Name){$Label = $Name}Else{$Label = $ChildURLPath}

         Write-Verbose ("{0} : Initializing File Download from URL: {1}" -f ${CmdletName},$Url)

         while ($count -gt 0)
         {
             $targetStream.Write($buffer, 0, $count)
             $count = $responseStream.Read($buffer,0,$buffer.length)
             $downloadedBytes = $downloadedBytes + $count

             # display progress
             # Check if script is running with no user session or is not interactive
             If ( ($IsProcessUserInteractive -eq $false) -or $IsLocalSystemAccount -or $IsLocalServiceAccount -or $IsNetworkServiceAccount -or $IsServiceAccount) {
                 # display nothing
                 #write-host "." -NoNewline
             }
             Else{
                 Write-Progress -Activity ("Downloading {0}" -f $Name) -Status ("Downloading: {0} ($([System.Math]::Floor($downloadedBytes/1024))K of $($totalLength)K): " -f $Label) -PercentComplete ( ([System.Math]::Floor($downloadedBytes/1024) / $totalLength) * 100 ) -id 1
             }
         }

         Start-Sleep 1

         $targetStream.Flush()
         $targetStream.Close()
         $targetStream.Dispose()
         $responseStream.Dispose()
    }
    End{
        #Write-Progress -activity "Finished downloading file '$($url.split('/') | Select-Object -Last 1)'"
        If($Name){$Label = $Name}Else{$Label = $ChildURLPath}
        Write-Progress -Activity ("Finished downloading file: {0}" -f $Label) -Completed
        #change meta in file from internet to allow to run on system
        If(Test-Path $TargetFile){Unblock-File $TargetFile -ErrorAction SilentlyContinue | Out-Null}
    }

}

function Get-ZipFileSize {

    param (
        [ValidateScript({Get-item $_ -Include '*.zip'})]
        $Path
    )
    $ZipSize = (Get-item $path).length/1kb

    #open zip using explorer to unzip
    $shell = New-Object -ComObject shell.application
    $zip = $shell.NameSpace($Path)
    $size = 0
    foreach ($item in $zip.items()) {
        if ($item.IsFolder) {
            $size += Get-UncompressedZipFileSize -Path $item.Path
        } else {
            $size += $item.size
        }
    }

    # It might be a good idea to dispose the COM object now explicitly, see comments below
    [System.Runtime.InteropServices.Marshal]::ReleaseComObject([System.__ComObject]$shell) | Out-Null
    [System.GC]::Collect()
    [System.GC]::WaitForPendingFinalizers()

    $Zipdata = '' | Select ZippedKbSize,UnzippedKbSize
    $Zipdata.ZippedKbSize = ("{0:F2}" -f $ZipSize)
    $Zipdata.UnzippedKbSize = ("{0:F2}" -f ($size/1kb))

    return $Zipdata
}

function Expand-ShortLink {
    param (
        [Parameter(Mandatory=$true,Position=1,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)]
        [string]$ShortLink
    )
    Try{
        $RedirectedURI = [System.Net.HttpWebRequest]::Create($ShortLink).GetResponse().ResponseUri.AbsoluteUri
    }Catch{
        $RedirectedURI = $Null
    }
    Return $RedirectedURI
}

# MICROSOFT DOWNLOAD
#==================================================
function Get-MsftLink {
    <#
        .SYNOPSIS
        Retrieves File from Microsoft
 
        .DESCRIPTION
        Download files from Microsoft download site using LinkID
 
        .NOTES
        Created by: @PowershellCrack
 
        .PARAMETER LinkID
        Required. Link id from download url
 
        .PARAMETER ShortLink
        Required. Use short link to pull link id from download url
 
        .PARAMETER Filter
        Filter to reduce files found in link
 
        .PARAMETER Language
        Defaults to en-US. English.
 
        .EXAMPLE
        Get-MsftLink -LinkID '49117','104223','844652'
 
        .EXAMPLE
        Get-MsftLink -LinkID '55319' -Filter 'LGPO'
 
        .EXAMPLE
        49117,55319,104223 | Get-MsftLink -Language en-gb
 
        .LINK
        Get-HrefMatches
    #>

    [CmdletBinding(DefaultParameterSetName='ID')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory=$true,Position=1,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true,ParameterSetName='ID')]
        [int[]]$LinkID,

        [Parameter(Mandatory=$true,Position=1,ParameterSetName='Short')]
        [string]$ShortLink,

        [parameter(Mandatory=$false,Position=2)]
        [string]$Filter,

        [ValidateSet('en-us','en-gb','en-sg','en-au')]
        [string]$Language = "en-us"
    )
    Begin{
        ## Get the name of this function
        [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name

        [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12

        [System.Uri]$SourceURL = "https://www.microsoft.com/$Language/download"
        [string]$DownloadURL = "https://download.microsoft.com/download"

        
        $LinkCollection = @()
    }
    Process
    {
        If($PsCmdlet.ParameterSetName -eq 'Short'){
            $RealLink = Expand-ShortLink -ShortLink $ShortLink
            $Null = $RealLink -match '\d+$'
            $LinkID = $Matches[0]
        }

        Foreach($ID in $LinkID){
            
            Try{
                ## -------- FIND FILE LINKS ----------
                $ConfirmationLink = $SourceURL.OriginalString + "/confirmation.aspx?id=$ID"
                Write-Verbose ("{0} : Grabbing links from [{1}]..." -f ${CmdletName},$ConfirmationLink)

                $ConfirmationContent = Invoke-WebRequest $ConfirmationLink -UseBasicParsing -ErrorAction Stop
                $OfficialDownloads = Get-HrefMatches -content [string]$ConfirmationContent  | Where-Object {$_ -match $DownloadURL} | Select-Object -Unique

                #Filter
                If($Filter){
                    $OfficialDownloads = $OfficialDownloads | Where-Object {$_ -like "*$Filter*"}
                    Write-Verbose ("{0} : Found {1} official downloadable links with filter [{2}]" -f ${CmdletName},$OfficialDownloads.Count,$Filter)
                }Else{
                    Write-Verbose ("{0} : Found {1} official downloadable links" -f ${CmdletName},$OfficialDownloads.Count)
                }

                #TESTS $link = $OfficialDownloads[0]
                Foreach($link in $OfficialDownloads)
                {
                    #Build collection object
                    $Data = '' | Select LinkID,DownloadLink,FileName
                    $Data.LinkID = $ConfirmationLink
                    $Data.DownloadLink = $link

                    $Filename = $link | Split-Path -Leaf
                    $Data.FileName = $Filename

                    #add data to array
                    $LinkCollection += $Data
                } #end loop
            }
            catch {
                Write-Error ("{0} : Unable to download [{1}]. {2}" -f ${CmdletName},$Filter,$_.Exception.Message)
            }
        }
    }
    End{
        return $LinkCollection
    }
}

Function Invoke-MsftLinkDownload {
    <#
        .SYNOPSIS
        Retrieves File from Microsoft
 
        .DESCRIPTION
        Download files from Microsoft download site using LinkID
 
        .NOTES
        Created by: @PowershellCrack
 
        .PARAMETER LinkID
        Required. Link id from download url
 
        .PARAMETER Filter
        Filter to reduce files found in link
 
        .PARAMETER Language
        Defaults to en-US. English.
 
        .PARAMETER DownloadLink
        Required. download url )usually obtained by Get-MsftLink
 
        .PARAMETER Extract
        Attempts to extract zip files or extractable exe files
 
        .PARAMETER Cleanup
        Available with extract; removes archive after extraction
 
        .PARAMETER Force
        Re-downloads file even if it exists (overwrites)
 
        .PARAMETER NoProgress
        Shows no progress during download (this is useful for large file sizes and speed)
 
        .PARAMETER Passthru
        Export downloaded information as object
 
        .EXAMPLE
        Invoke-MsftLinkDownload -LinkID 49117 -DestPath C:\temp\Downloads -Force
        Invoke-MsftLinkDownload -DownloadLink -DestPath C:\temp\Downloads -Force
 
        .EXAMPLE
        Invoke-MsftLinkDownload -LinkID 55319,104223 -Filter 'Server' -DestPath C:\temp\Downloads -Force -verbose
 
        .EXAMPLE
        Invoke-MsftLinkDownload -DownloadLink 'https://download.microsoft.com/download/8/5/C/85C25433-A1B0-4FFA-9429-7E023E7DA8D8/LGPO.zip' -DestPath C:\temp\Downloads -Force
 
        .EXAMPLE
        Invoke-MsftLinkDownload -DownloadLink 'https://download.microsoft.com/download/8/5/C/85C25433-A1B0-4FFA-9429-7E023E7DA8D8/LGPO.zip' -DestPath C:\temp\Downloads -Force -Extract -Cleanup
 
        .EXAMPLE
        Get-MsftLink -LinkID 49117,104223,844652 | Invoke-MsftLinkDownload -DestPath C:\temp\Downloads -Passthru
 
        .EXAMPLE
        $Links = Get-MsftLink -LinkID 49117,55319,104223
        $Links | Invoke-MsftLinkDownload -DestPath C:\temp\Downloads -Passthru -NoProgress -Extract -Cleanup -verbose
 
        .LINK
        Get-MsftLink
        Initialize-FileDownload
        Get-UncompressedZipFileSize
    #>

    [CmdletBinding(DefaultParameterSetName='URL')]
    param(
        [Parameter(Mandatory=$true,Position=1,ParameterSetName='ID',ValueFromPipelineByPropertyName=$true)]
        [int[]]$LinkID,

        [Parameter(Mandatory=$false,ParameterSetName='ID')]
        [string]$Filter,

        [Parameter(Mandatory=$false, ParameterSetName='ID')]
        [ValidateSet('en-us','en-gb','en-sg','en-au')]
        [string]$Language = "en-us",

        [Parameter(Mandatory=$true,
                    Position=1,
                    ValueFromPipeline=$true,
                    ValueFromPipelineByPropertyName=$true,
                    ParameterSetName='URL'
        )]
        [string[]]$DownloadLink,

        [Parameter(Mandatory=$true,ParameterSetName='ID')]
        [Parameter(Mandatory=$true,ParameterSetName='URL')]
        [string]$DestPath,

        [Parameter(Mandatory=$false,ParameterSetName='ID')]
        [Parameter(Mandatory=$false,ParameterSetName='URL')]
        [switch]$Extract,

        [Parameter(Mandatory=$false,ParameterSetName='ID')]
        [Parameter(Mandatory=$false,ParameterSetName='URL')]
        [switch]$Cleanup,

        [switch]$Force,

        [switch]$NoProgress,

        [switch]$Passthru
    )
    Begin{
        ## Get the name of this function
        [string]${CmdletName} = $PSCmdlet.MyInvocation.MyCommand.Name

        ## -------- BUILD ROOT FOLDER ----------
        If( !(Test-Path $DestPath)){
            New-Item $DestPath -type directory -ErrorAction SilentlyContinue | Out-Null
        }

        If($PsCmdlet.ParameterSetName -eq 'ID'){
            $DownloadLink = @()
            $MSFTLinkParams = @{}
            $LinkIds=@()
            Foreach($Id in $LinkID){
                If($Id){
                    $LinkIds += $Id
                }
            }
            If($LinkID){
                $MSFTLinkParams += @{LinkID = ($LinkIds -join ',')}
             }
            If($Filter){
                $MSFTLinkParams += @{Filter = $Filter}
            }
            If($Language){
                $MSFTLinkParams += @{Language = $Language}
            }
            
            Write-Verbose ("{0} : Attempting to get links: [{1}]..." -f ${CmdletName},($LinkIds -join ','))
            $DownloadLink += Get-MsftLink @MSFTLinkParams | Select -ExpandProperty DownloadLink
        }

        $DownloadCollection = @()
    }
    Process
    {
        Write-Verbose ("{0} : Processing download link: [{1}]..." -f ${CmdletName},$DownloadLink)
        #TESTS $Link = $DownloadLink[0]
        Foreach($Link in $DownloadLink)
        {
            #Build collection object
            $Data = '' | Select DownloadURL,FileName,FilePath,Downloaded,Extracted,Removed
            $Data.DownloadURL = $Link

            $Filename = $Link | Split-Path -Leaf
            $destination = Join-Path $DestPath -ChildPath $Filename
            #collect data
            $Data.FileName = $Filename
            $Data.FilePath = $destination

            ## -------- DOWNLOAD ----------
            $Data.Downloaded = $False
            If( (Test-Path $destination) -and !$Force){
                Write-Verbose ("{0} : File already exists: [{1}]..." -f ${CmdletName},$Filename)
                $Data.Downloaded = $True
                #Continue
            }
            Else{
                Try{
                    Write-Verbose ("{0} : Attempting to download: [{1}]..." -f ${CmdletName},$Filename)
                    If($PSBoundParameters.ContainsKey('NoProgress') )
                    {
                        Invoke-WebRequest -Uri $Data.DownloadURL -OutFile $destination -UseBasicParsing -ErrorAction Stop
                    }
                    Else{
                        Initialize-FileDownload -Name ("{0}" -f $Filename) -Url $Data.DownloadURL -TargetDest $destination
                    }
                    Write-Verbose ("{0} : Successfully downloaded: {1}" -f ${CmdletName},$destination)
                    $Data.Downloaded = $True
                }
                Catch {
                    Write-Error ("{0} : Failed downloading [{1}]: {2}" -f ${CmdletName},$Filter,$_.Exception.Message)
                }
            }


            ## -------- EXTRACT ----------
            $Data.Extracted = $False
            If($PSBoundParameters.ContainsKey('Extract'))
            {
                $File = Split-path $destination -Leaf
                Try{
                    Write-Verbose ("{0} : Attempting to Extract file [{1}] to [{2}]" -f ${CmdletName},$destination,$DestPath)
                    If([System.IO.Path]::GetExtension($File) -eq '.zip'){
                        Expand-Archive -LiteralPath "$destination" -DestinationPath $DestPath -Force -ErrorAction Stop
                        $Data.Extracted = $True
                    }
                    #Assume if executable and extract is used; its an extractable file
                    If([System.IO.Path]::GetExtension($File) -eq '.exe'){
                        $result = Start-Process -FilePath $destination -ArgumentList "/extract:$DestPath /quiet" -Wait -ErrorAction Stop -PassThru
                        If($result.ExitCode -eq 0){$Data.Extracted = $True}
                    }
                }
                catch {
                    Write-Error ("{0} : Unable to download [{1}]. {2}" -f ${CmdletName},$Filter,$_.Exception.Message)
                }
            }

            ## -------- REMOVE ARCHIVE ----------
            $Data.Removed = $False
            If($PSBoundParameters.ContainsKey('Cleanup') -and $Data.Extracted)
            {
                Write-Verbose ("{0} : Removing file [{0}]" -f ${CmdletName},$destination,${CmdletName})
                Remove-Item $destination -Force -ErrorAction SilentlyContinue | Out-Null
                $Data.Removed = $True
            }

            #add data to array
            $DownloadCollection += $Data
        } #end loop
    }
    End{
        If($PSBoundParameters.ContainsKey('Passthru')){
            return $DownloadCollection
        }
    }
}

$exportModuleMemberParams = @{
    Function = @(
        'Get-MsftLink'
        'Invoke-MsftLinkDownload'
    )
}

Export-ModuleMember @exportModuleMemberParams