src/common/GraphUtilities.ps1

# Copyright 2019, Adam Edwards
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

ScriptClass GraphUtilities {
    static {

        function ToGraphRelativeUriPathUnqualified( $relativeUri, $context = $null ) {
            $this.__ToGraphRelativeUriPath($relativeUri, $context)
        }

        function __NormalizeBacktrack( $uriAbsoluteString ) {
            if ( $uriAbsoluteString[0] -ne '/' ) {
                throw "'$uriAbsoluteString' is not an absolute path"
            }

            $segments = $uriAbsoluteString -split '/'

            $newSegments = new-object System.Collections.Generic.List[string]

            $segments | foreach {
                if ( $_ -eq '..' ) {
                    if ( $newSegments.count -gt 0 ) {
                        $newSegments.RemoveAt($newSegments.count - 1)
                     }
                } else {
                    $newSegments.Add($_)
                }
            }

            $result = $newSegments -join '/'

            if ( $result[0] -ne '/' ) {
                $result = '/' + $result
            }
            write-verbose "Backtrack '$uriAbsoluteString' converted to '$result'"
            $result
        }

        function ToGraphRelativeUri( $relativeUri, $context = $null ) {
             __ToGraphRelativeUriPath $relativeUri $context
        }

        function __ToGraphRelativeUriPath( $relativeUri, $context = $null ) {
            $normalizedUri = ($relativeUri -split '/' | where { $_ -ne '.' }) -join '/'
            $result = if ( $relativeUri.tostring()[0] -eq '/' ) {
                [Uri] ($this.__NormalizeBacktrack($normalizedUri))
            } else {
                $graphContext = if ( $context ) {
                    $context
                } else {
                    'GraphContext' |::> GetCurrent
                }

                $locationUri = if ( $graphContext.location ) {
                    $graphContext.location |=> ToGraphUri
                } else {
                    # By default, locations are not defined, so if there isn't one,
                    # assume the root
                    '/'
                }

                $graphUri = $this.JoinGraphUri($locationUri, $normalizedUri)
                $canonicalGraphUri = __NormalizeBacktrack $graphUri.tostring()
                $canonicalGraphUri
            }

            $result
        }

        function ToLocationUriPath( $context, $relativeUri ) {
            $graphRelativeUri = $this.ToGraphRelativeUriPathUnqualified($relativeUri, $context)
            "/{0}:{1}" -f $context.name, $graphRelativeUri
        }

        function JoinAbsoluteUri([Uri] $absoluteUri, [string] $relativeUri) {
            if ( ! $absoluteUri.IsAbsoluteUri ) {
                throw "Absolute uri argument '$($absoluteUri.tostring())' is not an absolute uri"
            }

            $::.GraphUtilities.JoinFragmentUri($absoluteUri, $relativeUri)
        }

        function JoinRelativeUri([string] $relativeUri1, [string] $relativeUri2) {
            if ( $relativeUri1[0] -eq '/' ) {
                throw "'$relativeUri1' is an absolute path"
            }

            $this.JoinFragmentUri($relativeUri1, $relativeUri2)
        }

        function JoinFragmentUri([string] $fragmentUri1, [string] $fragmentUri2) {
            ($fragmentUri1.trimend('/'), $fragmentUri2.trim('/') -join '/')
        }

        function JoinGraphUri([Uri] $graphUri, [string] $relativeUri) {
            if ( $graphUri.tostring()[0] -ne '/' ) {
                throw "Graph uri parameter '$graphUri' is not an absolute graph path"
            }

            $uriString = $this.JoinFragmentUri($graphUri, $relativeUri)
            [Uri] $uriString
        }

        function ParseLocationUriPath($UriPath) {
            $context = $null
            $isAbsolute = $false
            $graphRelativeUri = $null
            if ( $UriPath ) {
                $UriString = $UriPath.tostring()
                $contextEnd = $UriString.IndexOf(':')
                $isAbsolute = $UriString[0] -eq '/'
                $graphRelativeUri = if ( $contextEnd -eq -1 ) {
                    $isAbsolute = $UriString[0] -eq '/'
                    $UriString
                } else {
                    if ( $isAbsolute ) {
                        $contextComponents = $UriString.substring(0, $contextEnd) -split '/'
                        if ( $contextComponents.length -eq 2 ) {
                            $context = $contextComponents[1]
                            $UriString.substring($contextEnd + 1, $UriString.length - $contextEnd - 1)
                        } else {
                            $UriString
                        }
                    } else {
                        $UriString
                    }
                }
            }

            [PSCustomObject]@{
                ContextName=$context
                RelativeUri=$graphRelativeUri
                IsAbsoluteUri=$isAbsolute
            }
        }

        function ParseGraphUri([Uri] $uri, $context) {
            $endpoint = $null
            $version = $null
            $sameEndpoint = $true
            $sameVersion = $true
            $isAbsolute = $uri.IsAbsoluteUri
            $matchedContext = $null

            $relativeUri = if ( $isAbsolute ) {
                $endpoint = [Uri] ('https://{0}' -f $uri.host)
                $graphRelativeUri = ''
                $version = if ( $uri.segments.length -gt 0 ) {
                    for ( $uriIndex = 2; $uriIndex -lt $uri.segments.length; $uriIndex++ ) {
                        $graphRelativeUri += $uri.segments[$uriIndex]
                    }
                    $uri.segments[1].trim('/')
                } else {
                    throw [ArgumentException]::new("Invalid uri '$($uri.tostring())'")
                }

                if ( $context ) {
                    $sameEndpoint = if ( $context.connection -and $context.connection.GraphEndpoint ) {
                        $userAbsoluteUri = $uri.absoluteuri
                        $userEndpoint = $userAbsoluteUri.substring(0, $userAbsoluteUri.length - $uri.PathAndQuery.length).trimend('/')
                        write-verbose ("Comparing endpoints: user: '{0}' vs. context '{1}'" -f $userEndpoint, $context.connection.GraphEndpoint.Graph)
                        $userEndpoint -eq ($context.connection.GraphEndpoint.Graph).tostring().trimend('/')
                    } else {
                        $false
                    }
                    $sameVersion = $version -eq $context.version
                }

                $graphRelativeUri
            } else {
                $uri
            }

            if ( $relativeUri -eq $null ) {
                throw "Invalid graph uri '$uri'"
            }

            $normalized = JoinGraphUri / $relativeUri

            $matchedContext = if ( $isAbsolute )  {
                if ( $context -and ($sameEndpoint -and $sameVersion) ) {
                    $context
                } else {
                    $::.GraphContext |=> FindContext $endpoint $version | select -first 1
                }
            } else {
                $::.GraphContext.GetCurrent()
            }

            [PSCustomObject]@{
                GraphRelativeUri = $normalized
                GraphVersion = $version
                EndpointMatchesContext = $sameEndpoint
                VersionMatchesContext = $sameVersion
                IsContextCompatible = $sameEndpoint -and $sameVersion
                MatchedContext = $matchedContext
                IsAbsolute = $isAbsolute
            }
        }

        function ParseGraphRelativeLocation([string] $locationUri) {
            $graphName = if ( $locationUri.startswith('/') ) {
                $segments = $locationUri.tostring() -split '/'
                if ( $segments[1].endswith(':') ) {
                    ($locationUri -split ':')[0].trimstart('/')
                }
            }

            # Handle absolute web uri's, e.g. https://mygraph.microsoft.com/v1.0/singleton/etc
            $locationUriAsWebUri = [Uri] $locationUri

            if ( $locationUriAsWebUri.IsAbsoluteUri -and ( !( !$locationUriAsWebUri.host) ) -eq $true ) {
                # This is an absolute web uri
                $parsedUri = ParseGraphUri $locationUriAsWebUri
                @{
                    Context = $parsedUri.MatchedContext
                    GraphRelativeUri = $parsedUri.GraphRelativeUri
                }
            } else {
                $relativeUri = $locationUri
                $context = if ( ! $graphName ) {
                    $::.GraphContext |=> GetCurrent
                } else {
                    # Start 2 after graphname due to the required '/' and ':' in '/graphname:', and account for that in length as well
                    $relativeUri = $locationUri.substring($graphName.length + 2, $locationUri.length - $graphName.length - 2)
                    $::.logicalgraphmanager.Get().contexts[$graphName].Context
                }

                @{
                    Context = $context
                    GraphRelativeUri = $::.GraphUtilities |=> ToGraphRelativeUri $relativeUri $context
                }
            }
        }
    }
}