Przeglądaj źródła

feat: Multi-language support (#514)

Kroese 1 rok temu
rodzic
commit
2c8549b594

+ 1 - 0
assets/winvistax64-enterprise.xml

@@ -119,6 +119,7 @@
   <settings pass="oobeSystem">
     <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
       <InputLocale>0409:00000409</InputLocale>
+      <SystemLocale>en-US</SystemLocale>
       <UILanguage>en-US</UILanguage>
       <UserLocale>en-US</UserLocale>
     </component>

+ 1 - 0
assets/winvistax64-ultimate.xml

@@ -119,6 +119,7 @@
   <settings pass="oobeSystem">
     <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
       <InputLocale>0409:00000409</InputLocale>
+      <SystemLocale>en-US</SystemLocale>
       <UILanguage>en-US</UILanguage>
       <UserLocale>en-US</UserLocale>
     </component>

+ 1 - 0
assets/winvistax64.xml

@@ -119,6 +119,7 @@
   <settings pass="oobeSystem">
     <component name="Microsoft-Windows-International-Core" processorArchitecture="amd64" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
       <InputLocale>0409:00000409</InputLocale>
+      <SystemLocale>en-US</SystemLocale>
       <UILanguage>en-US</UILanguage>
       <UserLocale>en-US</UserLocale>
     </component>

+ 1 - 0
assets/winvistax86-enterprise.xml

@@ -119,6 +119,7 @@
   <settings pass="oobeSystem">
     <component name="Microsoft-Windows-International-Core" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
       <InputLocale>0409:00000409</InputLocale>
+      <SystemLocale>en-US</SystemLocale>
       <UILanguage>en-US</UILanguage>
       <UserLocale>en-US</UserLocale>
     </component>

+ 1 - 0
assets/winvistax86-ultimate.xml

@@ -119,6 +119,7 @@
   <settings pass="oobeSystem">
     <component name="Microsoft-Windows-International-Core" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
       <InputLocale>0409:00000409</InputLocale>
+      <SystemLocale>en-US</SystemLocale>
       <UILanguage>en-US</UILanguage>
       <UserLocale>en-US</UserLocale>
     </component>

+ 1 - 0
assets/winvistax86.xml

@@ -119,6 +119,7 @@
   <settings pass="oobeSystem">
     <component name="Microsoft-Windows-International-Core" processorArchitecture="x86" publicKeyToken="31bf3856ad364e35" language="neutral" versionScope="nonSxS">
       <InputLocale>0409:00000409</InputLocale>
+      <SystemLocale>en-US</SystemLocale>
       <UILanguage>en-US</UILanguage>
       <UserLocale>en-US</UserLocale>
     </component>

+ 25 - 1
readme.md

@@ -15,6 +15,7 @@ Windows in a Docker container.
 
 ## Features
 
+ - Multi-language
  - ISO downloader
  - KVM acceleration
  - Web-based viewer
@@ -107,6 +108,19 @@ kubectl apply -f kubernetes.yml
 
   To install ARM64 versions of Windows use [dockur/windows-arm](https://github.com/dockur/windows-arm/).
 
+* ### How do I select the Windows language?
+
+  By default, the English version of Windows will be downloaded. But you can add the `LANGUAGE` environment variable to your compose file, in order to specify an alternative language:
+
+  ```yaml
+  environment:
+    LANGUAGE: "Chinese"
+  ```
+
+  You can choose between `Arabic`, `Bulgarian`, `Chinese`, `Croatian`, `Czech`, `Danish`, `Dutch`, `Estonian`, `Finnish`, `French`, `German`, `Greek`, `Hebrew`, `Hungarian`, `Italian`, `Japanese`, `Korean`, `Latvian`, `Lithuanian`, `Norwegian`, `Polish`, `Portuguese`, `Romanian`, `Russian`, `Serbian`, `Slovak`, `Slovenian`, `Spanish`, `Swedish`, `Turkish`, `Thai` and `Ukrainian`.
+
+  If you want to use a keyboard layout or region/locale that is not the default for the selected language, you can add the `KEYBOARD` and `REGION` variables with a culture code, like `en-US`.
+  
 * ### How do I change the storage location?
 
   To change the storage location, include the following bind mount in your compose file:
@@ -242,11 +256,21 @@ kubectl apply -f kubernetes.yml
     CPU_CORES: "4"
   ```
 
+* ### How do I configure the username and password?
+
+  By default, a user called `Docker` is created during installation with an empty password. You can change these credentials in your compose file:
+
+  ```yaml
+  environment:
+    USERNAME: "john"
+    PASSWORD: "secret"
+  ```
+
 * ### How do I connect using RDP?
 
   The web-viewer is mainly meant to be used during installation, as its picture quality is low, and it has no audio or clipboard for example.
 
-  So for a better experience you can connect using any Microsoft Remote Desktop client to the IP of the container, using the username `docker` and by leaving the password empty.
+  So for a better experience you can connect using any Microsoft Remote Desktop client to the IP of the container, using the username `Docker` and by leaving the password empty.
 
   There is a good RDP client for [Android](https://play.google.com/store/apps/details?id=com.microsoft.rdc.androidx) available from the Play Store and one for [iOS](https://apps.apple.com/nl/app/microsoft-remote-desktop/id714464092?l=en-GB) in the Apple Store. For Linux you can use [FreeRDP](https://www.freerdp.com/) and on Windows just type `mstsc` in the search box.
 

Plik diff jest za duży
+ 1108 - 219
src/define.sh


+ 1 - 0
src/entry.sh

@@ -10,6 +10,7 @@ cd /run
 
 . reset.sh      # Initialize system
 . define.sh     # Define versions
+. mido.sh       # Download code
 . install.sh    # Run installation
 . disk.sh       # Initialize disks
 . display.sh    # Initialize graphics

+ 146 - 306
src/install.sh

@@ -73,6 +73,16 @@ startInstall() {
       : "${file//+/ }"; printf -v file '%b' "${_//%/\\x}"
       file=$(echo "$file" | sed -e 's/[^A-Za-z0-9._-]/_/g')
 
+    else
+
+      local language
+      language=$(getLanguage "$LANGUAGE" "culture")
+      language="${language%%-*}"
+
+      if [ -n "$language" ] && [[ "${language,,}" != "en" ]]; then
+        file="${VERSION/\//}_${language,,}.iso"
+      fi
+
     fi
 
     BOOT="$STORAGE/$file"
@@ -209,273 +219,6 @@ detectCustom() {
   return 0
 }
 
-getESD() {
-
-  local dir="$1"
-  local version="$2"
-  local editionName
-  local winCatalog size
-
-  if ! isESD "${version,,}"; then
-    error "Invalid VERSION specified, value \"$version\" is not recognized!" && return 1
-  fi
-
-  winCatalog=$(getCatalog "$version" "url")
-  editionName=$(getCatalog "$version" "edition")
-
-  local msg="Downloading product information from Microsoft..."
-  info "$msg" && html "$msg"
-
-  rm -rf "$dir"
-  mkdir -p "$dir"
-
-  local wFile="catalog.cab"
-  local xFile="products.xml"
-  local eFile="esd_edition.xml"
-  local fFile="products_filter.xml"
-
-  { wget "$winCatalog" -O "$dir/$wFile" -q --timeout=10; rc=$?; } || :
-  (( rc != 0 )) && error "Failed to download $winCatalog , reason: $rc" && return 1
-
-  cd "$dir"
-
-  if ! cabextract "$wFile" > /dev/null; then
-    cd /run
-    error "Failed to extract $wFile!" && return 1
-  fi
-
-  cd /run
-
-  if [ ! -s "$dir/$xFile" ]; then
-    error "Failed to find $xFile in $wFile!" && return 1
-  fi
-
-  local esdLang="en-us"
-  local edQuery='//File[Architecture="'${PLATFORM}'"][Edition="'${editionName}'"]'
-
-  echo -e '<Catalog>' > "$dir/$fFile"
-  xmllint --nonet --xpath "${edQuery}" "$dir/$xFile" >> "$dir/$fFile" 2>/dev/null
-  echo -e '</Catalog>'>> "$dir/$fFile"
-  xmllint --nonet --xpath '//File[LanguageCode="'${esdLang}'"]' "$dir/$fFile" >"$dir/$eFile"
-
-  size=$(stat -c%s "$dir/$eFile")
-  if ((size<20)); then
-    error "Failed to find Windows product in $eFile!" && return 1
-  fi
-
-  local tag="FilePath"
-  ESD=$(xmllint --nonet --xpath "//$tag" "$dir/$eFile" | sed -E -e "s/<[\/]?$tag>//g")
-
-  if [ -z "$ESD" ]; then
-    error "Failed to find ESD URL in $eFile!" && return 1
-  fi
-
-  tag="Sha1"
-  ESD_SUM=$(xmllint --nonet --xpath "//$tag" "$dir/$eFile" | sed -E -e "s/<[\/]?$tag>//g")
-  tag="Size"
-  ESD_SIZE=$(xmllint --nonet --xpath "//$tag" "$dir/$eFile" | sed -E -e "s/<[\/]?$tag>//g")
-
-  rm -rf "$dir"
-  return 0
-}
-
-verifyFile() {
-
-  local iso="$1"
-  local size="$2"
-  local total="$3"
-  local check="$4"
-
-  if [ -n "$size" ] && [[ "$total" != "$size" ]] && [[ "$size" != "0" ]]; then
-    warn "The downloaded file has an unexpected size: $total bytes, while expected value was: $size bytes. Please report this at $SUPPORT/issues"
-  fi
-
-  local hash=""
-  local algo="SHA256"
-
-  [ -z "$check" ] && return 0
-  [[ "$VERIFY" != [Yy1]* ]] && return 0
-  [[ "${#check}" == "40" ]] && algo="SHA1"
-
-  local msg="Verifying downloaded ISO..."
-  info "$msg" && html "$msg"
-
-  if [[ "${algo,,}" != "sha256" ]]; then
-    hash=$(sha1sum "$iso" | cut -f1 -d' ')
-  else
-    hash=$(sha256sum "$iso" | cut -f1 -d' ')
-  fi
-
-  if [[ "$hash" == "$check" ]]; then
-    info "Succesfully verified ISO!" && return 0
-  fi
-
-  error "The downloaded file has an invalid $algo checksum: $hash , while expected value was: $check. Please report this at $SUPPORT/issues"
-
-  rm -f "$iso"
-  return 1
-}
-
-doMido() {
-
-  local iso="$1"
-  local version="$2"
-  local desc="$3"
-  local rc sum size total
-
-  rm -f "$iso"
-  rm -f "$iso.PART"
-
-  size=$(getMido "$version" "size")
-  sum=$(getMido "$version" "sum")
-
-  local msg="Downloading $desc..."
-  info "$msg" && html "$msg"
-  /run/progress.sh "$iso.PART" "$size" "Downloading $desc ([P])..." &
-
-  cd "$TMP"
-  { /run/mido.sh "${version,,}"; rc=$?; } || :
-  cd /run
-
-  fKill "progress.sh"
-
-  if (( rc == 0 )) && [ -f "$iso" ]; then
-    total=$(stat -c%s "$iso")
-    if [ "$total" -gt 100000000 ]; then
-      ! verifyFile "$iso" "$size" "$total" "$sum" && return 1
-      html "Download finished successfully..." && return 0
-    fi
-  fi
-
-  rm -f "$iso"
-  rm -f "$iso.PART"
-
-  return 1
-}
-
-downloadFile() {
-
-  local iso="$1"
-  local url="$2"
-  local sum="$3"
-  local size="$4"
-  local desc="$5"
-  local rc total progress domain dots
-
-  rm -f "$iso"
-
-  # Check if running with interactive TTY or redirected to docker log
-  if [ -t 1 ]; then
-    progress="--progress=bar:noscroll"
-  else
-    progress="--progress=dot:giga"
-  fi
-
-  local msg="Downloading $desc..."
-  html "$msg"
-
-  domain=$(echo "$url" | awk -F/ '{print $3}')
-  dots=$(echo "$domain" | tr -cd '.' | wc -c)
-  (( dots > 1 )) && domain=$(expr "$domain" : '.*\.\(.*\..*\)')
-
-  if [ -n "$domain" ] && [[ "${domain,,}" != *"microsoft.com" ]]; then
-    msg="Downloading $desc from $domain..."
-  fi
-
-  info "$msg"
-  /run/progress.sh "$iso" "$size" "Downloading $desc ([P])..." &
-
-  { wget "$url" -O "$iso" -q --timeout=10 --show-progress "$progress"; rc=$?; } || :
-
-  fKill "progress.sh"
-
-  if (( rc == 0 )) && [ -f "$iso" ]; then
-    total=$(stat -c%s "$iso")
-    if [ "$total" -gt 100000000 ]; then
-      ! verifyFile "$iso" "$size" "$total" "$sum" && return 1
-      html "Download finished successfully..." && return 0
-    fi
-  fi
-
-  error "Failed to download $url , reason: $rc"
-
-  rm -f "$iso"
-  return 1
-}
-
-downloadImage() {
-
-  local iso="$1"
-  local version="$2"
-  local tried="n"
-  local url sum size base desc
-
-  if [[ "${version,,}" == "http"* ]]; then
-    base=$(basename "$iso")
-    desc=$(fromFile "$base")
-    downloadFile "$iso" "$version" "" "" "$desc" && return 0
-    return 1
-  fi
-
-  if ! validVersion "$version"; then
-    error "Invalid VERSION specified, value \"$version\" is not recognized!" && return 1
-  fi
-
-  desc=$(printVersion "$version" "")
-
-  if isMido "$version"; then
-    tried="y"
-    doMido "$iso" "$version" "$desc" && return 0
-  fi
-
-  switchEdition "$version"
-
-  if isESD "$version"; then
-
-    if [[ "$tried" != "n" ]]; then
-      info "Failed to download $desc using Mido, will try a diferent method now..."
-    fi
-
-    tried="y"
-
-    if getESD "$TMP/esd" "$version"; then
-      ISO="${ISO%.*}.esd"
-      downloadFile "$ISO" "$ESD" "$ESD_SUM" "$ESD_SIZE" "$desc" && return 0
-      ISO="$iso"
-    fi
-
-  fi
-
-  for ((i=1;i<=MIRRORS;i++)); do
-
-    url=$(getLink "$i" "$version")
-
-    if [ -n "$url" ]; then
-      if [[ "$tried" != "n" ]]; then
-        info "Failed to download $desc, will try another mirror now..."
-      fi
-      tried="y"
-      size=$(getSize "$i" "$version")
-      sum=$(getHash "$i" "$version")
-      downloadFile "$iso" "$url" "$sum" "$size" "$desc" && return 0
-    fi
-
-  done
-
-  return 1
-}
-
-removeDownload() {
-
-  local iso="$1"
-
-  [ ! -f "$iso" ] && return 0
-  [ -n "$CUSTOM" ] && return 0
-  ! rm -f "$iso" 2> /dev/null && warn "failed to remove $iso !"
-
-  return 0
-}
-
 extractESD() {
 
   local iso="$1"
@@ -609,19 +352,6 @@ extractImage() {
   return 0
 }
 
-setXML() {
-
-  local file="/custom.xml"
-  [ ! -f "$file" ] || [ ! -s "$file" ] && file="$STORAGE/custom.xml"
-  [ ! -f "$file" ] || [ ! -s "$file" ] && file="/run/assets/custom.xml"
-  [ ! -f "$file" ] || [ ! -s "$file" ] && file="$1"
-  [ ! -f "$file" ] || [ ! -s "$file" ] && file="/run/assets/$DETECTED.xml"
-  [ ! -f "$file" ] || [ ! -s "$file" ] && return 1
-
-  XML="$file"
-  return 0
-}
-
 getPlatform() {
 
   local xml="$1"
@@ -641,6 +371,26 @@ getPlatform() {
   return 0
 }
 
+checkPlatform() {
+
+  local xml="$1"
+  local platform compat
+
+  platform=$(getPlatform "$xml")
+
+  case "${platform,,}" in
+    "x86" ) compat="x64" ;;
+    "x64" ) compat="$platform" ;;
+    "arm64" ) compat="$platform" ;;
+    * ) compat="${PLATFORM,,}" ;;
+  esac
+
+  [[ "${compat,,}" == "${PLATFORM,,}" ]] && return 0
+
+  error "You cannot boot ${platform^^} images on a $PLATFORM CPU!"
+  return 1
+}
+
 hasVersion() {
 
   local id="$1"
@@ -686,26 +436,6 @@ selectVersion() {
   return 0
 }
 
-checkPlatform() {
-
-  local xml="$1"
-  local platform compat
-
-  platform=$(getPlatform "$xml")
-
-  case "${platform,,}" in
-    "x86" ) compat="x64" ;;
-    "x64" ) compat="$platform" ;;
-    "arm64" ) compat="$platform" ;;
-    * ) compat="${PLATFORM,,}" ;;
-  esac
-
-  [[ "${compat,,}" == "${PLATFORM,,}" ]] && return 0
-
-  error "You cannot boot ${platform^^} images on a $PLATFORM CPU!"
-  return 1
-}
-
 detectVersion() {
 
   local xml="$1"
@@ -720,11 +450,51 @@ detectVersion() {
   return 0
 }
 
+detectLanguage() {
+
+  local xml="$1"
+  local lang=""
+
+  if [[ "$xml" == *"LANGUAGE><DEFAULT>"* ]]; then
+    lang="${xml#*LANGUAGE><DEFAULT>}"
+    lang="${lang%%<*}"
+  else
+    if [[ "$xml" == *"FALLBACK><DEFAULT>"* ]]; then
+      lang="${xml#*FALLBACK><DEFAULT>}"
+      lang="${lang%%<*}"
+    fi
+  fi
+
+  if [ -z "$lang" ]; then
+   warn "Language could not be detected from ISO!" && return 0
+  fi
+
+  local culture
+  culture=$(getLanguage "$lang" "culture")
+  [ -n "$culture" ] && LANGUAGE="$lang" && return 0
+
+  warn "Invalid language detected: \"$lang\""
+  return 0
+}
+
+setXML() {
+
+  local file="/custom.xml"
+  [ ! -f "$file" ] || [ ! -s "$file" ] && file="$STORAGE/custom.xml"
+  [ ! -f "$file" ] || [ ! -s "$file" ] && file="/run/assets/custom.xml"
+  [ ! -f "$file" ] || [ ! -s "$file" ] && file="$1"
+  [ ! -f "$file" ] || [ ! -s "$file" ] && file="/run/assets/$DETECTED.xml"
+  [ ! -f "$file" ] || [ ! -s "$file" ] && return 1
+
+  XML="$file"
+  return 0
+}
+
 detectImage() {
 
   local dir="$1"
   local version="$2"
-  local desc msg
+  local desc msg language
 
   XML=""
 
@@ -787,6 +557,12 @@ detectImage() {
   fi
 
   desc=$(printEdition "$DETECTED" "$DETECTED")
+  detectLanguage "$info"
+
+  if [[ "${LANGUAGE,,}" != "en" ]] && [[ "${LANGUAGE,,}" != "en-"* ]]; then
+    language=$(getLanguage "$LANGUAGE" "desc")
+    desc="$desc ($language)"
+  fi
 
   info "Detected: $desc"
   setXML "" && return 0
@@ -844,10 +620,55 @@ prepareImage() {
   return 1
 }
 
+updateXML() {
+
+  local asset="$1"
+  local language="$2"
+  local culture region keyboard
+
+  culture=$(getLanguage "$language" "culture")
+
+  if [ -n "$culture" ] && [[ "${culture,,}" != "en-us" ]]; then
+    sed -i "s/<UILanguage>en-US<\/UILanguage>/<UILanguage>$culture<\/UILanguage>/g" "$asset"
+  fi
+
+  region="$REGION"
+  [ -z "$region" ] && region="$culture"
+
+  if [ -n "$region" ] && [[ "${region,,}" != "en-us" ]]; then
+    sed -i "s/<UserLocale>en-US<\/UserLocale>/<UserLocale>$region<\/UserLocale>/g" "$asset"
+    sed -i "s/<SystemLocale>en-US<\/SystemLocale>/<SystemLocale>$region<\/SystemLocale>/g" "$asset"
+  fi
+
+  keyboard="$KEYBOARD"
+  [ -z "$keyboard" ] && keyboard="$culture"
+
+  if [ -n "$keyboard" ] && [[ "${keyboard,,}" != "en-us" ]]; then
+    sed -i "s/<InputLocale>en-US<\/InputLocale>/<InputLocale>$keyboard<\/InputLocale>/g" "$asset"
+    sed -i "s/<InputLocale>0409:00000409<\/InputLocale>/<InputLocale>$keyboard<\/InputLocale>/g" "$asset"
+  fi
+
+  if [ -n "$USERNAME" ]; then
+    sed -i "s/where name=\"Docker\"/where name=\"$USERNAME\"/g" "$asset"
+    sed -i "s/<Name>Docker<\/Name>/<Name>$USERNAME<\/Name>/g" "$asset"
+    sed -i "s/<FullName>Docker<\/FullName>/<FullName>$USERNAME<\/FullName>/g" "$asset"
+    sed -i "s/<Username>Docker<\/Username>/<Username>$USERNAME<\/Username>/g" "$asset"
+  fi
+
+  if [ -n "$PASSWORD" ]; then
+    sed -i "s/<Value>password<\/Value>/<Value>$PASSWORD<\/Value>/g" "$asset"
+    sed -z "s/<Password>...........<Value \/>/<Password>\n          <Value>$PASSWORD<\/Value>/g" -i "$asset"
+    sed -z "s/<Password>...............<Value \/>/<Password>\n              <Value>$PASSWORD<\/Value>/g" -i "$asset"
+  fi
+
+  return 0
+}
+
 updateImage() {
 
   local dir="$1"
   local asset="$2"
+  local language="$3"
   local file="autounattend.xml"
   local org="${file/.xml/.org}"
   local dat="${file/.xml/.dat}"
@@ -903,13 +724,19 @@ updateImage() {
     xml=$(basename "$asset")
     info "Adding $xml for automatic installation..."
 
-    if ! wimlib-imagex update "$loc" "$index" --command "add $asset /$file" > /dev/null; then
+    local answer="$TMP/$xml"
+    cp "$asset" "$answer"
+    updateXML "$answer" "$language"
+
+    if ! wimlib-imagex update "$loc" "$index" --command "add $answer /$file" > /dev/null; then
       MANUAL="Y"
       warn "failed to add answer file ($xml) to ISO image, $FB"
     else
-      wimlib-imagex update "$loc" "$index" --command "add $asset /$dat" > /dev/null || true
+      wimlib-imagex update "$loc" "$index" --command "add $answer /$dat" > /dev/null || true
     fi
 
+    rm -f "$answer"
+
   fi
 
   if [[ "$MANUAL" == [Yy1]* ]]; then
@@ -920,9 +747,10 @@ updateImage() {
       if ! wimlib-imagex update "$loc" "$index" --command "add $TMP/$org /$file" > /dev/null; then
         warn "failed to restore original answer file ($org)."
       fi
-      rm -f "$TMP/$org"
     fi
 
+    rm -f "$TMP/$org"
+
   fi
 
   local find="$file"
@@ -940,6 +768,17 @@ updateImage() {
   return 0
 }
 
+removeDownload() {
+
+  local iso="$1"
+
+  [ ! -f "$iso" ] && return 0
+  [ -n "$CUSTOM" ] && return 0
+  ! rm -f "$iso" 2> /dev/null && warn "failed to remove $iso !"
+
+  return 0
+}
+
 copyOEM() {
 
   local dir="$1"
@@ -1105,6 +944,7 @@ bootWindows() {
 ######################################
 
 ! parseVersion && exit 58
+! parseLanguage && exit 56
 ! detectCustom && exit 59
 
 if ! startInstall; then
@@ -1113,7 +953,7 @@ if ! startInstall; then
 fi
 
 if [ ! -s "$ISO" ] || [ ! -f "$ISO" ]; then
-  if ! downloadImage "$ISO" "$VERSION"; then
+  if ! downloadImage "$ISO" "$VERSION" "$LANGUAGE"; then
     rm -f "$ISO" 2> /dev/null || true
     exit 61
   fi
@@ -1134,7 +974,7 @@ if ! prepareImage "$ISO" "$DIR"; then
   exit 60
 fi
 
-if ! updateImage "$DIR" "$XML"; then
+if ! updateImage "$DIR" "$XML" "$LANGUAGE"; then
   abortInstall "$ISO" && return 0
   exit 60
 fi

+ 530 - 715
src/mido.sh

@@ -1,761 +1,576 @@
-#!/bin/sh
-
-# Copyright (C) 2024 Elliot Killick <contact@elliotkillick.com>
-# Licensed under the MIT License. See LICENSE file for details.
-
-# Prefer Dash shell for greater security if available
-if [ "$BASH" ] && command -v dash > /dev/null; then
-    exec dash "$0" "$@"
-fi
-
-# Test for 4-bit color (16 colors)
-# Operand "colors" is undefined by POSIX
-# If the operand doesn't exist, the terminal probably doesn't support color and the program will continue normally without it
-if [ "0$(tput colors 2> /dev/null)" -ge 16 ]; then
-    RED='\033[0;31m'
-    BLUE='\033[0;34m'
-    GREEN='\033[0;32m'
-    NC='\033[0m'
-fi
-
-# Avoid printing messages as potential terminal escape sequences
-echo_ok() { printf "%b%s%b" "${GREEN}[+]${NC} " "$1" "\n" >&2; }
-echo_info() { printf "%b%s%b" "${BLUE}[i]${NC} " "$1" "\n" >&2; }
-echo_err() { printf "%b%s%b" "${RED}[!]${NC} " "$1" "\n" >&2; }
-
-# https://pubs.opengroup.org/onlinepubs/9699919799/utilities/fold.html
-format() { fold -s; }
-
-word_count() { echo $#; }
-
-usage() {
-    echo "Mido - The Secure Microsoft Windows Downloader"
-    echo ""
-    echo "Usage: $0 <windows_media>..."
-    echo ""
-    echo "Download specified list of Windows media."
-    echo ""
-    echo "Specify \"all\", or one or more of the following Windows media:"
-    echo "  win7x64-ultimate"
-    echo "  win81x64"
-    echo "  win10x64"
-    echo "  win11x64"
-    echo "  win81x64-enterprise-eval"
-    echo "  win10x64-enterprise-eval"
-    echo "  win11x64-enterprise-eval"
-    echo "  win10x64-enterprise-ltsc-eval (most secure)"
-    echo "  win2008r2"
-    echo "  win2012r2-eval"
-    echo "  win2016-eval"
-    echo "  win2019-eval"
-    echo "  win2022-eval"
-    echo ""
-    echo "Each ISO download takes between 3 - 7 GiBs (average: 5 GiBs)."
-    echo ""
-    echo "Updates"
-    echo "-------"
-    echo "All the downloads provided here are the most up-to-date releases that Microsoft provides. This is ensured by programmatically checking Microsoft's official download pages to get the latest download link. In other cases, the Windows version in question is no longer supported by Microsoft meaning a direct download link (stored in Mido) will always point to the most up-to-date release." | format
-    echo ""
-    echo "Remember to update Windows to the latest patch level after installation."
-    echo ""
-    echo "Overuse"
-    echo "-------"
-    echo "Newer consumer versions of Windows including win81x64, win10x64, and win11x64 are downloaded through Microsoft's gated download web interface. Do not overuse this interface. Microsoft may be quick to do ~24 hour IP address bans after only a few download requests (especially if they are done in quick succession). Being temporarily banned from one of these downloads (e.g. win11x64) doesn't cause you to be banned from any of the other downloads provided through this interface." | format
-    echo ""
-    echo "Privacy Preserving Technologies"
-    echo "-------------------------------"
-    echo "The aforementioned Microsoft gated download web interface is currently blocking Tor (and similar technologies). They say this is to prevent people in restricted regions from downloading certain Windows media they shouldn't have access to. This is fine by most standards because Tor is too slow for large downloads anyway and we have checksum verification for security." | format
-    echo ""
-    echo "Language"
-    echo "--------"
-    echo "All the downloads provided here are for English (United States). This helps to great simplify maintenance and minimize the user's fingerprint. If another language is desired then that can easily be configured in Windows once it's installed." | format
-    echo ""
-    echo "Architecture"
-    echo "------------"
-    echo "All the downloads provided here are for x86-64 (x64). This is the only architecture Microsoft ships Windows Server in.$([ -d /run/qubes ] && echo ' Also, the only architecture Qubes OS supports.')" | format
+#!/usr/bin/env bash
+set -Eeuo pipefail
+
+handle_curl_error() {
+
+  local error_code="$1"
+
+  case "$error_code" in
+    6) error "Failed to resolve Microsoft servers! Is there an Internet connection?" ;;
+    7) error "Failed to contact Microsoft servers! Is there an Internet connection or is the server down?" ;;
+    8) error "Microsoft servers returned a malformed HTTP response!" ;;
+    22) error "Microsoft servers returned a failing HTTP status code!" ;;
+    23) error "Failed at writing Windows media to disk! Out of disk space or permission error?" ;;
+    26) error "Ran out of memory during download!" ;;
+    28) error "Connection timed out to Microsoft server!" ;;
+    35) error "SSL connection error from Microsoft server!" ;;
+    36) error "Failed to continue earlier download!" ;;
+    52) error "Received no data from the Microsoft server!" ;;
+    63) error "Microsoft servers returned an unexpectedly large response!" ;;
+    # POSIX defines exit statuses 1-125 as usable by us
+    # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02
+    $((error_code <= 125)))
+      # Must be some other server or network error (possibly with this specific request/file)
+      # This is when accounting for all possible errors in the curl manual assuming a correctly formed curl command and an HTTP(S) request, using only the curl features we're using, and a sane build
+      error "Miscellaneous server or network error, reason: $error_code"
+      ;;
+    126 | 127 ) error "Curl command not found!" ;;
+    # Exit statuses are undefined by POSIX beyond this point
+    *)
+      case "$(kill -l "$error_code")" in
+        # Signals defined to exist by POSIX:
+        # https://pubs.opengroup.org/onlinepubs/009695399/basedefs/signal.h.html
+        INT) error "Curl was interrupted!" ;;
+        # There could be other signals but these are most common
+        SEGV | ABRT ) error "Curl crashed! Failed exploitation attempt? Please report any core dumps to curl developers." ;;
+        *) error "Curl terminated due to a fatal signal!" ;;
+      esac
+  esac
+
+  return 1
 }
 
-# Media naming scheme info:
-# Windows Server has no architecture because Microsoft only supports amd64 for this version of Windows (the last version to support x86 was Windows Server 2008 without the R2)
-# "eval" is short for "evaluation", it's simply the license type included with the Windows installation (only exists on enterprise/server) and must be specified in the associated answer file
-# "win7x64" has the "ultimate" edition appended to it because it isn't "multi-edition" like the other Windows ISOs (for multi-edition ISOs the edition is specified in the associated answer file)
-
-readonly win7x64_ultimate="win7x64-ultimate.iso"
-readonly win81x64="win81x64.iso"
-readonly win10x64="win10x64.iso"
-readonly win11x64="win11x64.iso"
-readonly win81x64_enterprise_eval="win81x64-enterprise-eval.iso"
-readonly win10x64_enterprise_eval="win10x64-enterprise-eval.iso"
-readonly win11x64_enterprise_eval="win11x64-enterprise-eval.iso"
-readonly win10x64_enterprise_ltsc_eval="win10x64-enterprise-ltsc-eval.iso"
-readonly win2008r2="win2008r2.iso"
-readonly win2012r2_eval="win2012r2-eval.iso"
-readonly win2016_eval="win2016-eval.iso"
-readonly win2019_eval="win2019-eval.iso"
-readonly win2022_eval="win2022-eval.iso"
-
-parse_args() {
-    for arg in "$@"; do
-        if [ "$arg" = "-h" ] ||  [ "$arg" = "--help" ]; then
-            usage
-            exit
-        fi
-    done
-
-    if [ $# -lt 1 ]; then
-        usage >&2
-        exit 1
-    fi
+download_windows() {
+
+  local id="$1"
+  local lang="$2"
+  local sku_id=""
+  local language=""
+  local session_id=""
+  local browser_version=""
+  local windows_version=""
+  local iso_download_link=""
+  local product_edition_id=""
+  local iso_download_link_html=""
+  local iso_download_page_html=""
+  local language_skuid_table_html=""
+
+  case "${id,,}" in
+    "win11${PLATFORM,,}" ) windows_version="11" ;;
+    "win10${PLATFORM,,}" ) windows_version="10" ;;
+    "win81${PLATFORM,,}" ) windows_version="8" ;;
+    * ) error "Invalid VERSION specified, value \"$id\" is not recognized!" && return 1 ;;
+  esac
+
+  language=$(getLanguage "$lang" "name")
+
+  local url="https://www.microsoft.com/en-us/software-download/windows$windows_version"
+  case "$windows_version" in
+    8 | 10) url="${url}ISO";;
+  esac
+
+  # Determine approximate latest Firefox release
+  browser_version="$((124 + ($(date +%s) - 1710892800) / 2419200))"
+  local user_agent="Mozilla/5.0 (X11; Linux x86_64; rv:${browser_version}.0) Gecko/20100101 Firefox/${browser_version}.0"
+
+  # uuidgen: For MacOS (installed by default) and other systems (e.g. with no /proc) that don't have a kernel interface for generating random UUIDs
+  session_id="$(cat /proc/sys/kernel/random/uuid 2> /dev/null || uuidgen --random)"
+
+  # Get product edition ID for latest release of given Windows version
+  # Product edition ID: This specifies both the Windows release (e.g. 22H2) and edition ("multi-edition" is default, either Home/Pro/Edu/etc., we select "Pro" in the answer files) in one number
+  # This is the *only* request we make that Fido doesn't. Fido manually maintains a list of all the Windows release/edition product edition IDs in its script (see: $WindowsVersions array). This is helpful for downloading older releases (e.g. Windows 10 1909, 21H1, etc.) but we always want to get the newest release which is why we get this value dynamically
+  # Also, keeping a "$WindowsVersions" array like Fido does would be way too much of a maintenance burden
+  # Remove "Accept" header that curl sends by default
+  [[ "$DEBUG" == [Yy1]* ]] && echo " - Parsing download page: ${url}"
+  iso_download_page_html="$(curl --silent --max-time 30 --user-agent "$user_agent" --header "Accept:" --max-filesize 1M --fail --proto =https --tlsv1.2 --http1.1 -- "$url")" || {
+    handle_curl_error $?
+    return $?
+  }
+
+  [[ "$DEBUG" == [Yy1]* ]] && echo -n "Getting Product edition ID: "
+  # tr: Filter for only numerics to prevent HTTP parameter injection
+  # head -c was recently added to POSIX: https://austingroupbugs.net/view.php?id=407
+  product_edition_id="$(echo "$iso_download_page_html" | grep -Eo '<option value="[0-9]+">Windows' | cut -d '"' -f 2 | head -n 1 | tr -cd '0-9' | head -c 16)"
+  [[ "$DEBUG" == [Yy1]* ]] && echo "$product_edition_id"
+
+  [[ "$DEBUG" == [Yy1]* ]] && echo "Permit Session ID: $session_id"
+  # Permit Session ID
+  # "org_id" is always the same value
+  curl --silent --max-time 30 --output /dev/null --user-agent "$user_agent" --header "Accept:" --max-filesize 100K --fail --proto =https --tlsv1.2 --http1.1 -- "https://vlscppe.microsoft.com/tags?org_id=y6jn8c31&session_id=$session_id" || {
+    # This should only happen if there's been some change to how this API works
+    handle_curl_error $?
+    return $?
+  }
+
+  # Extract everything after the last slash
+  local url_segment_parameter="${url##*/}"
+
+  [[ "$DEBUG" == [Yy1]* ]] && echo -n "Getting language SKU ID: "
+  # Get language -> skuID association table
+  # SKU ID: This specifies the language of the ISO. We always use "English (United States)", however, the SKU for this changes with each Windows release
+  # We must make this request so our next one will be allowed
+  # --data "" is required otherwise no "Content-Length" header will be sent causing HTTP response "411 Length Required"
+  language_skuid_table_html="$(curl --silent --max-time 30 --request POST --user-agent "$user_agent" --data "" --header "Accept:" --max-filesize 10K --fail --proto =https --tlsv1.2 --http1.1 -- "https://www.microsoft.com/en-US/api/controls/contentinclude/html?pageId=a8f8f489-4c7f-463a-9ca6-5cff94d8d041&host=www.microsoft.com&segments=software-download,$url_segment_parameter&query=&action=getskuinformationbyproductedition&sessionId=$session_id&productEditionId=$product_edition_id&sdVersion=2")" || {
+    handle_curl_error $?
+    return $?
+  }
+
+  # tr: Filter for only alphanumerics or "-" to prevent HTTP parameter injection
+  sku_id="$(echo "$language_skuid_table_html" | grep -m 1 ">${language}<" | sed 's/&quot;//g' | cut -d ',' -f 1  | cut -d ':' -f 2 | tr -cd '[:alnum:]-' | head -c 16)"
+
+  if [ -z "$sku_id" ]; then
+    language=$(getLanguage "$lang" "desc")
+    error "No download for the $language language available!"
+    return 1
+  fi
+
+  [[ "$DEBUG" == [Yy1]* ]] && echo "$sku_id"
+  [[ "$DEBUG" == [Yy1]* ]] && echo "Getting ISO download link..."
 
-    # Append to media_list so media is downloaded in the order they're passed in
-    for arg in "$@"; do
-        case "$arg" in
-            win7x64-ultimate)
-                media_list="$media_list $win7x64_ultimate"
-                ;;
-            win81x64)
-                media_list="$media_list $win81x64"
-                ;;
-            win10x64)
-                media_list="$media_list $win10x64"
-                ;;
-            win11x64)
-                media_list="$media_list $win11x64"
-                ;;
-            win81x64-enterprise-eval)
-                media_list="$media_list $win81x64_enterprise_eval"
-                ;;
-            win10x64-enterprise-eval)
-                media_list="$media_list $win10x64_enterprise_eval"
-                ;;
-            win11x64-enterprise-eval)
-                media_list="$media_list $win11x64_enterprise_eval"
-                ;;
-            win10x64-enterprise-ltsc-eval)
-                media_list="$media_list $win10x64_enterprise_ltsc_eval"
-                ;;
-            win2008r2)
-                media_list="$media_list $win2008r2"
-                ;;
-            win2012r2-eval)
-                media_list="$media_list $win2012r2_eval"
-                ;;
-            win2016-eval)
-                media_list="$media_list $win2016_eval"
-                ;;
-            win2019-eval)
-                media_list="$media_list $win2019_eval"
-                ;;
-            win2022-eval)
-                media_list="$media_list $win2022_eval"
-                ;;
-            all)
-                media_list="$win7x64_ultimate $win81x64 $win10x64 $win11x64 $win81x64_enterprise_eval $win10x64_enterprise_eval $win11x64_enterprise_eval $win10x64_enterprise_ltsc_eval $win2008r2 $win2012r2_eval $win2016_eval $win2019_eval $win2022_eval"
-                break
-                ;;
-            *)
-                echo_err "Invalid Windows media specified: $arg"
-                exit 1
-                ;;
-        esac
-    done
+  # Get ISO download link
+  # If any request is going to be blocked by Microsoft it's always this last one (the previous requests always seem to succeed)
+  # --referer: Required by Microsoft servers to allow request
+  iso_download_link_html="$(curl --silent --max-time 30 --request POST --user-agent "$user_agent" --data "" --referer "$url" --header "Accept:" --max-filesize 100K --fail --proto =https --tlsv1.2 --http1.1 -- "https://www.microsoft.com/en-US/api/controls/contentinclude/html?pageId=6e2a1789-ef16-4f27-a296-74ef7ef5d96b&host=www.microsoft.com&segments=software-download,$url_segment_parameter&query=&action=GetProductDownloadLinksBySku&sessionId=$session_id&skuId=$sku_id&language=English&sdVersion=2")"
+
+  if ! [ "$iso_download_link_html" ]; then
+    # This should only happen if there's been some change to how this API works
+    error "Microsoft servers gave us an empty response to our request for an automated download."
+    return 1
+  fi
+
+  if echo "$iso_download_link_html" | grep -q "We are unable to complete your request at this time."; then
+    error "Microsoft blocked the automated download request based on your IP address."
+    return 1
+  fi
+
+  # Filter for 64-bit ISO download URL
+  # sed: HTML decode "&" character
+  # tr: Filter for only alphanumerics or punctuation
+  iso_download_link="$(echo "$iso_download_link_html" | grep -o "https://software.download.prss.microsoft.com.*IsoX64" | cut -d '"' -f 1 | sed 's/&amp;/\&/g' | tr -cd '[:alnum:][:punct:]')"
+
+  if ! [ "$iso_download_link" ]; then
+    # This should only happen if there's been some change to the download endpoint web address
+    error "Microsoft servers gave us no download link to our request for an automated download!"
+    return 1
+  fi
+
+  MIDO_URL="$iso_download_link"
+  return 0
 }
 
-handle_curl_error() {
-    error_code="$1"
-
-    fatal_error_action=2
-
-    case "$error_code" in
-        6)
-            echo_err "Failed to resolve Microsoft servers! Is there an Internet connection? Exiting..."
-            return "$fatal_error_action"
-            ;;
-        7)
-            echo_err "Failed to contact Microsoft servers! Is there an Internet connection or is the server down?"
-            ;;
-        8)
-            echo_err "Microsoft servers returned a malformed HTTP response!"
-            ;;
-        22)
-            echo_err "Microsoft servers returned a failing HTTP status code!"
-            ;;
-        23)
-            echo_err "Failed at writing Windows media to disk! Out of disk space or permission error? Exiting..."
-            return "$fatal_error_action"
-            ;;
-        26)
-            echo_err "Ran out of memory during download! Exiting..."
-            return "$fatal_error_action"
-            ;;
-        36)
-            echo_err "Failed to continue earlier download!"
-            ;;
-        63)
-            echo_err "Microsoft servers returned an unexpectedly large response!"
-            ;;
-        # POSIX defines exit statuses 1-125 as usable by us
-        # https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02
-        $((error_code <= 125)))
-            # Must be some other server or network error (possibly with this specific request/file)
-            # This is when accounting for all possible errors in the curl manual assuming a correctly formed curl command and an HTTP(S) request, using only the curl features we're using, and a sane build
-            echo_err "Miscellaneous server or network error!"
-            ;;
-        126 | 127)
-            echo_err "Curl command not found! Please install curl and try again. Exiting..."
-            return "$fatal_error_action"
-            ;;
-        # Exit statuses are undefined by POSIX beyond this point
-        *)
-            case "$(kill -l "$error_code")" in
-                # Signals defined to exist by POSIX:
-                # https://pubs.opengroup.org/onlinepubs/009695399/basedefs/signal.h.html
-                INT)
-                    echo_err "Curl was interrupted!"
-                    ;;
-                # There could be other signals but these are most common
-                SEGV | ABRT)
-                    echo_err "Curl crashed! Failed exploitation attempt? Please report any core dumps to curl developers. Exiting..."
-                    return "$fatal_error_action"
-                    ;;
-                *)
-                    echo_err "Curl terminated due to a fatal signal!"
-                    ;;
-            esac
-    esac
+download_windows_eval() {
+
+  local id="$1"
+  local lang="$2"
+  local culture=""
+  local language=""
+  local windows_version=""
+  local enterprise_type=""
+
+  case "${id,,}" in
+    "win11${PLATFORM,,}-enterprise-eval" )
+      windows_version="windows-11-enterprise"
+      enterprise_type="enterprise" ;;
+    "win10${PLATFORM,,}-enterprise-eval" )
+      windows_version="windows-10-enterprise"
+      enterprise_type="enterprise" ;;
+    "win10${PLATFORM,,}-enterprise-ltsc-eval" )
+      windows_version="windows-10-enterprise"
+      enterprise_type="ltsc" ;;
+    "win2022-eval" )
+      windows_version="windows-server-2022"
+      enterprise_type="server" ;;
+    "win2019-eval" )
+      windows_version="windows-server-2019"
+      enterprise_type="server" ;;
+    "win2016-eval" )
+      windows_version="windows-server-2016"
+      enterprise_type="server" ;;
+    "win2012r2-eval" )
+      windows_version="windows-server-2012-r2"
+      enterprise_type="server" ;;
+    * )
+      error "Invalid VERSION specified, value \"$id\" is not recognized!" && return 1 ;;
+  esac
+
+  culture=$(getLanguage "$lang" "culture")
+
+  local country="${culture#*-}"
+  local iso_download_page_html=""
+  local url="https://www.microsoft.com/en-us/evalcenter/download-$windows_version"
+
+  [[ "$DEBUG" == [Yy1]* ]] && echo "Parsing download page: ${url}"
+  iso_download_page_html="$(curl --silent --max-time 30 --location --max-filesize 1M --fail --proto =https --tlsv1.2 --http1.1 -- "$url")" || {
+    handle_curl_error $?
+    return $?
+  }
+
+  if ! [ "$iso_download_page_html" ]; then
+    # This should only happen if there's been some change to where this download page is located
+    error "Windows server download page gave us an empty response"
+    return 1
+  fi
 
+  [[ "$DEBUG" == [Yy1]* ]] && echo "Getting download link.."
+  iso_download_links="$(echo "$iso_download_page_html" | grep -o "https://go.microsoft.com/fwlink/p/?LinkID=[0-9]\+&clcid=0x[0-9a-z]\+&culture=${culture,,}&country=${country^^}")" || {
+    # This should only happen if there's been some change to the download endpoint web address
+    if [[ "${lang,,}" == "en" ]] || [[ "${lang,,}" == "en-"* ]]; then
+      error "Windows server download page gave us no download link!"
+    else
+      language=$(getLanguage "$lang" "desc")
+      error "No download for the $language language available!"
+    fi
     return 1
+  }
+
+  # Limit untrusted size for input validation
+  iso_download_links="$(echo "$iso_download_links" | head -c 1024)"
+
+  case "$enterprise_type" in
+    # Select x64 download link
+    "enterprise") iso_download_link=$(echo "$iso_download_links" | head -n 2 | tail -n 1) ;;
+    # Select x64 LTSC download link
+    "ltsc") iso_download_link=$(echo "$iso_download_links" | head -n 4 | tail -n 1) ;;
+    *) iso_download_link="$iso_download_links" ;;
+  esac
+
+  # Follow redirect so proceeding log message is useful
+  # This is a request we make this Fido doesn't
+  # We don't need to set "--max-filesize" here because this is a HEAD request and the output is to /dev/null anyway
+  iso_download_link="$(curl --silent --max-time 30 --location --output /dev/null --silent --write-out "%{url_effective}" --head --fail --proto =https --tlsv1.2 --http1.1 -- "$iso_download_link")" || {
+    # This should only happen if the Microsoft servers are down
+    handle_curl_error $?
+    return $?
+  }
+
+  MIDO_URL="$iso_download_link"
+  return 0
 }
 
-part_ext=".PART"
-unverified_ext=".UNVERIFIED"
-
-scurl_file() {
-    out_file="$1"
-    tls_version="$2"
-    url="$3"
-
-    part_file="${out_file}${part_ext}"
-
-    # --location: Microsoft likes to change which endpoint these downloads are stored on but is usually kind enough to add redirects
-    # --fail: Return an error on server errors where the HTTP response code is 400 or greater
-    curl --progress-bar --location --output "$part_file" --continue-at - --max-filesize 10G --fail --proto =https "--tlsv$tls_version" --http1.1 -- "$url" || {
-        error_code=$?
-        handle_curl_error "$error_code"
-        error_action=$?
-
-        # Clean up and make sure a future resume doesn't happen from a bad download resume file
-        if [ -f "$out_file" ]; then
-            # If file is empty, bad HTTP code, or bad download resume file
-            if [ ! -s "$out_file" ] || [ "$error_code" = 22 ] || [ "$error_code" = 36 ]; then
-                echo_info "Deleting failed download..."
-                rm -f "$out_file"
-            fi
-        fi
-
-        return "$error_action"
-    }
-
-    # Full downloaded succeeded, ready for verification check
-    mv "$part_file" "${out_file}"
+getWindows() {
+
+  local version="$1"
+  local lang="$2"
+  local desc="$3"
+  local language
+
+  local msg="Requesting $desc from Microsoft server..."
+  info "$msg" && html "$msg"
+
+  case "${version,,}" in
+    "win81${PLATFORM,,}" | "win10${PLATFORM,,}" | "win11${PLATFORM,,}" )
+      download_windows "$version" "$lang" && return 0
+      ;;
+    "win11${PLATFORM,,}-enterprise-eval" )
+      download_windows_eval "$version" "$lang" && return 0
+      ;;
+    "win10${PLATFORM,,}-enterprise-eval" | "win10${PLATFORM,,}-enterprise-ltsc-eval" )
+      download_windows_eval "$version" "$lang" && return 0
+      ;;
+    "win2022-eval" | "win2019-eval" | "win2016-eval" | "win2012r2-eval" )
+      download_windows_eval "$version" "$lang" && return 0
+      ;;
+    "win81${PLATFORM,,}-enterprise-eval" )
+      if [[ "${lang,,}" == "en" ]] || [[ "${lang,,}" == "en-"* ]]; then
+        MIDO_URL="https://download.microsoft.com/download/B/9/9/B999286E-0A47-406D-8B3D-5B5AD7373A4A/9600.17050.WINBLUE_REFRESH.140317-1640_X64FRE_ENTERPRISE_EVAL_EN-US-IR3_CENA_X64FREE_EN-US_DV9.ISO" && return 0
+      fi
+      language=$(getLanguage "$lang" "desc")
+      error "No download for the $language language available!"
+      ;;
+    "win2008r2" )
+      if [[ "${lang,,}" == "en" ]] || [[ "${lang,,}" == "en-"* ]]; then
+        MIDO_URL="https://download.microsoft.com/download/4/1/D/41DEA7E0-B30D-4012-A1E3-F24DC03BA1BB/7601.17514.101119-1850_x64fre_server_eval_en-us-GRMSXEVAL_EN_DVD.iso" && return 0
+      fi
+      language=$(getLanguage "$lang" "desc")
+      error "No download for the $language language available!"
+      ;;
+    * ) error "Invalid VERSION specified, value \"$version\" is not recognized!" ;;
+  esac
+
+  MIDO_URL=""
+  return 1
 }
 
-manual_verification() {
-    media_verification_failed_list="$1"
-    checksum_verification_failed_list="$2"
-
-    echo_info "Manual verification instructions"
-    echo "    1. Get checksum (may already be done for you):" >&2
-    echo "    sha256sum <ISO_FILENAME>" >&2
-    echo "" >&2
-    echo "    2. Verify media:" >&2
-    echo "    Web search: https://duckduckgo.com/?q=%22CHECKSUM_HERE%22" >&2
-    echo "    Onion search: https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion/?q=%22CHECKSUM_HERE%22" >&2
-    echo "    \"No results found\" or unexpected results indicates the media has been modified and should not be used." >&2
-    echo "" >&2
-    echo "    3. Remove the $unverified_ext extension from the file after performing or deciding to skip verification (not recommended):" >&2
-    echo "    mv <ISO_FILENAME>$unverified_ext <ISO_FILENAME>" >&2
-    echo "" >&2
-
-    for media in $media_verification_failed_list; do
-        # Read current checksum in list and then read remaining checksums back into the list (effectively running "shift" on the variable)
-        # POSIX sh doesn't support indexing so do this instead to iterate both lists at once
-        # POSIX sh doesn't support here-strings (<<<). We could also use the "cut" program but that's not a builtin
-        IFS=' ' read -r checksum checksum_verification_failed_list << EOF
-$checksum_verification_failed_list
-EOF
-
-        echo "    ${media}${unverified_ext} = $checksum" >&2
-        echo "        Web search: https://duckduckgo.com/?q=%22$checksum%22" >&2
-        echo "        Onion search: https://duckduckgogg42xjoc72x3sjasowoarfbgcmvfimaftt6twagswzczad.onion/?q=%22$checksum%22" >&2
-        echo "        mv ${media}${unverified_ext} $media" >&2
-        echo "" >&2
-    done
-
-    echo "    Theses searches can be performed in a web/Tor browser or more securely using" >&2
-    echo "    ddgr (Debian/Fedora packages available) terminal search tool if preferred." >&2
-    echo "    Once validated, consider updating the checksums in Mido by submitting a pull request on GitHub." >&2
-
-    # If you're looking for a single secondary source to cross-reference checksums then try here: https://files.rg-adguard.net/search
-    # This site is recommended by the creator of Rufus in the Fido README and has worked well for me
+getCatalog() {
+
+  local id="$1"
+  local ret="$2"
+  local url=""
+  local name=""
+  local edition=""
+
+  case "${id,,}" in
+    "win11${PLATFORM,,}" )
+      edition="Professional"
+      name="Windows 11 Pro"
+      url="https://go.microsoft.com/fwlink?linkid=2156292" ;;
+    "win10${PLATFORM,,}" )
+      edition="Professional"
+      name="Windows 10 Pro"
+      url="https://go.microsoft.com/fwlink/?LinkId=841361" ;;
+    "win11${PLATFORM,,}-enterprise" | "win11${PLATFORM,,}-enterprise-eval")
+      edition="Enterprise"
+      name="Windows 11 Enterprise"
+      url="https://go.microsoft.com/fwlink?linkid=2156292" ;;
+    "win10${PLATFORM,,}-enterprise" | "win10${PLATFORM,,}-enterprise-eval" )
+      edition="Enterprise"
+      name="Windows 10 Enterprise"
+      url="https://go.microsoft.com/fwlink/?LinkId=841361" ;;
+  esac
+
+  case "${ret,,}" in
+    "url" ) echo "$url" ;;
+    "name" ) echo "$name" ;;
+    "edition" ) echo "$edition" ;;
+    *) echo "";;
+  esac
+
+  return 0
 }
 
-consumer_download() {
-    # Copyright (C) 2024 Elliot Killick <contact@elliotkillick.com>
-    # Licensed under the MIT License. See LICENSE file for details.
-    #
-    # This function is from the Mido project:
-    # https://github.com/ElliotKillick/Mido
-
-    # Download newer consumer Windows versions from behind gated Microsoft API
-
-    out_file="$1"
-    # Either 8, 10, or 11
-    windows_version="$2"
-
-    url="https://www.microsoft.com/en-us/software-download/windows$windows_version"
-    case "$windows_version" in
-        8 | 10) url="${url}ISO" ;;
-    esac
-
-    user_agent="Mozilla/5.0 (X11; Linux x86_64; rv:100.0) Gecko/20100101 Firefox/100.0"
-    # uuidgen: For MacOS (installed by default) and other systems (e.g. with no /proc) that don't have a kernel interface for generating random UUIDs
-    session_id="$(cat /proc/sys/kernel/random/uuid 2> /dev/null || uuidgen --random)"
-
-    # Get product edition ID for latest release of given Windows version
-    # Product edition ID: This specifies both the Windows release (e.g. 22H2) and edition ("multi-edition" is default, either Home/Pro/Edu/etc., we select "Pro" in the answer files) in one number
-    # This is a request we make that Fido doesn't. Fido manually maintains a list of all the Windows release/edition product edition IDs in its script (see: $WindowsVersions array). This is helpful for downloading older releases (e.g. Windows 10 1909, 21H1, etc.) but we always want to get the newest release which is why we get this value dynamically
-    # Also, keeping a "$WindowsVersions" array like Fido does would be way too much of a maintenance burden
-    # Remove "Accept" header that curl sends by default (match Fido requests)
-    iso_download_page_html="$(curl -sS --user-agent "$user_agent" --header "Accept:" --max-filesize 1M --fail --proto =https --tlsv1.2 --http1.1 -- "$url")" || {
-        handle_curl_error $?
-        return $?
-    }
-
-    # tr: Filter for only numerics to prevent HTTP parameter injection
-    # head -c was recently added to POSIX: https://austingroupbugs.net/view.php?id=407
-    product_edition_id="$(echo "$iso_download_page_html" | grep -Eo '<option value="[0-9]+">Windows' | cut -d '"' -f 2 | head -n 1 | tr -cd '0-9' | head -c 16)"
-    [ "$VERBOSE" ] && echo "Product edition ID: $product_edition_id" >&2
-
-    # Permit Session ID
-    # "org_id" is always the same value
-    curl -sS --output /dev/null --user-agent "$user_agent" --header "Accept:" --max-filesize 100K --fail --proto =https --tlsv1.2 --http1.1 -- "https://vlscppe.microsoft.com/tags?org_id=y6jn8c31&session_id=$session_id" || {
-        # This should only happen if there's been some change to how this API works
-        handle_curl_error $?
-        return $?
-    }
-
-    # Extract everything after the last slash
-    url_segment_parameter="${url##*/}"
-
-    # Get language -> skuID association table
-    # SKU ID: This specifies the language of the ISO. We always use "English (United States)", however, the SKU for this changes with each Windows release
-    # We must make this request so our next one will be allowed
-    # --data "" is required otherwise no "Content-Length" header will be sent causing HTTP response "411 Length Required"
-    language_skuid_table_html="$(curl -sS --request POST --user-agent "$user_agent" --data "" --header "Accept:" --max-filesize 10K --fail --proto =https --tlsv1.2 --http1.1 -- "https://www.microsoft.com/en-US/api/controls/contentinclude/html?pageId=a8f8f489-4c7f-463a-9ca6-5cff94d8d041&host=www.microsoft.com&segments=software-download,$url_segment_parameter&query=&action=getskuinformationbyproductedition&sessionId=$session_id&productEditionId=$product_edition_id&sdVersion=2")" || {
-        handle_curl_error $?
-        return $?
-    }
-
-    # tr: Filter for only alphanumerics or "-" to prevent HTTP parameter injection
-    sku_id="$(echo "$language_skuid_table_html" | grep "English (United States)" | sed 's/&quot;//g' | cut -d ',' -f 1  | cut -d ':' -f 2 | tr -cd '[:alnum:]-' | head -c 16)"
-    [ "$VERBOSE" ] && echo "SKU ID: $sku_id" >&2
-
-    # Get ISO download link
-    # If any request is going to be blocked by Microsoft it's always this last one (the previous requests always seem to succeed)
-    # --referer: Required by Microsoft servers to allow request
-    iso_download_link_html="$(curl -sS --request POST --user-agent "$user_agent" --data "" --referer "$url" --header "Accept:" --max-filesize 100K --fail --proto =https --tlsv1.2 --http1.1 -- "https://www.microsoft.com/en-US/api/controls/contentinclude/html?pageId=6e2a1789-ef16-4f27-a296-74ef7ef5d96b&host=www.microsoft.com&segments=software-download,$url_segment_parameter&query=&action=GetProductDownloadLinksBySku&sessionId=$session_id&skuId=$sku_id&language=English&sdVersion=2")" || {
-        # This should only happen if there's been some change to how this API works
-        handle_curl_error $?
-        return $?
-    }
-
-    if ! [ "$iso_download_link_html" ]; then
-        # This should only happen if there's been some change to how this API works
-        echo_err "Microsoft servers gave us an empty response to our request for an automated download."
-        manual_verification="true"
-        return 1
-    fi
+getESD() {
 
-    if echo "$iso_download_link_html" | grep -q "We are unable to complete your request at this time."; then
-        echo_err "Microsoft blocked the automated download request based on your IP address."
-        manual_verification="true"
-        return 1
-    fi
+  local dir="$1"
+  local version="$2"
+  local lang="$3"
+  local desc="$4"
+  local culture
+  local language
+  local editionName
+  local winCatalog size
 
-    # Filter for 64-bit ISO download URL
-    # sed: HTML decode "&" character
-    # tr: Filter for only alphanumerics or punctuation
-    iso_download_link="$(echo "$iso_download_link_html" | grep -o "https://software.download.prss.microsoft.com.*IsoX64" | cut -d '"' -f 1 | sed 's/&amp;/\&/g' | tr -cd '[:alnum:][:punct:]')"
+  culture=$(getLanguage "$lang" "culture")
+  winCatalog=$(getCatalog "$version" "url")
+  editionName=$(getCatalog "$version" "edition")
 
-    if ! [ "$iso_download_link" ]; then
-        # This should only happen if there's been some change to the download endpoint web address
-        echo_err "Microsoft servers gave us no download link to our request for an automated download."
-        manual_verification="true"
-        return 1
-    fi
+  if [ -z "$winCatalog" ] || [ -z "$editionName" ]; then
+    error "Invalid VERSION specified, value \"$version\" is not recognized!" && return 1
+  fi
 
-    #echo_ok "Got latest ISO download link (valid for 24 hours): $iso_download_link"
+  local msg="Downloading product information from Microsoft server..."
+  info "$msg" && html "$msg"
 
-    # Download ISO
-    scurl_file "$out_file" "1.3" "$iso_download_link"
-}
+  rm -rf "$dir"
+  mkdir -p "$dir"
 
-enterprise_eval_download() {
-    # Copyright (C) 2024 Elliot Killick <contact@elliotkillick.com>
-    # Licensed under the MIT License. See LICENSE file for details.
-    #
-    # This function is from the Mido project:
-    # https://github.com/ElliotKillick/Mido
+  local wFile="catalog.cab"
+  local xFile="products.xml"
+  local eFile="esd_edition.xml"
+  local fFile="products_filter.xml"
 
-    # Download enterprise evaluation Windows versions
+  { wget "$winCatalog" -O "$dir/$wFile" -q --timeout=30; rc=$?; } || :
+  (( rc == 4 )) && error "Failed to download $winCatalog , network failure!" && return 1
+  (( rc != 0 )) && error "Failed to download $winCatalog , reason: $rc" && return 1
 
-    out_file="$1"
-    windows_version="$2"
-    enterprise_type="$3"
+  cd "$dir"
 
-    url="https://www.microsoft.com/en-us/evalcenter/download-$windows_version"
+  if ! cabextract "$wFile" > /dev/null; then
+    cd /run
+    error "Failed to extract $wFile!" && return 1
+  fi
 
-    iso_download_page_html="$(curl -sS --location --max-filesize 1M --fail --proto =https --tlsv1.2 --http1.1 -- "$url")" || {
-        handle_curl_error $?
-        return $?
-    }
+  cd /run
 
-    if ! [ "$iso_download_page_html" ]; then
-        # This should only happen if there's been some change to where this download page is located
-        echo_err "Windows enterprise evaluation download page gave us an empty response"
-        return 1
-    fi
+  if [ ! -s "$dir/$xFile" ]; then
+    error "Failed to find $xFile in $wFile!" && return 1
+  fi
 
-    iso_download_links="$(echo "$iso_download_page_html" | grep -o "https://go.microsoft.com/fwlink/p/?LinkID=[0-9]\+&clcid=0x[0-9a-z]\+&culture=en-us&country=US")" || {
-        # This should only happen if there's been some change to the download endpoint web address
-        echo_err "Windows enterprise evaluation download page gave us no download link"
-        return 1
-    }
-
-    # Limit untrusted size for input validation
-    iso_download_links="$(echo "$iso_download_links" | head -c 1024)"
-
-    case "$enterprise_type" in
-        # Select x64 download link
-        "enterprise") iso_download_link=$(echo "$iso_download_links" | head -n 2 | tail -n 1) ;;
-        # Select x64 LTSC download link
-        "ltsc") iso_download_link=$(echo "$iso_download_links" | head -n 4 | tail -n 1) ;;
-        *) iso_download_link="$iso_download_links" ;;
-    esac
-
-    # Follow redirect so proceeding log message is useful
-    # This is a request we make this Fido doesn't
-    # We don't need to set "--max-filesize" here because this is a HEAD request and the output is to /dev/null anyway
-    iso_download_link="$(curl -sS --location --output /dev/null --silent --write-out "%{url_effective}" --head --fail --proto =https --tlsv1.2 --http1.1 -- "$iso_download_link")" || {
-        # This should only happen if the Microsoft servers are down
-        handle_curl_error $?
-        return $?
-    }
-
-    # Limit untrusted size for input validation
-    iso_download_link="$(echo "$iso_download_link" | head -c 1024)"
-
-    #echo_ok "Got latest ISO download link: $iso_download_link"
-
-    # Use highest TLS version for endpoints that support it
-    case "$iso_download_link" in
-        "https://download.microsoft.com"*) tls_version="1.2" ;;
-        *) tls_version="1.3" ;;
-    esac
-
-    # Download ISO
-    scurl_file "$out_file" "$tls_version" "$iso_download_link"
-}
+  local edQuery='//File[Architecture="'${PLATFORM}'"][Edition="'${editionName}'"]'
+
+  echo -e '<Catalog>' > "$dir/$fFile"
+  xmllint --nonet --xpath "${edQuery}" "$dir/$xFile" >> "$dir/$fFile" 2>/dev/null
+  echo -e '</Catalog>'>> "$dir/$fFile"
 
-download_media() {
-    echo_info "Downloading Windows media from official Microsoft servers..."
-
-    media_download_failed_list=""
-
-    for media in $media_list; do
-        case "$media" in
-            "$win7x64_ultimate")
-                echo_info "Downloading Windows 7..."
-                # Source, Google search this (it can be found many places): "dec04cbd352b453e437b2fe9614b67f28f7c0b550d8351827bc1e9ef3f601389" "download.microsoft.com"
-                # This Windows 7 ISO bundles MSU update packages
-                # It's the most up-to-date Windows 7 ISO that Microsoft offers (August 2018 update): https://files.rg-adguard.net/files/cea4210a-3474-a17a-88d4-4b3e10bd9f66
-                # Of particular interest to us is the update that adds support for SHA-256 driver signatures so Qubes Windows Tools installs correctly
-                #
-                # Microsoft purged Windows 7 from all their servers...
-                # More info about this event: https://github.com/pbatard/Fido/issues/64
-                # Luckily, the ISO is still available on the Wayback Machine so get the last copy of it from there
-                # This is still secure because we validate with the checksum from before the purge
-                # The only con then is that web.archive.org is a much slower download source than the Microsoft servers
-                echo_info "Microsoft has unfortunately purged all downloads of Windows 7 from their servers so this identical download is sourced from: web.archive.org"
-                scurl_file "$media" "1.3" "https://web.archive.org/web/20221228154140/https://download.microsoft.com/download/5/1/9/5195A765-3A41-4A72-87D8-200D897CBE21/7601.24214.180801-1700.win7sp1_ldr_escrow_CLIENT_ULTIMATE_x64FRE_en-us.iso"
-                ;;
-            "$win81x64")
-                echo_info "Downloading Windows 8.1..."
-                consumer_download "$media" 8
-                ;;
-            "$win10x64")
-                echo_info "Downloading Windows 10..."
-                consumer_download "$media" 10
-                ;;
-            "$win11x64")
-                echo_info "Downloading Windows 11..."
-                consumer_download "$media" 11
-                ;;
-
-            "$win81x64_enterprise_eval")
-                echo_info "Downloading Windows 8.1 Enterprise Evaluation..."
-                # This download link is "Update 1": https://files.rg-adguard.net/file/166cbcab-1647-53d5-1785-6ef9e22a6500
-                # A more up-to-date "Update 3" enterprise ISO exists but it was only ever distributed by Microsoft through MSDN which means it's impossible to get a Microsoft download link now: https://files.rg-adguard.net/file/549a58f2-7813-3e77-df6c-50609bc6dd7c
-                # win81x64 is "Update 3" but that's isn't an enterprise version (although technically it's possible to modify a few files in the ISO to get any edition)
-                # If you want "Update 3" enterprise though (not from Microsoft servers), then you should still be able to get it from here: https://archive.org/details/en_windows_8.1_enterprise_with_update_x64_dvd_6054382_202110
-                # "Update 1" enterprise also seems to be the ISO used by other projects
-                # Old source, used to be here but Microsoft deleted it: http://technet.microsoft.com/en-us/evalcenter/hh699156.aspx
-                # Source: https://gist.github.com/eyecatchup/11527136b23039a0066f
-                scurl_file "$media" "1.2" "https://download.microsoft.com/download/B/9/9/B999286E-0A47-406D-8B3D-5B5AD7373A4A/9600.17050.WINBLUE_REFRESH.140317-1640_X64FRE_ENTERPRISE_EVAL_EN-US-IR3_CENA_X64FREE_EN-US_DV9.ISO"
-                ;;
-            "$win10x64_enterprise_eval")
-                echo_info "Downloading Windows 10 Enterprise Evaluation..."
-                enterprise_eval_download "$media" windows-10-enterprise enterprise
-                ;;
-            "$win11x64_enterprise_eval")
-                echo_info "Downloading Windows 11 Enterprise Evaluation..."
-                enterprise_eval_download "$media" windows-11-enterprise enterprise
-                ;;
-            "$win10x64_enterprise_ltsc_eval")
-                echo_info "Downloading Windows 10 Enterprise LTSC Evaluation..."
-                enterprise_eval_download "$media" windows-10-enterprise ltsc
-                ;;
-
-            "$win2008r2")
-                echo_info "Downloading Windows Server 2008 R2..."
-                # Old source, used to be here but Microsoft deleted it: https://www.microsoft.com/en-us/download/details.aspx?id=11093
-                # Microsoft took down the original download link provided by that source too but this new one has the same checksum
-                # Source: https://github.com/rapid7/metasploitable3/pull/563
-                scurl_file "$media" "1.2" "https://download.microsoft.com/download/4/1/D/41DEA7E0-B30D-4012-A1E3-F24DC03BA1BB/7601.17514.101119-1850_x64fre_server_eval_en-us-GRMSXEVAL_EN_DVD.iso"
-                ;;
-            "$win2012r2_eval")
-                echo_info "Downloading Windows Server 2012 R2 Evaluation..."
-                enterprise_eval_download "$media" windows-server-2012-r2 server
-                ;;
-            "$win2016_eval")
-                echo_info "Downloading Windows Server 2016 Evaluation..."
-                enterprise_eval_download "$media" windows-server-2016 server
-                ;;
-            "$win2019_eval")
-                echo_info "Downloading Windows Server 2019 Evaluation..."
-                enterprise_eval_download "$media" windows-server-2019 server
-                ;;
-            "$win2022_eval")
-                echo_info "Downloading Windows Server 2022 Evaluation..."
-                enterprise_eval_download "$media" windows-server-2022 server
-                ;;
-        esac || {
-            error_action=$?
-            media_download_failed_list="$media_download_failed_list $media"
-            # Return immediately on a fatal error action
-            if [ "$error_action" = 2 ]; then
-                return
-            fi
-        }
-    done
+  xmllint --nonet --xpath "//File[LanguageCode=\"${culture,,}\"]" "$dir/$fFile" >"$dir/$eFile"
+
+  size=$(stat -c%s "$dir/$eFile")
+  if ((size<20)); then
+    language=$(getLanguage "$lang" "desc")
+    error "the $language language is not supported by this download method!" && return 1
+  fi
+
+  local tag="FilePath"
+  ESD=$(xmllint --nonet --xpath "//$tag" "$dir/$eFile" | sed -E -e "s/<[\/]?$tag>//g")
+
+  if [ -z "$ESD" ]; then
+    error "Failed to find ESD URL in $eFile!" && return 1
+  fi
+
+  tag="Sha1"
+  ESD_SUM=$(xmllint --nonet --xpath "//$tag" "$dir/$eFile" | sed -E -e "s/<[\/]?$tag>//g")
+  tag="Size"
+  ESD_SIZE=$(xmllint --nonet --xpath "//$tag" "$dir/$eFile" | sed -E -e "s/<[\/]?$tag>//g")
+
+  rm -rf "$dir"
+  return 0
 }
 
-verify_media() {
-    # SHA256SUMS file
-    # Some of these Windows ISOs are EOL (e.g. win81x64) so their checksums will always match
-    # For all other Windows ISOs, a new release will make their checksums no longer match
-    #
-    # IMPORTANT: These checksums are not necessarily subject to being updated
-    # Unfortunately, the maintenance burden would be too large and even if I did there would still be some time gap between Microsoft releasing a new ISO and me updating the checksum (also, users would have to update this script)
-    # For these reasons, I've opted for a slightly more manual verification where you have to look up the checksum to see if it's a well-known Windows ISO checksum
-    # Ultimately, you have to trust Microsoft because they could still include a backdoor in the verified ISO (keeping Windows air gapped could help with this)
-    # Community contributions for these checksums are welcome
-    #
-    # Leading backslash is to avoid prepending a newline while maintaining alignment
-    readonly sha256sums="\
-dec04cbd352b453e437b2fe9614b67f28f7c0b550d8351827bc1e9ef3f601389  win7x64-ultimate.iso
-d8333cf427eb3318ff6ab755eb1dd9d433f0e2ae43745312c1cd23e83ca1ce51  win81x64.iso
-# Windows 10 22H2
-a6f470ca6d331eb353b815c043e327a347f594f37ff525f17764738fe812852e  win10x64.iso
-# Windows 11 23H2 v2
-36de5ecb7a0daa58dce68c03b9465a543ed0f5498aa8ae60ab45fb7c8c4ae402  win11x64.iso
-2dedd44c45646c74efc5a028f65336027e14a56f76686a4631cf94ffe37c72f2  win81x64-enterprise-eval.iso
-ef7312733a9f5d7d51cfa04ac497671995674ca5e1058d5164d6028f0938d668  win10x64-enterprise-eval.iso
-ebbc79106715f44f5020f77bd90721b17c5a877cbc15a3535b99155493a1bb3f  win11x64-enterprise-eval.iso
-e4ab2e3535be5748252a8d5d57539a6e59be8d6726345ee10e7afd2cb89fefb5  win10x64-enterprise-ltsc-eval.iso
-30832ad76ccfa4ce48ccb936edefe02079d42fb1da32201bf9e3a880c8ed6312  win2008r2.iso
-6612b5b1f53e845aacdf96e974bb119a3d9b4dcb5b82e65804ab7e534dc7b4d5  win2012r2-eval.iso
-1ce702a578a3cb1ac3d14873980838590f06d5b7101c5daaccbac9d73f1fb50f  win2016-eval.iso
-6dae072e7f78f4ccab74a45341de0d6e2d45c39be25f1f5920a2ab4f51d7bcbb  win2019-eval.iso
-3e4fa6d8507b554856fc9ca6079cc402df11a8b79344871669f0251535255325  win2022-eval.iso"
-
-    # Read sha256sums line-by-line to build known checksum and media lists
-    # Only use shell builtins for better security and stability
-    # Don't use a for loop because IFS cannot temporarily be set using that
-    while IFS="$(printf '\n')" read -r line; do
-        # Ignore comments and empty lines
-        case "$line" in
-            "#"* | "") continue ;;
-        esac
-
-        # Read first and second words of line
-        IFS=' ' read -r known_checksum known_media _ << EOF
-$line
-EOF
-
-        known_checksum_list="$known_checksum_list $known_checksum"
-        known_media_list="$known_media_list $known_media"
-    done << EOF
-$sha256sums
-EOF
-
-    media_verification_failed_list=""
-    checksum_verification_failed_list=""
-
-    for media in $media_list; do
-        # Scan for unverified media files
-        if ! [ -f "${media}${unverified_ext}" ]; then
-            continue
-        fi
-
-        if [ "$verify_media_message_shown" != "true" ]; then
-            echo_info "Verifying integrity..."
-            verify_media_message_shown="true"
-        fi
-
-        checksum_line="$(sha256sum "${media}${unverified_ext}")"
-        # Get first word of checksum line
-        IFS=' ' read -r checksum _ << EOF
-$checksum_line
-EOF
-
-        # Sanity check: Assert correct size of SHA-256 checksum
-        if [ ${#checksum} != 64 ]; then
-            echo_err "Failed SHA-256 sanity check! Exiting..."
-            exit 2
-        fi
-
-        known_checksum_list_iterator="$known_checksum_list"
-
-        # Search known media and checksum lists for the current media
-        for known_media in $known_media_list; do
-            IFS=' ' read -r known_checksum known_checksum_list_iterator << EOF
-$known_checksum_list_iterator
-EOF
-
-            if [ "$media" = "$known_media" ]; then
-                break
-            fi
-        done
-
-        # Verify current media integrity
-        if [ "$checksum" = "$known_checksum" ]; then
-            echo "$media: OK"
-            mv "${media}${unverified_ext}" "$media"
-        else
-            echo "$media: UNVERIFIED"
-            media_verification_failed_list="$media_verification_failed_list $media"
-            checksum_verification_failed_list="$checksum_verification_failed_list $checksum"
-        fi
-
-        # Reset known checksum list iterator so we can iterate on it again for the next media
-        known_checksum_list_iterator="$known_checksum_list"
-    done
+verifyFile() {
+
+  local iso="$1"
+  local size="$2"
+  local total="$3"
+  local check="$4"
+
+  if [ -n "$size" ] && [[ "$total" != "$size" ]] && [[ "$size" != "0" ]]; then
+    warn "The downloaded file has an unexpected size: $total bytes, while expected value was: $size bytes. Please report this at $SUPPORT/issues"
+  fi
+
+  local hash=""
+  local algo="SHA256"
+
+  [ -z "$check" ] && return 0
+  [[ "$VERIFY" != [Yy1]* ]] && return 0
+  [[ "${#check}" == "40" ]] && algo="SHA1"
+
+  local msg="Verifying downloaded ISO..."
+  info "$msg" && html "$msg"
+
+  if [[ "${algo,,}" != "sha256" ]]; then
+    hash=$(sha1sum "$iso" | cut -f1 -d' ')
+  else
+    hash=$(sha256sum "$iso" | cut -f1 -d' ')
+  fi
+
+  if [[ "$hash" == "$check" ]]; then
+    info "Succesfully verified ISO!" && return 0
+  fi
+
+  error "The downloaded file has an invalid $algo checksum: $hash , while expected value was: $check. Please report this at $SUPPORT/issues"
+
+  rm -f "$iso"
+  return 1
 }
 
-ending_summary() {
-    echo "" >&2
+downloadFile() {
+
+  local iso="$1"
+  local url="$2"
+  local sum="$3"
+  local size="$4"
+  local lang="$5"
+  local desc="$6"
+  local rc total progress domain dots
+
+  rm -f "$iso"
+
+  # Check if running with interactive TTY or redirected to docker log
+  if [ -t 1 ]; then
+    progress="--progress=bar:noscroll"
+  else
+    progress="--progress=dot:giga"
+  fi
 
-    if [ "$media_download_failed_list" ]; then
-        for media in $media_download_failed_list; do
-            media_download_failed_argument_list="$media_download_failed_argument_list ${media%%.iso}"
-        done
+  local msg="Downloading $desc..."
+  html "$msg"
+
+  domain=$(echo "$url" | awk -F/ '{print $3}')
+  dots=$(echo "$domain" | tr -cd '.' | wc -c)
+  (( dots > 1 )) && domain=$(expr "$domain" : '.*\.\(.*\..*\)')
+
+  if [ -n "$domain" ] && [[ "${domain,,}" != *"microsoft.com" ]]; then
+    msg="Downloading $desc from $domain..."
+  fi
+
+  info "$msg"
+  /run/progress.sh "$iso" "$size" "Downloading $desc ([P])..." &
+
+  { wget "$url" -O "$iso" -q --timeout=30 --show-progress "$progress"; rc=$?; } || :
+
+  fKill "progress.sh"
+
+  if (( rc == 0 )) && [ -f "$iso" ]; then
+    total=$(stat -c%s "$iso")
+    if [ "$total" -gt 100000000 ]; then
+      ! verifyFile "$iso" "$size" "$total" "$sum" && return 1
+      html "Download finished successfully..." && return 0
     fi
+  fi
 
-    # Exit codes
-    # 0: Success
-    # 1: Argument parsing error
-    # 2: Runtime error (see error message for more info)
-    # 3: One or more downloads failed
-    # 4: One or more verifications failed
-    # 5: At least one download and one verification failed (when more than one media is specified)
+  if (( rc != 4 )); then
+    error "Failed to download $url , reason: $rc"
+  else
+    error "Failed to download $url , network failure!"
+  fi
 
-    exit_code=0
+  rm -f "$iso"
+  return 1
+}
 
-    # Determine exit code
-    if [ "$media_download_failed_list" ] && [ "$media_verification_failed_list" ]; then
-        exit_code=5
-    else
-        if [ "$media_download_failed_list" ]; then
-            exit_code=3
-        elif [ "$media_verification_failed_list" ]; then
-            exit_code=4
-        fi
+downloadImage() {
+
+  local iso="$1"
+  local version="$2"
+  local lang="$3"
+  local tried="n"
+  local url sum size base desc language
+
+  if [[ "${version,,}" == "http"* ]]; then
+    base=$(basename "$iso")
+    desc=$(fromFile "$base")
+    downloadFile "$iso" "$version" "" "" "" "$desc" && return 0
+    return 1
+  fi
+
+  if ! validVersion "$version" "en"; then
+    error "Invalid VERSION specified, value \"$version\" is not recognized!" && return 1
+  fi
+
+  desc=$(printVersion "$version" "")
+
+  if [[ "${lang,,}" != "en" ]] && [[ "${lang,,}" != "en-"* ]]; then
+    language=$(getLanguage "$lang" "desc")
+    if ! validVersion "$version" "$lang"; then
+      desc=$(printEdition "$version" "$desc")
+      error "The $language language version of $desc is not available, please switch to English." && return 1
     fi
+    desc="$desc in $language"
+  fi
+
+  if isMido "$version" "$lang"; then
+    tried="y"
+    if getWindows "$version" "$lang" "$desc"; then
+      size=$(getMido "$version" "$lang" "size" )
+      sum=$(getMido "$version" "$lang" "sum")
+      downloadFile "$iso" "$MIDO_URL" "$sum" "$size" "$lang" "$desc" && return 0
+    fi
+  fi
 
-    trap -- - EXIT
+  switchEdition "$version"
 
-    if [ "$exit_code" = 0 ]; then
-        echo_ok "Successfully downloaded Windows image!"
-    else
-        echo_ok "Finished! Please see the above errors with information"
-        exit "$exit_code"
+  if isESD "$version" "$lang"; then
+
+    if [[ "$tried" != "n" ]]; then
+      info "Failed to download $desc, will try a diferent method now..."
     fi
-}
 
-# https://unix.stackexchange.com/questions/752570/why-does-trap-passthough-zero-instead-of-the-signal-the-process-was-killed-wit
-handle_exit() {
-    exit_code=$?
-    signal="$1"
+    tried="y"
 
-    if [ "$exit_code" != 0 ] || [ "$signal" ]; then
-        echo "" >&2
-        echo_err "Mido was exited abruptly!"
+    if getESD "$TMP/esd" "$version" "$lang" "$desc"; then
+      ISO="${ISO%.*}.esd"
+      downloadFile "$ISO" "$ESD" "$ESD_SUM" "$ESD_SIZE" "$lang" "$desc" && return 0
+      ISO="$iso"
     fi
 
-    if [ "$exit_code" != 0 ]; then
-        trap -- - EXIT
-        exit "$exit_code"
-    elif [ "$signal" ]; then
-        trap -- - "$signal"
-        kill -s "$signal" -- $$
+  fi
+
+  for ((i=1;i<=MIRRORS;i++)); do
+
+    url=$(getLink "$i" "$version" "$lang")
+
+    if [ -n "$url" ]; then
+      if [[ "$tried" != "n" ]]; then
+        info "Failed to download $desc, will try another mirror now..."
+      fi
+      tried="y"
+      size=$(getSize "$i" "$version" "$lang")
+      sum=$(getHash "$i" "$version" "$lang")
+      downloadFile "$iso" "$url" "$sum" "$size" "$lang" "$desc" && return 0
     fi
+
+  done
+
+  return 1
 }
 
-# Enable exiting on error
-#
-# Disable shell globbing
-# This isn't necessary given that all unquoted variables (e.g. for determining word count) are set directly by us but it's just a precaution
-set -ef
-
-# IFS defaults to many different kinds of whitespace but we only care about space
-# Note: This means that ISO filenames cannot contain spaces but that's a bad idea anyway
-IFS=' '
-
-parse_args "$@"
-
-# POSIX sh doesn't include signals in its EXIT trap so do it ourselves
-signo=1
-while true; do
-    # "kill" is a shell builtin
-    # shellcheck disable=SC2064
-    case "$(kill -l "$signo" 2> /dev/null)" in
-        # Trap on all catchable terminating signals as defined by POSIX
-        # Stop (i.e. suspend) signals (like Ctrl + Z or TSTP) are fine because they can be resumed
-        # Most signals result in termination so this way is easiest (Linux signal(7) only adds more terminating signals)
-        #
-        # https://pubs.opengroup.org/onlinepubs/009695399/basedefs/signal.h.html
-        # https://unix.stackexchange.com/a/490816
-        # Signal WINCH was recently added to POSIX: https://austingroupbugs.net/view.php?id=249
-        CHLD | CONT | URG | WINCH | KILL | STOP | TSTP | TTIN | TTOU) ;;
-        *) trap "handle_exit $signo" "$signo" 2> /dev/null || break ;;
-    esac
-
-    signo=$((signo + 1))
-done
-trap handle_exit EXIT
-
-download_media
-verify_media
-ending_summary
+return 0

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików