PrtgAPI.GoPrtg.ps1

function Get-GoPrtgServer
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [string]
        $Server
    )

    if(Get-Command -Name __goPrtgGetServers -ErrorAction SilentlyContinue)
    {
        $serversText = @(__goPrtgGetServers)|foreach {
            "`"[ ]`",$_"
        }
        
        $servers = $serversText | ConvertFrom-Csv -Header "[!]","Server","Alias","UserName","PassHash"

        $client = Get-PrtgClient

        foreach($s in $servers)
        {
            if($s.Alias -eq "")
            {
                $s.Alias = $null
            }

            if($s.Server -eq $client.Server -and $s.UserName -eq $client.UserName)
            {
                $s."[!]" = "[*]"
            }
        }

        if($Server)
        {
            $servers = @($servers | ForEach-Object {
                if($_.Server -like $Server)
                {
                    $_
                }
                else
                {
                    if($_.Alias -eq $Server)
                    {
                        $_
                    }
                }
            })
        }

        $resp = $servers | Select-Object @{name="[!]";expression={$_."[!]"}},Server,Alias,UserName

        return $resp

        $response = @()

        foreach($server in $servers)
        {
            $username = $server.UserName
            $alias = $servers.Alias

            $obj = New-Object PSObject
            $obj | Add-Member NoteProperty Server $server.Server
            $obj | Add-Member NoteProperty Alias $alias
            $obj | Add-Member NoteProperty UserName $username

            $response += $obj
        }

        return $response
    }
    else
    {
        Write-ColorOutput "`nGoPrtg is not installed. Run Install-GoPrtgServer first to install a GoPrtg server.`n" -ForegroundColor Red
    }
}

function Connect-GoPrtgServer
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [string]
        $Server
    )

    if(Get-Command __goPrtgGetServers -ErrorAction SilentlyContinue)
    {
        $servers = @(GetServers)

        if(!$Server)
        {
            $connectTo = $servers | Select-Object -First 1

            ConnectToGoPrtgServer $connectTo
        }
        else
        {
            $matches = @($servers | Where-Object {$_.Server -like $Server -or $_.Alias -eq $Server})

            if($matches.Count -eq 1)
            {
                ConnectToGoPrtgServer $matches
            }
            elseif($matches.Count -gt 1)
            {
                Write-ColorOutput "`nAmbiguous server specified. The following servers matched the specified server name or alias" -ForegroundColor Red

                Get-GoPrtgServer | Where-Object {$_.Server -like $Server -or $_.Alias -eq $Server}
            }
            else
            {
                Write-ColorOutput "`nCould not find server that matches name or alias '$Server'`n" -ForegroundColor Red
            }
        }    
    }
    else
    {
        Write-ColorOutput "`nNo GoPrtg servers are installed. Please install a server first using Install-GoPrtgServer`n" -ForegroundColor Red
    }
}

function ConnectToGoPrtgServer($server)
{
    $client = Get-PrtgClient

    if($server.Server -eq $client.Server -and $server.UserName -eq $client.UserName)
    {
        Write-ColorOutput "`nAlready connected to $($server.Server) as $($server.UserName)`n" -ForegroundColor Yellow
    }
    else
    {
        $credential = New-Object System.Management.Automation.PSCredential -ArgumentList $server.UserName, (ConvertTo-SecureString $server.PassHash)
        Connect-PrtgServer $server.Server $credential -PassHash -Force

        Write-ColorOutput "`nConnected to $($server.Server) as $($server.UserName)`n" -ForegroundColor Green
    }
}

function Set-GoPrtgAlias
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSUseShouldProcessForStateChangingFunctions", "", Scope="Function")]
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [string]
        $Alias
    )

    if(!(Test-Path $Profile))
    {
        throw "No GoPrtg servers are installed. To install a GoPrtg server, run Install-GoPrtgServer."
    }

    $contents = Get-Content $Profile

    $functionStart = GetGoPrtgStart $contents
    $functionEnd = GetGoPrtgEnd $contents $functionStart

    $functionExists = Get-Command __goPrtgGetServers -ErrorAction SilentlyContinue

    ValidateGoPrtgBlock $functionStart $functionEnd $functionExists

    if($functionExists)
    {
        if(!(Get-PrtgClient))
        {
            throw "You are not connected to a PRTG Server. Please connect first using GoPrtg [<server>]."
        }

        $client = Get-PrtgClient

        $servers = @(GetServers)

        if($servers | Where-Object Alias -EQ $Alias)
        {
            throw "Cannot set alias for server '$($client.Server)': a record with alias '$Alias' already exists. For more information see Get-GoPrtgServer."
        }

        #todo: when we update the aliases in the file, we need to also update them in memory!! is that even happening?

        $targetServer = @($servers | Where-Object {$_.Server -eq $client.Server -and $_.UserName -eq $client.UserName })

        if($targetServer.Count -eq 0)
        {
            throw "Server '$($client.Server)' is not a valid GoPrtg server. To install this server, run Install-GoPrtgServer [<alias>]"
        }
        elseif($targetServer.Count -eq 1)
        {
            if(!$Alias -and $targetServer.Alias -ne $null)
            {
                if(($servers | Where-Object {$_.Server -eq $client.Server}).Count -gt 1)
                {
                    throw "Cannot remove alias of server: multiple entries for server '$($client.Server)' are stored within GoPrtg. To remove this alias uninstall all other entries for this server. For more information see Get-GoPrtgServer."
                }
            }

            SetPrtgGoAliasInternal $contents $targetServer $functionStart $functionEnd $Alias
        }
        elseif($targetServer.Count -gt 1)
        {
            throw "A critical error has occurred: more than one server exists with the same server and username. Please remove the duplicate from your $Profile manually."
        }
    }
    else
    {
        throw "GoPrtg is not installed. Run Install-GoPrtgServer <alias> to install a server with the specified alias."
    }
}

function SetPrtgGoAliasInternal($contents, $server, $functionStart, $functionEnd, $Alias)
{
    $new = @()

    for($i = $functionStart + 3; $i -le $functionEnd -3; $i++)
    {
        $line = $contents[$i]

        $compare = $null

        if($null -ne $server.Alias -and $server.Alias -ne "")
        {
            $compare = " `"```````"$($server.Server)```````",```````"$($server.Alias)```````",```````"$($server.UserName)```````",```````"$($server.PassHash)```````"`"*"
        }
        else
        {
            $compare = " `"```````"$($server.Server)```````",,```````"$($server.UserName)```````",```````"$($server.PassHash)```````"`"*"
        }

        if($line -like $compare)
        {
            $newLine = $null

            if($null -ne $Alias -and $Alias -ne "")
            {
                $newLine = " `"```"$($server.Server)```",```"$Alias```",```"$($server.UserName)```",```"$($server.PassHash)```"`""
            }
            else
            {
                $newLine = " `"```"$($server.Server)```",,```"$($server.UserName)```",```"$($server.PassHash)```"`""
            }

            if($line.EndsWith(","))
            {
                $newLine += ","
            }

            $new += $newLine
        }
        else
        {
            $new += $line
        }        
    }

    $final = AddGoPrtgFunctionHeaderAndFooter $contents $new $functionStart $functionEnd

    UpdateGoPrtgFunctionBody $final $contents $functionStart $functionEnd

    .([ScriptBlock]::Create(($final -replace "function ","function global:")))
}

#region Install-GoPrtgServer

function Install-GoPrtgServer
{
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false, Position = 0)]
        [string]
        $Alias
    )

    if(!(Get-PrtgClient))
    {
        throw "You are not connected to a PRTG Server. Please connect first using Connect-PrtgServer."
    }

    $new = $false

    if(!(Test-Path $Profile))
    {
        New-Item $Profile -Type File -Force
        $new = $true
    }

    #todo: what is the force parameter meant to do? overwrite? if so we need to update some of the existing exception messages

    $newContents = GetNewContents $Alias
    $profileContent = GetProfileContents $newContents

    if($new)
    {
        Add-Content $Profile $profileContent
    }
    else
    {
        Set-Content $Profile $profileContent
    }

    .([ScriptBlock]::Create(($newContents.Func -replace "function ","function global:")))
}

function GetNewContents($alias)
{
    $contents = Get-Content $Profile

    $functionStart = GetGoPrtgStart $contents
    $functionEnd = GetGoPrtgEnd $contents $functionStart

    $functionExists = Get-Command __goPrtgGetServers -ErrorAction SilentlyContinue

    ValidateGoPrtgBlock $functionStart $functionEnd $functionExists

    # If the function didn't exist but the header and footer did, an exception would have been thrown above.
    # We can infer then that if we've reached this stage and the function doesn't exist, the header and footer don't exist either

    if($functionExists)
    {
        $servers = @(GetServers)

        CheckServerAgainstExistingRecords $servers $alias

        $pre = GetPre $contents $functionStart

        $post = GetPost $contents $functionEnd

        $new = GetNew $servers $alias
        
        $obj = ContentsObj
        $obj.Pre = $pre
        $obj.Func = $new
        $obj.Post = $post

        return $obj
    }
    else
    {
        $new = GetNew $null $alias

        $obj = ContentsObj
        $obj.Pre = $contents
        $obj.Func = $new
        $obj.Post = $null

        return $obj
    }
}

function ValidateGoPrtgBlock
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSPossibleIncorrectComparisonWithNull", "", Scope="Function")]
    param($functionStart, $functionEnd, $functionExists)

    if($functionStart -eq $null -and $functionEnd -ne $null)
    {
        throw "GoPrtg Servers start line '########################### Start GoPrtg Servers ###########################' has been removed from PowerShell profile. Please reinstate line or remove all lines pertaining to GoPrtg from your profile."
    }

    if($functionStart -ne $null -and $functionEnd -eq $null)
    {
        throw "GoPrtg Servers end line '############################ End GoPrtg Servers ############################' has been removed from PowerShell profile. Please reinstate line or remove all lines pertaining to GoPrtg from your profile."
    }

    if($functionStart -ne $null -and $functionEnd -ne $null)
    {
        if(!$functionExists)
        {
            throw "GoPrtg header and footer are present in PowerShell profile, however __goPrtgGetServers function was not loaded into the current session. Please verify the function has not been corrupted or remove the GoPrtg header and footer and re-run Install-GoPrtgServer."
        }
    }
}

function GetServers
{
    $servers = __goPrtgGetServers | ConvertFrom-Csv -Header "Server","Alias","UserName","PassHash"

    foreach($server in $servers)
    {
        if($server.Alias -eq "")
        {
            $server.Alias = $null
        }
    }

    return $servers
}

function GetProfileContents($newContents)
{
    $funcBody = ""

    if($null -ne $newContents.Pre)
    {
        $funcBody += [string]::Join("`r`n", $newContents.Pre) + "`r`n"
    }

    # Due to the way Powershell's Command Discovery works, PowerShell will detect match the Get-GoPrtg alias to the command GoPrtg
    # before detecting GoPrtg is in fact a valid alias all by itself. By forcing the module to import, PowerShell will correctly
    # identify that GoPrtg is the intended command.

    $funcBody += "########################### Start GoPrtg Servers ###########################`r`nImport-Module PrtgAPI`r`n" + 
                 [string]::Join("`r`n", $newContents.Func) +
                 "`r`n`r`n############################ End GoPrtg Servers ############################"                

    if($null -ne $newContents.Post)
    {
        $funcBody += "`r`n" + [string]::Join("`r`n", $newContents.Post)
    }

    return $funcBody
}

function CheckServerAgainstExistingRecords($servers, $alias)
{
    $client = Get-PrtgClient

    if($servers | Where-Object {$_.Server -eq $client.Server })
    {
        $record = $servers | Where-Object {$_.Server -eq $client.Server } | Select-Object -first 1

        if($null -ne $record.Alias)
        {
            if($record.UserName -eq $client.UserName)
            {
                throw "Cannot add server '$($client.Server)': a record for user '$($client.UserName)' already exists. To update the alias of this record use Set-GoPrtgAlias. To reinstall this record, first uninstall with Uninstall-GoPrtgServer and then re-run Install-GoPrtgServer."
            }

            if(!$alias)
            {
                throw "Cannot add server '$($client.Server)': an alias must be specified to differentiate this connection from an existing connection with the same server address."
            }
        }
        else
        {
            if($record.UserName -eq $client.UserName)
            {
                throw "Cannot add server '$($client.Server)': a record for user '$($client.UserName)' already exists."
            }

            throw "Cannot add server '$($client.Server)': a record for server already exists without an alias. Please update the alias of this record with Set-GoPrtgAlias and try again."
        }
    }
    elseif($servers | Where-Object {$_.Alias -eq $alias})
    {
        throw "Cannot add server '$($client.Server)' with alias '$alias': a record for this alias already exists. For more information see 'Get-GoPrtgServer $alias'"
    }
}

function GetGoPrtgStart($contents)
{
    for($i = 0; $i -lt $contents.Length; $i++)
    {
        if($contents[$i] -eq "########################### Start GoPrtg Servers ###########################")
        {
            return $i
        }
    }
    
    return $null
}

function GetGoPrtgEnd($contents)
{
    if($functionStart -ne -1)
    {
        for($i = $functionStart + 1; $i -lt $contents.Length; $i++)
        {
            if($contents[$i] -eq "############################ End GoPrtg Servers ############################")
            {
                return $i
            }
        }
    }

    return $null
}

function GetPre($contents, $functionStart)
{
    $index = 0

    $pre = $contents | ForEach-Object {
        if($index -lt $functionStart)
        {
            $_
        }

        $index++
    }

    return $pre
}

function GetPost($contents, $functionEnd)
{
    $index = 0

    $post = $contents | ForEach-Object {
        if($index -gt $functionEnd)
        {
            return $_
        }

        $index++
    }

    return $post
}

function GetNew
{
    [Diagnostics.CodeAnalysis.SuppressMessageAttribute("PSAvoidUsingConvertToSecureStringWithPlainText", "", Scope="Function")]
    param($servers, $alias)

    $client = Get-PrtgClient

    $secureString = ConvertTo-SecureString $client.PassHash -AsPlainText -Force
    $encryptedString = ConvertFrom-SecureString $secureString
    #$encryptedString = $client.PassHash

    if(!$servers)
    {
        $servers = @()
    }

    $newRecord = SerializeRecord $client.Server $alias $client.UserName $encryptedString

    $newRecord = $newRecord -replace "`"```"","`"" -replace "```"`"","`"" -replace "```"","`""

    $r = $newRecord | ConvertFrom-Csv -Header "Server","Alias","UserName","PassHash"
    
    if($r.Alias -eq "")
    {
        $r.Alias = $null
    }

    <#$obj = New-Object PSObject
    $obj | Add-Member NoteProperty Server $client.Server
    $obj | Add-Member NoteProperty Alias $alias
    $obj | Add-Member NoteProperty UserName $client.UserName
    $obj | Add-Member NoteProperty PassHash $encryptedString#>


    $servers += $r

    $newContents = @()

    $newContents += "function __goPrtgGetServers {@("

    $index = 0
    $count = $servers.Count

    foreach($s in $servers)
    {
        $toAdd = SerializeRecord $s.Server $s.Alias $s.UserName $s.PassHash

        if($count -gt 1 -and ($index -lt $count - 1))
        {
            $toAdd += ","
        }

        $newContents += $toAdd

        $index++
    }

    $newContents += ")}"

    return $newContents
}

function SerializeRecord($server, $alias, $username, $passhash)
{
    $record = $null

    if($alias)
    {
        $record = " `"```"$server```",```"$alias```",```"$username```",```"$passhash```"`""
    }
    else
    {
        $record = " `"```"$server```",,```"$username```",```"$passhash```"`""
    }

    return $record
}

function ContentsObj
{
    $obj = @{
        "Pre" = @();
        "Func" = @();
        "Post" = @()
    }

    return $obj
}

#endregion Install-GoPrtgServer
#region Uninstall-GoPrtgServer

function Uninstall-GoPrtgServer([string]$Server, [switch]$Force)
{
    if(!(Test-Path $Profile))
    {
        throw "No GoPrtg servers are installed. To install a GoPrtg server, run Install-GoPrtgServer."
    }

    $contents = Get-Content $Profile

    $functionStart = GetGoPrtgStart $contents
    $functionEnd = GetGoPrtgEnd $contents $functionStart

    $functionExists = Get-Command __goPrtgGetServers -ErrorAction SilentlyContinue

    ValidateGoPrtgBlock $functionStart $functionEnd $functionExists

    if($functionExists)
    {
        $servers = @(GetServers)

        $Server = SetServerWildcardIfMissing $Server $servers $Force
        $matches = @(GetMatches $Server $servers)

        $filtered = GetFiltered $contents $matches $functionStart $functionEnd

        if($filtered)
        {
            $filtered = AddGoPrtgFunctionHeaderAndFooter $contents $filtered $functionStart $functionEnd
        }

        UpdateGoPrtgFunctionBody $filtered $contents $functionStart $functionEnd

        if($null -ne $filtered)
        {
            .([ScriptBlock]::Create(($filtered -replace "function ","function global:")))
        }
        else
        {
            Remove-Item Function:\__goPrtgGetServers
        }
    }
    else
    {
        throw "No GoPrtg servers are installed. To install a GoPrtg server, run Install-GoPrtgServer."
    }
}

function SetServerWildcardIfMissing($server, $servers, $force)
{
    if($force)
    {
        $server = "*"
    }
    else
    {
        if(!$server)
        {
            if($servers.Count -gt 1)
            {
                throw "Cannot remove servers; server name or alias must be specified when multiple entries exist. To remove all servers, specify -Force"
            }
            else
            {
                $server = "*"
            }
        }
    }

    return $server
}

function GetMatches($server, $servers)
{
    $matches = @()

    foreach($s in $servers)
    {
        if($s.Server -like $server)
        {
            $matches += $s
        }
        else
        {
            if($null -ne $s.Alias)
            {
                if($s.Alias -like $server) # the alias
                {
                    $matches += $s
                }
            }
        }
    }

    if(!$matches)
    {
        throw "'$server' is not a valid server name or alias. To view all saved servers, run Get-GoPrtgServer"
    }

    return $matches
}

function GetFiltered($contents, $matches, $functionStart, $functionEnd)
{
    $filtered = @()

    for($i = $functionStart + 3; $i -le $functionEnd - 3; $i++)
    {
        $line = $contents[$i]

        $include = $true

        foreach($s in $matches)
        {
            $toRemove = $null

            if($null -ne $s.Alias)
            {
                $toRemove = " `"```````"$($s.Server)```````",```````"$($s.Alias)```````",```````"$($s.UserName)```````",```````"$($s.PassHash)```````"`"*" #do we need twice as many backticks?
            }
            else
            {
                $toRemove = " `"```````"$($s.Server)```````",,```````"$($s.UserName)```````",```````"$($s.PassHash)```````"`"*"
            }

            if($line -like $toRemove)
            {
                $include = $false
                break
            }
        }

        if($include)
        {
            $filtered += $line
        }
    }

    if($filtered)
    {
        $filtered = AdjustFilterRecordDelimiters $contents $filtered $functionStart $functionEnd
    }

    return $filtered
}

function AdjustFilterRecordDelimiters($contents, $filtered, $functionStart, $functionEnd)
{
    for($i = 0; $i -lt $filtered.Count; $i++)
    {
        if($i -lt $filtered.Count - 1)
        {
            if(!$filtered[$i].EndsWith(","))
            {
                $filtered[$i] = $filtered[$i] + ","
            }
        }
        else
        {
            if($filtered[$i].EndsWith(","))
            {
                $filtered[$i] = $filtered[$i].Substring(0, $filtered[$i].Length - 1)
            }
        }
    }

    return $filtered
}

function AddGoPrtgHeaderAndFooter($contents, $funcBody, $functionStart, $functionEnd)
{
    $formatted = @()
    $formatted += $contents[$functionStart]
    $formatted += $contents[$functionStart + 1]
    $formatted += $funcBody
    $formatted += $contents[$functionEnd - 1]
    $formatted += $contents[$functionEnd]

    return $formatted
}

function AddGoPrtgFunctionHeaderAndFooter($contents, $funcBody, $functionStart, $functionEnd)
{
    $formatted = @()
    $formatted += $contents[$functionStart + 2]
    $formatted += $funcBody
    $formatted += $contents[$functionEnd - 2]

    return $formatted
}

function UpdateGoPrtgFunctionBody($funcBody, $contents, $functionStart, $functionEnd)
{
    $pre = GetPre $contents $functionStart
    $post = GetPost $contents $functionEnd

    $newContent = @()
    $newContent += $pre

    if($null -ne $funcBody)
    {
        $funcBody = AddGoPrtgHeaderAndFooter $contents $funcBody $functionStart $functionEnd
        $newContent += $funcBody
    }

    $newContent += $post

    $str = [string]::Join("`r`n", $newContent)

    if($str -eq "")
    {
        Set-Content $Profile $str -NoNewline
    }
    else
    {
        Set-Content $Profile $str
    }
}

#endregion

function Write-ColorOutput($Object, $ForegroundColor)
{
    $initialColor = $Host.UI.RawUI.ForegroundColor

    if($initialColor -ne -1)
    {
        $Host.UI.RawUI.ForegroundColor = $ForegroundColor
    }

    $Object | Write-Output

    if($initialColor -ne -1)
    {
        $Host.UI.RawUI.ForegroundColor = $initialColor
    }

    #need to test piping something with a header
}


#todo: all of these tests should test extracting the passhash and confirming it decrypts to the original value


#todo: run invoke-scriptanalyzer when finished

#todo: make the passhash encrypted again, change the should be's to should belike's

#todo: if you remove the last server, the second last server should remove its semicolon

#make nuget.org and powershellgallery nuspecs able to show whats new by querying appveyor
#push notes

#maybe we should consider using the password vault?
#https://www.powershellgallery.com/packages/BetterCredentials/4.3/Content/BetterCredentials.psm1

#note: if we use the windows credential store, our module wont be cross platform compatible

Export-ModuleMember -Function Install-GoPrtgServer,Uninstall-GoPrtgServer,Get-GoPrtgServer,Connect-GoPrtgServer,Set-GoPrtgAlias