Private/New-GarminFunctions.ps1

New-Variable -Name GarminLoginSession -Scope Global -Visibility Public -Force

function ConvertTo-UrlEncodedFormBody {
    param(
        [Parameter(Mandatory = $true)]
        [hashtable]$Data
    )

    $pairs = foreach ($key in $Data.Keys) {
        $encodedKey = [System.Uri]::EscapeDataString([string]$key)
        $encodedValue = [System.Uri]::EscapeDataString([string]$Data[$key])
        "{0}={1}" -f $encodedKey, $encodedValue
    }

    return ($pairs -join '&')
}

function Get-HtmlInputValue {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Html,

        [Parameter(Mandatory = $true)]
        [string]$InputName
    )

    $patternNameBeforeValue = 'name=(?:"|\''){0}(?:"|\'')[^>]*?value=(?:"([^"]*)"|\''([^\'']*)\'')' -f [regex]::Escape($InputName)
    $match = [regex]::Match($Html, $patternNameBeforeValue, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
    if (-not $match.Success) {
        $patternValueBeforeName = 'value=(?:"([^"]*)"|\''([^\'']*)\'')[^>]*?name=(?:"|\''){0}(?:"|\'')' -f [regex]::Escape($InputName)
        $match = [regex]::Match($Html, $patternValueBeforeName, [System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
    }
    if ($match.Success) {
        if (-not [string]::IsNullOrEmpty($match.Groups[1].Value)) {
            return $match.Groups[1].Value
        }

        return $match.Groups[2].Value
    }

    return $null
}

function Get-HtmlHiddenInputValues {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Html
    )

    $result = @{}
    $inputMatches = [regex]::Matches($Html, '(?is)<input\b[^>]*>')

    foreach ($inputMatch in $inputMatches) {
        $inputTag = $inputMatch.Value

        $typeMatch = [regex]::Match($inputTag, '(?i)\btype\s*=\s*(?:"([^"]*)"|\''([^\'']*)\''|([^\s>]+))')
        if ($typeMatch.Success) {
            $typeValue = $typeMatch.Groups[1].Value
            if ([string]::IsNullOrWhiteSpace($typeValue)) {
                $typeValue = $typeMatch.Groups[2].Value
            }
            if ([string]::IsNullOrWhiteSpace($typeValue)) {
                $typeValue = $typeMatch.Groups[3].Value
            }

            if (-not [string]::Equals($typeValue, 'hidden', [System.StringComparison]::OrdinalIgnoreCase)) {
                continue
            }
        }
        else {
            continue
        }

        $nameMatch = [regex]::Match($inputTag, '(?i)\bname\s*=\s*(?:"([^"]*)"|\''([^\'']*)\''|([^\s>]+))')
        if (-not $nameMatch.Success) {
            continue
        }

        $nameValue = $nameMatch.Groups[1].Value
        if ([string]::IsNullOrWhiteSpace($nameValue)) {
            $nameValue = $nameMatch.Groups[2].Value
        }
        if ([string]::IsNullOrWhiteSpace($nameValue)) {
            $nameValue = $nameMatch.Groups[3].Value
        }

        if ([string]::IsNullOrWhiteSpace($nameValue)) {
            continue
        }

        $valueMatch = [regex]::Match($inputTag, '(?i)\bvalue\s*=\s*(?:"([^"]*)"|\''([^\'']*)\''|([^\s>]+))')
        $value = ''
        if ($valueMatch.Success) {
            $value = $valueMatch.Groups[1].Value
            if ([string]::IsNullOrWhiteSpace($value) -and $valueMatch.Groups[2].Success) {
                $value = $valueMatch.Groups[2].Value
            }
            if ([string]::IsNullOrWhiteSpace($value) -and $valueMatch.Groups[3].Success) {
                $value = $valueMatch.Groups[3].Value
            }
        }

        $result[$nameValue] = $value
    }

    return $result
}

function Get-MfaActionUri {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Html,

        [Parameter(Mandatory = $true)]
        [string]$FallbackBaseUri
    )

    $formActionMatch = [regex]::Match(
        $Html,
        'action=(?:"([^"]*(?:mfa|verify|code)[^"]*)"|\''([^\'']*(?:mfa|verify|code)[^\'']*)\'')',
        [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
    )

    if ($formActionMatch.Success) {
        $actionUri = $formActionMatch.Groups[1].Value
        if ([string]::IsNullOrWhiteSpace($actionUri)) {
            $actionUri = $formActionMatch.Groups[2].Value
        }

        if ([System.Uri]::IsWellFormedUriString($actionUri, [System.UriKind]::Absolute)) {
            return $actionUri
        }

        return ([System.Uri]::new([System.Uri]$FallbackBaseUri, $actionUri)).AbsoluteUri
    }

    return 'https://sso.garmin.com/sso/verifyMFA/loginEnterMfaCode'
}

function Get-MfaFieldName {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Html
    )

    $knownNames = @(
        'mfaCode',
        'mfa-code',
        'verificationCode',
        'verification-code',
        'securityCode',
        'security-code',
        'code'
    )

    foreach ($name in $knownNames) {
        if ($Html -match ('name=(?:"|\''){0}(?:"|\'')' -f [regex]::Escape($name))) {
            return $name
        }
    }

    return 'mfaCode'
}



function Publish-TCXToGarmin {
    param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateScript({ [IO.Path]::GetExtension($_) -eq '.tcx' })]
        [string]$FilePath
    )

    try {
        if ($null -ne $GarminLoginSession -and $GarminLoginSession -is [Microsoft.PowerShell.Commands.WebRequestSession]) {
            $headers = @{
                'method'          = 'POST'
                'authority'       = 'connect.garmin.com'
                'scheme'          = 'https'
                'path'            = '/modern/proxy/upload-service/upload/.tcx'
                'nk'              = 'NT'
                'dnt'             = '1'
                'x-app-ver'       = '4.33.2.0'
                'x-lang'          = 'en-US'
                'user-agent'      = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/84.0.4147.125 Safari/537.36 Edg/84.0.522.59'
                'accept'          = '*/*'
                'origin'          = 'https://connect.garmin.com'
                'sec-fetch-site'  = 'same-origin'
                'sec-fetch-mode'  = 'cors'
                'sec-fetch-dest'  = 'empty'
                'referer'         = 'https://connect.garmin.com/modern/import-data'
                'accept-encoding' = 'gzip, deflate, br'
                'accept-language' = 'en-US,en;q=0.9'
            }

            $garminUploadUri = 'https://connect.garmin.com/modern/proxy/upload-service/upload/.tcx'
            $uploadStatus = Invoke-RestMethod -Uri $garminUploadUri -Method POST -Form @{ file = (Get-Item -Path $FilePath) } -WebSession $GarminLoginSession -Headers $headers
            if ($null -ne $uploadStatus -and $uploadStatus.detailedImportResult.successes.Count -ge 1 -and [string]::IsNullOrEmpty($uploadStatus.detailedImportResult.successes[0].messages)) {
                Write-Verbose -Message 'Upload successful!' -Verbose
                return $true
            }

            return $false
        }

        Write-Error -Message 'Invalid login session! Please re-run New-GarminLoginSession Cmdlet and try again.' -ErrorAction Stop
    }
    catch {
        Write-Warning -Message 'Upload failed!'
        Write-Error -Exception $_.Exception -Message $_.Exception.Message -ErrorAction Stop
    }
}

function Get-HtmlFormActionUri {
    param(
        [Parameter(Mandatory = $true)]
        [string]$Html,

        [Parameter(Mandatory = $true)]
        [string]$FallbackBaseUri
    )

    $actionMatch = [regex]::Match(
        $Html,
        '<form[^>]*id=(?:"|\'')login-form(?:"|\'')[^>]*action=(?:"([^"]+)"|\''([^\'']+)\'')',
        [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
    )

    if (-not $actionMatch.Success) {
        $actionMatch = [regex]::Match(
            $Html,
            '<form[^>]*action=(?:"([^"]+)"|\''([^\'']+)\'')',
            [System.Text.RegularExpressions.RegexOptions]::IgnoreCase
        )
    }

    if ($actionMatch.Success) {
        $actionUri = $actionMatch.Groups[1].Value
        if ([string]::IsNullOrWhiteSpace($actionUri)) {
            $actionUri = $actionMatch.Groups[2].Value
        }

        if ([System.Uri]::IsWellFormedUriString($actionUri, [System.UriKind]::Absolute)) {
            return $actionUri
        }

        return ([System.Uri]::new([System.Uri]$FallbackBaseUri, $actionUri)).AbsoluteUri
    }

    return $FallbackBaseUri
}