Compare commits

...

3 Commits
v1.20 ... v1.21

Author SHA1 Message Date
Pete Batard
6160db3c66 Improve download handling for commandline
* Report the download size and handle errors
* Also improve exit code and verbosity handling
2021-08-15 13:44:38 +02:00
flx5
8b40c85d76 Add Commandline support
* Also fix a regression introduced by previous commit
* Closes #15
* Closes #24
2021-08-07 23:32:09 +02:00
Pete Batard
f70836e6c1 Split stages into separate function calls
* This is based on work done by flx5 in branch https://github.com/flx5/Fido
2021-08-06 22:39:34 +02:00
2 changed files with 515 additions and 184 deletions

641
Fido.ps1
View File

@@ -1,6 +1,7 @@
#
# Fido v1.20 - Retail Windows ISO Downloader
# Fido v1.21 - Retail Windows ISO Downloader
# Copyright © 2019-2021 Pete Batard <pete@akeo.ie>
# Command line support: Copyright © 2021 flx5
# ConvertTo-ImageSource: Copyright © 2016 Chris Carter
#
# This program is free software: you can redistribute it and/or modify
@@ -30,14 +31,32 @@ param(
[string]$Icon,
# (Optional) Name of a pipe the download URL should be sent to.
# If not provided, a browser window is opened instead.
[string]$PipeName
[string]$PipeName,
# (Optional) Specify Windows version (e.g. "Windows 10") [Toggles commandline mode]
[string]$Win,
# (Optional) Specify Windows release (e.g. "21H1") [Toggles commandline mode]
[string]$Rel,
# (Optional) Specify Windows edition (e.g. "Pro") [Toggles commandline mode]
[string]$Ed,
# (Optional) Specify Windows language [Toggles commandline mode]
[string]$Lang,
# (Optional) Specify Windows architecture [Toggles commandline mode]
[string]$Arch,
# (Optional) Only display the download URL [Toggles commandline mode]
[switch]$GetUrl = $False,
# (Optional) Increase verbosity
[switch]$Verbose = $False
)
#endregion
try {
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
} catch {}
Write-Host Please Wait...
$Cmd = $False
if ($Win -or $Rel -or $Ed -or $Lang -or $Arch -or $GetUrl) {
$Cmd = $True
}
#region Assembly Types
$code = @"
@@ -59,14 +78,17 @@ $code = @"
}
"@
if ($host.version -ge "7.0") {
Add-Type -WarningAction Ignore -IgnoreWarnings -MemberDefinition $code -Namespace Gui -UsingNamespace System.Runtime, System.IO, System.Text, System.Drawing, System.Globalization -ReferencedAssemblies System.Drawing.Common -Name Utils -ErrorAction Stop
} else {
Add-Type -MemberDefinition $code -Namespace Gui -UsingNamespace System.IO, System.Text, System.Drawing, System.Globalization -ReferencedAssemblies System.Drawing -Name Utils -ErrorAction Stop
if (!$Cmd) {
Write-Host Please Wait...
if ($host.version -ge "7.0") {
Add-Type -WarningAction Ignore -IgnoreWarnings -MemberDefinition $code -Namespace Gui -UsingNamespace System.Runtime, System.IO, System.Text, System.Drawing, System.Globalization -ReferencedAssemblies System.Drawing.Common -Name Utils -ErrorAction Stop
} else {
Add-Type -MemberDefinition $code -Namespace Gui -UsingNamespace System.IO, System.Text, System.Drawing, System.Globalization -ReferencedAssemblies System.Drawing -Name Utils -ErrorAction Stop
}
Add-Type -AssemblyName PresentationFramework
# Hide the powershell window: https://stackoverflow.com/a/27992426/1069307
[Gui.Utils]::ShowWindow(([System.Diagnostics.Process]::GetCurrentProcess() | Get-Process).MainWindowHandle, 0) | Out-Null
}
Add-Type -AssemblyName PresentationFramework
# Hide the powershell window: https://stackoverflow.com/a/27992426/1069307
[Gui.Utils]::ShowWindow(([System.Diagnostics.Process]::GetCurrentProcess() | Get-Process).MainWindowHandle, 0) | Out-Null
#endregion
#region Data
@@ -209,7 +231,7 @@ $WindowsVersions = @(
@("Windows 8.1", "windows8ISO"),
@(
"Update 3 (build 9600)",
@("Windows 8.1", 52),
@("Windows 8.1 Standard", 52),
@("Windows 8.1 N", 55)
@("Windows 8.1 Single Language", 48),
@("Windows 8.1 K", ($ko + 61)),
@@ -363,7 +385,7 @@ function Add-Entry([int]$pos, [string]$Name, [array]$Items, [string]$DisplayName
function Refresh-Control([object]$Control)
{
$Control.Dispatcher.Invoke("Render", [Windows.Input.InputEventHandler] { $Continue.UpdateLayout() }, $null, $null)
$Control.Dispatcher.Invoke("Render", [Windows.Input.InputEventHandler] { $Continue.UpdateLayout() }, $null, $null) | Out-Null
}
function Send-Message([string]$PipeName, [string]$Message)
@@ -452,11 +474,15 @@ function GetElementById([object]$Request, [string]$Id)
function Error([string]$ErrorMessage)
{
Write-Host Error: $ErrorMessage
$XMLForm.Title = $(Get-Translation("Error")) + ": " + $ErrorMessage
Refresh-Control($XMLForm)
$XMLGrid.Children[2 * $script:Stage + 1].IsEnabled = $True
$UserInput = [System.Windows.MessageBox]::Show($XMLForm.Title, $(Get-Translation("Error")), "OK", "Error")
$script:ExitCode = $script:Stage--
if (!$Cmd) {
$XMLForm.Title = $(Get-Translation("Error")) + ": " + $ErrorMessage
Refresh-Control($XMLForm)
$XMLGrid.Children[2 * $script:Stage + 1].IsEnabled = $True
$UserInput = [System.Windows.MessageBox]::Show($XMLForm.Title, $(Get-Translation("Error")), "OK", "Error")
$script:ExitCode = $script:Stage--
} else {
$script:ExitCode = 2
}
}
function Get-RandomDate()
@@ -487,20 +513,34 @@ function Get-RandomDate()
#region Globals
$ErrorActionPreference = "Stop"
$dh = 58;
$dh = 58
$Stage = 0
$SelectedIndex = 0
$ltrm = ""
if ($Cmd) {
$ltrm = ""
}
$MaxStage = 4
$SessionId = [guid]::NewGuid()
$ExitCode = 100
$Locale = "en-US"
$RequestData = @{}
# This GUID applies to all visitors, regardless of their locale
$RequestData["GetLangs"] = @("a8f8f489-4c7f-463a-9ca6-5cff94d8d041", "getskuinformationbyproductedition" )
$RequestData["GetLinks"] = @("cfa9e580-a81e-4a4b-a846-7b21bf4e2e5b", "GetProductDownloadLinksBySku" )
# This GUID applies to visitors of the en-US download page. Other locales may get a different GUID.
$RequestData["GetLinks"] = @("a224afab-2097-4dfa-a2ba-463eb191a285", "GetProductDownloadLinksBySku" )
# Create a semi-random Linux User-Agent string
$FirefoxVersion = Get-Random -Minimum 50 -Maximum 90
$FirefoxDate = Get-RandomDate
$UserAgent = "Mozilla/5.0 (X11; Linux i586; rv:$FirefoxVersion.0) Gecko/$FirefoxDate Firefox/$FirefoxVersion.0"
$Verbosity = 2
if ($Cmd) {
if ($GetUrl) {
$Verbosity = 0
} elseif (!$Verbose) {
$Verbosity = 1
}
}
#endregion
# Localization
@@ -529,6 +569,360 @@ if ($PSVersionTable.PSVersion.Major -lt 3) {
exit 102
}
# Convert a size in bytes to a human readable string
function Size-To-Human-Readable([uint64]$size)
{
$suffix = "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"
$i = 0
while ($size -gt 1kb) {
$size = $size / 1kb
$i++
}
"{0:N1} {1}" -f $size, $suffix[$i]
}
# Check if the locale we want is available - Fall back to en-US otherwise
function Check-Locale {
try {
$url = "https://www.microsoft.com/" + $QueryLocale + "/software-download/"
if ($Verbosity -ge 2) {
Write-Host Querying $url
}
Invoke-WebRequest -UseBasicParsing -MaximumRedirection 0 -UserAgent $UserAgent $url | Out-Null
} catch {
$script:QueryLocale = "en-US"
}
}
# Return an array of releases (e.g. 20H2, 21H1, ...) for the selected Windows version
function Get-Windows-Releases([int]$SelectedVersion)
{
$i = 0
$releases = @()
foreach ($version in $WindowsVersions[$SelectedVersion]) {
if (($i -ne 0) -and ($version -is [array])) {
$releases += @(New-Object PsObject -Property @{ Release = $ltrm + $version[0].Replace(")", ")" + $ltrm); Index = $i })
}
$i++
}
return $releases
}
# Return an array of editions (e.g. Home, Pro, etc) for the selected Windows release
function Get-Windows-Editions([int]$SelectedVersion, [int]$SelectedRelease)
{
$editions = @()
foreach ($release in $WindowsVersions[$SelectedVersion][$SelectedRelease])
{
if ($release -is [array]) {
if (($release[1] -lt 0x10000) -or ($Locale.StartsWith("ko") -and ($release[1] -band $ko)) -or ($Locale.StartsWith("zh") -and ($release[1] -band $zh))) {
$editions += @(New-Object PsObject -Property @{ Edition = $release[0]; Id = $($release[1] -band 0xFFFF) })
}
}
}
return $editions
}
# Return an array of languages for the selected edition
function Get-Windows-Languages([int]$SelectedVersion, [int]$SelectedEdition)
{
$languages = @()
$i = 0;
if ($WindowsVersions[$SelectedVersion][0][1] -eq "WIN7") {
foreach ($entry in $Windows7Versions[$SelectedEdition]) {
if ($entry[0] -ne "") {
$languages += @(New-Object PsObject -Property @{ DisplayLanguage = $entry[0]; Language = $entry[1]; Id = $i })
}
$i++
}
} else {
$url = "https://www.microsoft.com/" + $QueryLocale + "/api/controls/contentinclude/html"
$url += "?pageId=" + $RequestData["GetLangs"][0]
$url += "&host=www.microsoft.com"
$url += "&segments=software-download," + $WindowsVersions[$SelectedVersion][0][1]
$url += "&query=&action=" + $RequestData["GetLangs"][1]
$url += "&sessionId=" + $SessionId
$url += "&productEditionId=" + [Math]::Abs($SelectedEdition)
$url += "&sdVersion=2"
if ($Verbosity -ge 2) {
Write-Host Querying $url
}
$script:SelectedIndex = 0
try {
$r = Invoke-WebRequest -UseBasicParsing -UserAgent $UserAgent -SessionVariable "Session" $url
if ($r -match "errorModalMessage") {
Throw-Error -Req $r -Alt "Could not retrieve languages from server"
}
$pattern = '(?s)<select id="product-languages">(.*)?</select>'
$html = [regex]::Match($r, $pattern).Groups[1].Value
# Go through an XML conversion to keep all PowerShells happy...
$html = $html.Replace("selected value", "value")
$html = "<options>" + $html + "</options>"
$xml = [xml]$html
foreach ($var in $xml.options.option) {
$json = $var.value | ConvertFrom-Json;
if ($json) {
$languages += @(New-Object PsObject -Property @{ DisplayLanguage = $var.InnerText; Language = $json.language; Id = $json.id })
if (Select-Language($json.language)) {
$script:SelectedIndex = $i
}
$i++
}
}
if ($languages.Length -eq 0) {
Throw-Error -Req $r -Alt "Could not parse languages"
}
} catch {
Error($_.Exception.Message)
return @()
}
}
return $languages
}
# Return an array of download links for each supported arch
function Get-Windows-Download-Links([int]$SelectedVersion, [int]$SelectedEdition, [int]$SkuId, [string]$LanguageName)
{
$links = @()
if ($WindowsVersions[$SelectedVersion][0][1] -eq "WIN7") {
foreach ($Version in $Windows7Versions[$SelectedEdition][$SkuId][2]) {
$links += @(New-Object PsObject -Property @{ Type = $Version[0]; Link = $Version[1] })
}
} else {
$url = "https://www.microsoft.com/" + $QueryLocale + "/api/controls/contentinclude/html"
$url += "?pageId=" + $RequestData["GetLinks"][0]
$url += "&host=www.microsoft.com"
$url += "&segments=software-download," + $WindowsVersions[$SelectedVersion][0][1]
$url += "&query=&action=" + $RequestData["GetLinks"][1]
$url += "&sessionId=" + $SessionId
$url += "&skuId=" + $SkuId
$url += "&language=" + $LanguageName
$url += "&sdVersion=2"
if ($Verbosity -ge 2) {
Write-Host Querying $url
}
$i = 0
$script:SelectedIndex = 0
try {
$Is64 = [Environment]::Is64BitOperatingSystem
$r = Invoke-WebRequest -Method Post -UseBasicParsing -UserAgent $UserAgent -WebSession $Session $url
if ($r -match "errorModalMessage") {
Throw-Error -Req $r -Alt "Could not retrieve architectures from server"
}
$pattern = '(?s)(<input.*?></input>)'
ForEach-Object { [regex]::Matches($r, $pattern) } | ForEach-Object { $html += $_.Groups[1].value }
# Need to fix the HTML and JSON data so that it is well-formed
$html = $html.Replace("class=product-download-hidden", "")
$html = $html.Replace("type=hidden", "")
$html = $html.Replace("&nbsp;", " ")
$html = $html.Replace("IsoX86", "&quot;x86&quot;")
$html = $html.Replace("IsoX64", "&quot;x64&quot;")
$html = "<inputs>" + $html + "</inputs>"
$xml = [xml]$html
foreach ($var in $xml.inputs.input) {
$json = $var.value | ConvertFrom-Json;
if ($json) {
if (($Is64 -and $json.DownloadType -eq "x64") -or (-not $Is64 -and $json.DownloadType -eq "x86")) {
$script:SelectedIndex = $i
}
$links += @(New-Object PsObject -Property @{ Type = $json.DownloadType; Link = $json.Uri })
$i++
}
}
if ($links.Length -eq 0) {
Throw-Error -Req $r -Alt "Could not retrieve ISO download links"
}
} catch {
Error($_.Exception.Message)
return @()
}
}
return $links
}
# Process the download URL by either sending it through the pipe or by opening the browser
function Process-Download-Link([string]$Url)
{
try {
if ($PipeName -and -not $Check.IsChecked) {
Send-Message -PipeName $PipeName -Message $Url
} else {
if ($Cmd) {
$pattern = '.*\/(.*\.iso).*'
$File = [regex]::Match($Url, $pattern).Groups[1].Value
$Size = Size-To-Human-Readable (Invoke-WebRequest -UseBasicParsing -Uri $Url -Method Head).Headers.'Content-Length'
Write-Host "Downloading '$File' ($Size)..."
Invoke-WebRequest -UseBasicParsing -Uri $Url -OutFile $File
} else {
Write-Host Download Link: $Url
Start-Process -FilePath $Url
}
}
} catch {
Error($_.Exception.Message)
return 404
}
return 0
}
if ($Cmd) {
$winVersionId = $null
$winReleaseId = $null
$winEditionId = $null
$winLanguageId = $null
$winLanguageName = $null
$winLink = $null
$i = 0
$Selected = ""
if ($Win -eq "List") {
Write-Host "Please select a Windows Version (-Win):"
}
foreach($version in $WindowsVersions) {
if ($Win -eq "List") {
Write-Host " -" $version[0][0]
} elseif ($version[0][0] -match $Win) {
$Selected += $version[0][0]
$winVersionId = $i
break;
}
$i++
}
if ($winVersionId -eq $null) {
if ($Win -ne "List") {
Write-Host "Invalid Windows version provided."
Write-Host "Use '-Win List' for a list of available Windows versions."
}
exit 1
}
# Windows Version selection
$releases = Get-Windows-Releases $winVersionId
if ($Rel -eq "List") {
Write-Host "Please select a Windows Release (-Rel) for ${Selected} (or use 'Latest' for most recent):"
}
foreach ($release in $releases) {
if ($Rel -eq "List") {
Write-Host " -" $release.Release
} elseif (!$Rel -or $release.Release.StartsWith($Rel) -or $Rel -eq "Latest") {
if (!$Rel -and $Verbosity -ge 1) {
Write-Host "No release specified (-Rel). Defaulting to '$($release.Release)'."
}
$Selected += " " + $release.Release
$winReleaseId = $release.Index
break;
}
}
if ($winReleaseId -eq $null) {
if ($Rel -ne "List") {
Write-Host "Invalid Windows release provided."
Write-Host "Use '-Rel List' for a list of available $Selected releases or '-Rel Latest' for latest."
}
exit 1
}
# Windows Release selection => Populate Product Edition
$editions = Get-Windows-Editions $winVersionId $winReleaseId
if ($Ed -eq "List") {
Write-Host "Please select a Windows Edition (-Ed) for ${Selected}:"
}
foreach($edition in $editions) {
if ($Ed -eq "List") {
Write-Host " -" $edition.Edition
} elseif (!$Ed -or $edition.Edition -match $Ed) {
if (!$Ed -and $Verbosity -ge 1) {
Write-Host "No edition specified (-Ed). Defaulting to '$($edition.Edition)'."
}
$Selected += "," + $edition.Edition -replace "Windows [0-9\.]*", ""
$winEditionId = $edition.Id
break;
}
}
if ($winEditionId -eq $null) {
if ($Ed -ne "List") {
Write-Host "Invalid Windows edition provided."
Write-Host "Use '-Ed List' for a list of available editions or remove the -Ed parameter to use default."
}
exit 1
}
# Product Edition selection => Request and populate Languages
$languages = Get-Windows-Languages $winVersionId $winEditionId
if (!$languages) {
exit 3
}
if ($Lang -eq "List") {
Write-Host "Please select a Language (-Lang) for ${Selected}:"
}
$i = 0
foreach ($language in $languages) {
if ($Lang -eq "List") {
Write-Host " -" $language.Language
} elseif ((!$Lang -and $script:SelectedIndex -eq $i) -or ($Lang -and $language.Language -match $Lang)) {
if (!$Lang -and $Verbosity -ge 1) {
Write-Host "No language specified (-Lang). Defaulting to '$($language.Language)'."
}
$Selected += ", " + $language.Language
$winLanguageId = $language.Id
$winLanguageName = $language.Language
break;
}
$i++
}
if ($winLanguageId -eq $null -or $winLanguageName -eq $null) {
if ($Lang -ne "List") {
Write-Host "Invalid Windows language provided."
Write-Host "Use '-Lang List' for a list of available languages or remove the option to use system default."
}
exit 1
}
# Language selection => Request and populate Arch download links
$links = Get-Windows-Download-Links $winVersionId $winEditionId $winLanguageId $winLanguageName
if (!$links) {
exit 3
}
if ($Arch -eq "List") {
Write-Host "Please select an Architecture (-Arch) for ${Selected}:"
}
$i = 0
foreach ($link in $links) {
if ($Arch -eq "List") {
Write-Host " -" $link.Type
} elseif ((!$Arch -and $script:SelectedIndex -eq $i) -or ($Arch -and $link.Type -match $Arch)) {
if (!$Arch -and $Verbosity -ge 1) {
Write-Host "No architecture specified (-Arch). Defaulting to '$($link.Type)'."
}
$Selected += ", " + $link.Type
$winLink = $link
break;
}
$i++
}
if ($winLink -eq $null) {
if ($Arch -ne "List") {
Write-Host "Invalid Windows architecture provided."
Write-Host "Use '-Arch List' for a list of available architectures or remove the option to use system default."
}
exit 1
}
# Arch selection => Return selected download link
if ($GetUrl) {
Write-Host $winLink.Link
$ExitCode = 0
} else {
Write-Host "Selected: $Selected"
$ExitCode = Process-Download-Link $winLink.Link
}
# Clean up & exit
exit $ExitCode
}
# Form creation
$XMLForm = [Windows.Markup.XamlReader]::Load((New-Object System.Xml.XmlNodeReader $XAML))
$XAML.SelectNodes("//*[@Name]") | ForEach-Object { Set-Variable -Name ($_.Name) -Value $XMLForm.FindName($_.Name) -Scope Script }
@@ -538,7 +932,7 @@ if ($Icon) {
} else {
$XMLForm.Icon = [Gui.Utils]::ExtractIcon("shell32.dll", -41, $true) | ConvertTo-ImageSource
}
if ($Locale.StartsWith("ar") -or $Locale.StartsWith("fa") -or $Locale.StartsWith("he")) {
if ($Locale.StartsWith("ar") -or $Locale.StartsWith("fa") -or $Locale.StartsWith("he")) {
$XMLForm.FlowDirection = "RightToLeft"
}
$WindowsVersionTitle.Text = Get-Translation("Version")
@@ -547,12 +941,12 @@ $Back.Content = Get-Translation("Close")
# Populate the Windows versions
$i = 0
$array = @()
foreach($Version in $WindowsVersions) {
$array += @(New-Object PsObject -Property @{ Version = $Version[0][0]; PageType = $Version[0][1]; Index = $i })
$versions = @()
foreach($version in $WindowsVersions) {
$versions += @(New-Object PsObject -Property @{ Version = $version[0][0]; PageType = $version[0][1]; Index = $i })
$i++
}
$WindowsVersion.ItemsSource = $array
$WindowsVersion.ItemsSource = $versions
$WindowsVersion.DisplayMemberPath = "Version"
# Button Action
@@ -569,153 +963,38 @@ $Continue.add_click({
1 { # Windows Version selection
$XMLForm.Title = Get-Translation($English[12])
Refresh-Control($XMLForm)
# Check if the locale we want is available - Fall back to en-US otherwise
try {
$url = "https://www.microsoft.com/" + $QueryLocale + "/software-download/"
Write-Host Querying $url
Invoke-WebRequest -UseBasicParsing -MaximumRedirection 0 -UserAgent $UserAgent $url | Out-Null
} catch {
$script:QueryLocale = "en-US"
}
$i = 0
$array = @()
foreach ($Version in $WindowsVersions[$WindowsVersion.SelectedValue.Index]) {
if (($i -ne 0) -and ($Version -is [array])) {
$array += @(New-Object PsObject -Property @{ Release = $ltrm + $Version[0].Replace(")", ")" + $ltrm); Index = $i })
}
$i++
}
$script:WindowsRelease = Add-Entry $Stage "Release" $array
Check-Locale
$releases = Get-Windows-Releases $WindowsVersion.SelectedValue.Index
$script:WindowsRelease = Add-Entry $Stage "Release" $releases
$Back.Content = Get-Translation($English[8])
$XMLForm.Title = $AppTitle
}
2 { # Windows Release selection => Populate Product Edition
$array = @()
foreach ($Release in $WindowsVersions[$WindowsVersion.SelectedValue.Index][$WindowsRelease.SelectedValue.Index])
{
if ($Release -is [array]) {
if (($Release[1] -lt 0x10000) -or ($Locale.StartsWith("ko") -and ($Release[1] -band $ko)) -or ($Locale.StartsWith("zh") -and ($Release[1] -band $zh))) {
$array += @(New-Object PsObject -Property @{ Edition = $Release[0]; Id = $($Release[1] -band 0xFFFF) })
}
}
}
$script:ProductEdition = Add-Entry $Stage "Edition" $array
$editions = Get-Windows-Editions $WindowsVersion.SelectedValue.Index $WindowsRelease.SelectedValue.Index
$script:ProductEdition = Add-Entry $Stage "Edition" $editions
}
3 { # Product Edition selection => Request and populate Languages
3 { # Product Edition selection => Request and populate languages
$XMLForm.Title = Get-Translation($English[12])
Refresh-Control($XMLForm)
$array = @()
$i = 0;
if ($WindowsVersion.SelectedValue.PageType -eq "WIN7") {
foreach ($Entry in $Windows7Versions[$ProductEdition.SelectedValue.Id]) {
if ($Entry[0] -ne "") {
$array += @(New-Object PsObject -Property @{ DisplayLanguage = $Entry[0]; Language = $Entry[1]; Id = $i })
}
$i++
}
} else {
$url = "https://www.microsoft.com/" + $QueryLocale + "/api/controls/contentinclude/html"
$url += "?pageId=" + $RequestData["GetLangs"][0]
$url += "&host=www.microsoft.com"
$url += "&segments=software-download," + $WindowsVersion.SelectedValue.PageType
$url += "&query=&action=" + $RequestData["GetLangs"][1]
$url += "&sessionId=" + $SessionId
$url += "&productEditionId=" + [Math]::Abs($ProductEdition.SelectedValue.Id)
$url += "&sdVersion=2"
Write-Host Querying $url
$SelectedIndex = 0
try {
$r = Invoke-WebRequest -UseBasicParsing -UserAgent $UserAgent -SessionVariable "Session" $url
$pattern = '(?s)<select id="product-languages">(.*)?</select>'
$html = [regex]::Match($r, $pattern).Groups[1].Value
# Go through an XML conversion to keep all PowerShells happy...
$html = $html.Replace("selected value", "value")
$html = "<options>" + $html + "</options>"
$xml = [xml]$html
foreach ($var in $xml.options.option) {
$json = $var.value | ConvertFrom-Json;
if ($json) {
$array += @(New-Object PsObject -Property @{ DisplayLanguage = $var.InnerText; Language = $json.language; Id = $json.id })
if (Select-Language($json.language)) {
$SelectedIndex = $i
}
$i++
}
}
if ($array.Length -eq 0) {
Throw-Error -Req $r -Alt "Could not parse languages"
}
} catch {
Error($_.Exception.Message)
break
}
$languages = Get-Windows-Languages $WindowsVersion.SelectedValue.Index $ProductEdition.SelectedValue.Id
if ($languages.Length -eq 0) {
break
}
$script:Language = Add-Entry $Stage "Language" $array "DisplayLanguage"
$Language.SelectedIndex = $SelectedIndex
$script:Language = Add-Entry $Stage "Language" $languages "DisplayLanguage"
$Language.SelectedIndex = $script:SelectedIndex
$XMLForm.Title = $AppTitle
}
4 { # Language selection => Request and populate Arch download links
$array = @()
if ($WindowsVersion.SelectedValue.PageType -eq "WIN7") {
foreach ($Version in $Windows7Versions[$ProductEdition.SelectedValue.Id][$Language.SelectedValue.Id][2]) {
$array += @(New-Object PsObject -Property @{ Type = $Version[0]; Link = $Version[1] })
}
} else {
$XMLForm.Title = Get-Translation($English[12])
Refresh-Control($XMLForm)
$url = "https://www.microsoft.com/" + $QueryLocale + "/api/controls/contentinclude/html"
$url += "?pageId=" + $RequestData["GetLinks"][0]
$url += "&host=www.microsoft.com"
$url += "&segments=software-download," + $WindowsVersion.SelectedValue.PageType
$url += "&query=&action=" + $RequestData["GetLinks"][1]
$url += "&sessionId=" + $SessionId
$url += "&skuId=" + $Language.SelectedValue.Id
$url += "&language=" + $Language.SelectedValue.Language
$url += "&sdVersion=2"
Write-Host Querying $url
$i = 0
$SelectedIndex = 0
try {
$Is64 = [Environment]::Is64BitOperatingSystem
$r = Invoke-WebRequest -UseBasicParsing -UserAgent $UserAgent -WebSession $Session $url
$pattern = '(?s)(<input.*?/>)'
ForEach-Object { [regex]::Matches($r, $pattern) } | ForEach-Object { $html += $_.Groups[1].value }
# Need to fix the HTML and JSON data so that it is well-formed
$html = $html.Replace("class=product-download-hidden", "")
$html = $html.Replace("type=hidden", "")
$html = $html.Replace("&nbsp;", " ")
$html = $html.Replace("IsoX86", "&quot;x86&quot;")
$html = $html.Replace("IsoX64", "&quot;x64&quot;")
$html = "<inputs>" + $html + "</inputs>"
$xml = [xml]$html
foreach ($var in $xml.inputs.input) {
$json = $var.value | ConvertFrom-Json;
if ($json) {
if (($Is64 -and $json.DownloadType -eq "x64") -or (-not $Is64 -and $json.DownloadType -eq "x86")) {
$SelectedIndex = $i
}
$array += @(New-Object PsObject -Property @{ Type = $json.DownloadType; Link = $json.Uri })
$i++
}
}
if ($array.Length -eq 0) {
Throw-Error -Req $r -Alt "Could not retrieve ISO download links"
}
} catch {
Error($_.Exception.Message)
break
}
$XMLForm.Title = Get-Translation($English[12])
Refresh-Control($XMLForm)
$links = Get-Windows-Download-Links $WindowsVersion.SelectedValue.Index $ProductEdition.SelectedValue.Id $Language.SelectedValue.Id $Language.SelectedValue.Language
if ($links.Length -eq 0) {
break
}
$script:Arch = Add-Entry $Stage "Architecture" $array "Type"
$script:Architecture = Add-Entry $Stage "Architecture" $links "Type"
if ($PipeName) {
$XMLForm.Height += $dh / 2;
$Margin = $Continue.Margin
@@ -731,19 +1010,13 @@ $Continue.add_click({
$Check.Content = Get-Translation($English[13])
$Check.Visibility = "Visible"
}
$Arch.SelectedIndex = $SelectedIndex
$Architecture.SelectedIndex = $script:SelectedIndex
$Continue.Content = Get-Translation("Download")
$XMLForm.Title = $AppTitle
}
5 { # Arch selection => Return selected download link
if ($PipeName -and -not $Check.IsChecked) {
Send-Message -PipeName $PipeName -Message $Arch.SelectedValue.Link
} else {
Write-Host Download Link: $Arch.SelectedValue.Link
Start-Process -FilePath $Arch.SelectedValue.Link
}
$script:ExitCode = 0
$script:ExitCode = Process-Download-Link $Architecture.SelectedValue.Link
$XMLForm.Close()
}
}
@@ -793,8 +1066,8 @@ exit $ExitCode
# SIG # Begin signature block
# MIIcQgYJKoZIhvcNAQcCoIIcMzCCHC8CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCBGq2C5xqP9tvN9
# 4PFRzj/VzRRF7efttyTaScJEztjIcqCCCy4wggVGMIIELqADAgECAhAkaSZj72wM
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCC8WEqsYMEmJywK
# KfrYLzANdf83uJiw/hHlMhc0O5DSUqCCCy4wggVGMIIELqADAgECAhAkaSZj72wM
# Cjsjz6MQw2SbMA0GCSqGSIb3DQEBCwUAMH0xCzAJBgNVBAYTAkdCMRswGQYDVQQI
# ExJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcTB1NhbGZvcmQxGjAYBgNVBAoT
# EUNPTU9ETyBDQSBMaW1pdGVkMSMwIQYDVQQDExpDT01PRE8gUlNBIENvZGUgU2ln
@@ -859,17 +1132,17 @@ exit $ExitCode
# T01PRE8gQ0EgTGltaXRlZDEjMCEGA1UEAxMaQ09NT0RPIFJTQSBDb2RlIFNpZ25p
# bmcgQ0ECECRpJmPvbAwKOyPPoxDDZJswDQYJYIZIAWUDBAIBBQCgfDAQBgorBgEE
# AYI3AgEMMQIwADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3
# AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgqimOQTk8tVwof8f5
# pvZptCV+HNyosJr2EE/a/9GARhswDQYJKoZIhvcNAQEBBQAEggEAIMwr8aeqBaAD
# foTc7RuwRV56vXiOiWpMG82NQjyUsKcfwFabVsjZMvt3iRAbdjnFk3eAZ+RIqasB
# 6M58zOp/0X19870BhU3nPlXbkmIljLQQrtZLBuhAtOgmJXeypWHv4ZqGyoqZ4YG3
# +2wevR6C+5S/0B47CoMQUQ1v1y1S76IJDRvJ8sW7LtEmkgCIpQI5QQ1x7pRg+jX3
# B8adK/RdrLVtmeYjlc1jSJhK86HaWmXuF9DFyH/ncySXZQ4lERvet6RDCaTOT5K1
# YNdSScdSQr5ujG/lhNAzYzdjMrB6XiISmHi8mtTKjrTCKzMpW7uA1VUrq1P7pn8s
# QP/YyRKYv6GCDiswgg4nBgorBgEEAYI3AwMBMYIOFzCCDhMGCSqGSIb3DQEHAqCC
# AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgZj12my5nRF4uJIAK
# 2pQ8Di2zHfbup/2fmO8hFbjQF04wDQYJKoZIhvcNAQEBBQAEggEANiCQPKwAy5Is
# UgY7UngWmcRf7dAUfxnzwUFY7YS8vsTJ1ZhIITiemvZL2h55AmZtDvesU2tKr7oX
# yBf7c7FcZeP261IVwDZk44Wt9dF4SE+rfg8U1TyTRlyAwCiryXs7rMt6+PzYH4+p
# ZWpviEltpTG4OfCWNFUbXqTJspdM2+lcT8u+BNVR1Xd+2yaGS+/LDS/bkgnL+bJk
# /uRau5Rc5JQ2S4seZnvSvyhQFLx+drJohKM16QJIWZtU5JpK3yBfkpiS9RyFOurp
# eY1o8S6KeuMvvBAHUDqW4+tCTNKiEd4uDGngmzclpGJ55mdWTctRpLJRkrvhbemh
# /mv35kpEyKGCDiswgg4nBgorBgEEAYI3AwMBMYIOFzCCDhMGCSqGSIb3DQEHAqCC
# DgQwgg4AAgEDMQ0wCwYJYIZIAWUDBAIBMIH+BgsqhkiG9w0BCRABBKCB7gSB6zCB
# 6AIBAQYLYIZIAYb4RQEHFwMwITAJBgUrDgMCGgUABBQek/WA68bHr7eCxmX0T/Vd
# ee/SxgIUb0aeoa2p/9QaO1zxbhQfZ1pWBMkYDzIwMjEwNjEwMTAzODU5WjADAgEe
# 6AIBAQYLYIZIAYb4RQEHFwMwITAJBgUrDgMCGgUABBTo94oXALODEFYBtPH+MW9y
# fRlOhwIUUI4EgP1+UyBfXDsSgO4V15w2drgYDzIwMjEwODE1MTE0NDIwWjADAgEe
# oIGGpIGDMIGAMQswCQYDVQQGEwJVUzEdMBsGA1UEChMUU3ltYW50ZWMgQ29ycG9y
# YXRpb24xHzAdBgNVBAsTFlN5bWFudGVjIFRydXN0IE5ldHdvcmsxMTAvBgNVBAMT
# KFN5bWFudGVjIFNIQTI1NiBUaW1lU3RhbXBpbmcgU2lnbmVyIC0gRzOgggqLMIIF
@@ -933,13 +1206,13 @@ exit $ExitCode
# U3ltYW50ZWMgQ29ycG9yYXRpb24xHzAdBgNVBAsTFlN5bWFudGVjIFRydXN0IE5l
# dHdvcmsxKDAmBgNVBAMTH1N5bWFudGVjIFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EC
# EHvU5a+6zAc/oQEjBCJBTRIwCwYJYIZIAWUDBAIBoIGkMBoGCSqGSIb3DQEJAzEN
# BgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjEwNjEwMTAzODU5WjAvBgkq
# hkiG9w0BCQQxIgQgmkzzCEFINNxyi0VgUg+Xe7gn6JtaRlUItfaDirYqplswNwYL
# BgsqhkiG9w0BCRABBDAcBgkqhkiG9w0BCQUxDxcNMjEwODE1MTE0NDIwWjAvBgkq
# hkiG9w0BCQQxIgQgR0FR9IWPt5STvFFuudk1GbFPg5Fqf4oX0H0kTUPlyuIwNwYL
# KoZIhvcNAQkQAi8xKDAmMCQwIgQgxHTOdgB9AjlODaXk3nwUxoD54oIBPP72U+9d
# tx/fYfgwCwYJKoZIhvcNAQEBBIIBACpaTpSIzaaiV6meU/x2sTikIngCFWQsY+Pf
# DwhgEX7h36q5O92NtHjrUgGILvPSjtZ27Mpxbm84HCwPUn+VJcDBh4MbD8GDV+X4
# dKx/77zLUiuWlhub1Jz2wDRd+ac0GV1get6dBCAKVomtGqbg5he0p0WvgSosmhXi
# dVr7i59KIaP/J1e2DBDqJPsVFUpCSaaDLfqBR+0hV4sTczRLDrE8k8cTGNzcNhH8
# WzqLCEw0V40Tc5KZ1xghdxIYH3V7SJOIYTKopuN6G/hMnR+hABU578iPjhAQjbel
# Z0EkBvJk8bwoMBGEf3OnB8S7xJRDsbgYLubnItXJ6z5pLcNJRbc=
# tx/fYfgwCwYJKoZIhvcNAQEBBIIBADVjsVBj2pNLRuiYXP/7w+VOkD8l6/d9uM0b
# 9de0r8JxysW+uPpwtjoQd4uRdVklWBdF1IYRSMgEvXEe08tENDghsn96rtJtsueX
# bTUCa9cQeYC1Qwb6DPcpFwYr6ROxXmyLnZT07hNGtZfY0ldww5Oai2qzWRgZnPQL
# gvnBiX1f9CnmFhcM6RVnhAnUkpRmd+sgXMF15vM3sql+R99ou5C+Jfmh9YVWHZk+
# CvOABIeNLf4Ny58etDYlfBilaDluDgatl801zLaRq9kxTwAYlMEqlJ5NfdUdXLWZ
# dDik6XJiPLZFvdOPkwwbqX49JVJqqaRZSV0bx70X1y5MBExoBAg=
# SIG # End signature block

View File

@@ -65,6 +65,64 @@ of Windows 7).
Note that the current version of the script does not need Internet Explorer to be installed and should also work with
PowerShell 7.
Commandline mode
----------------
Fido supports commandline mode whereas, whenever one of the following options is provided, a GUI is not instantiated
and you can instead generate the ISO download from within a PowerShell console or script.
The options are:
- `Win`: Specify Windows version (e.g. _"Windows 10"_). Abbreviated version should work as well (e.g `-Win 10`) as long
as it is unique enough. If this option isn't specified, the most recent version of Windows is automatically selected.
You can obtain a list of supported versions by specifying `-Win List`.
- `Rel`: Specify Windows release (e.g. _"21H1"_). If this option isn't specified, the most recent release for the chosen
version of Windows is automatically selected. You can also use `-Rel Latest` to force the most recent to be used.
You can obtain a list of supported versions by specifying `-Rel List`.
- `Ed`: Specify Windows edition (e.g. _"Pro/Home"_). Abbreviated editions should work as well (e.g `-Ed Pro`) as long
as it is unique enough. If this option isn't specified, the most recent version of Windows is automatically selected.
You can obtain a list of supported versions by specifying `-Ed List`.
- `Lang`: Specify Windows language (e.g. _"Arabic"_). Abbreviated or part of a language (e.g. `-Lang Int` for
`English International`) should work as long as it's unique enough. If this option isn't specified, the script attempts
to select the same language as the system locale.
You can obtain a list of supported languages by specifying `-Lang List`.
- `Arch`: Specify Windows architecture (e.g. _"x64"_). If this option isn't specified, the script attempts to use the same
architecture as the one from the current system.
- `GetUrl`: By default, the script attempts to automatically launch the download. But when using the `-GetUrl` switch,
the script only displays the download URL, which can then be piped into another command or into a file.
Examples of a commandline download:
```
PS C:\Projects\Fido> .\Fido.ps1 -Win 10
No release specified (-Rel). Defaulting to '21H1 (Build 19043.985 - 2021.05)'.
No edition specified (-Ed). Defaulting to 'Windows 10 Home/Pro'.
No language specified (-Lang). Defaulting to 'English International'.
No architecture specified (-Arch). Defaulting to 'x64'.
Selected: Windows 10 21H1 (Build 19043.985 - 2021.05), Home/Pro, English International, x64
Downloading 'Win10_21H1_EnglishInternational_x64.iso' (5.0 GB)...
PS C:\Projects\Fido> .\Fido.ps1 -Win 10 -Rel List
Please select a Windows Release (-Rel) for Windows 10 (or use 'Latest' for most recent):
- 21H1 (Build 19043.985 - 2021.05)
- 20H2 (Build 19042.631 - 2020.12)
- 20H2 (Build 19042.508 - 2020.10)
- 20H1 (Build 19041.264 - 2020.05)
- 19H2 (Build 18363.418 - 2019.11)
- 19H1 (Build 18362.356 - 2019.09)
- 19H1 (Build 18362.30 - 2019.05)
- 1809 R2 (Build 17763.107 - 2018.10)
- 1809 R1 (Build 17763.1 - 2018.09)
- 1803 (Build 17134.1 - 2018.04)
- 1709 (Build 16299.15 - 2017.09)
- 1703 [Redstone 2] (Build 15063.0 - 2017.03)
- 1607 [Redstone 1] (Build 14393.0 - 2016.07)
- 1511 R3 [Threshold 2] (Build 10586.164 - 2016.04)
- 1511 R2 [Threshold 2] (Build 10586.104 - 2016.02)
- 1511 R1 [Threshold 2] (Build 10586.0 - 2015.11)
- 1507 [Threshold 1] (Build 10240.16384 - 2015.07)
PS C:\Projects\Fido> .\Fido.ps1 -Win 10 -Rel 20H2 -Ed Edu -Lang Fre -Arch x86 -GetUrl
https://software-download.microsoft.com/db/Win10_Edu_20H2_v2_French_x32.iso?t=c48b32d3-4cf3-46f3-a8ad-6dd9568ff4eb&e=1629113408&h=659cdd60399584c5dc1d267957924fbd
```
Additional Notes
----------------