Drm.Templates.Powershell.psm1

#Dot source all functions in all ps1 files located in the module's public and private folders, excluding tests and profiles.
Get-ChildItem -Path $PSScriptRoot\private\*.ps1 -Exclude *.tests.ps1, *profile.ps1 -ErrorAction SilentlyContinue |
ForEach-Object {
    . $_.FullName
}

function Set-SolutionEnvironmentVariables{
    <#
    .SYNOPSIS
        Set environment variable values directly in an unpacked solution.
    .DESCRIPTION
        Update the current or default value of an environment variable definition in an unpacked solution,
        using the provided schemaname. If the variable does not have a current or default value,
        it will be added as the default value.
    .PARAMETER UnpackedSolutionFolder
        Required: Location of the unpakced solution
    .PARAMETER VariableReplacements
        A Hashtable of variable replacements e.g. { <schemaname> = <value>;}
    .PARAMETER JsonVariableReplacements
        Location of a .json file with variable replacements
   .EXAMPLE
        Set-SolutionEnvironmentVariables -UnpackedSolutionFolder C:\drmdemo\unpackedSolution -VariableReplacements @{ "new_helloDrm" = "example"; "new_exampleSchemaName"= "Example2"}
   .EXAMPLE
        Set-SolutionEnvironmentVariables -UnpackedSolutionFolder C:\drmdemo\unpackedSolution -JsonVariableReplacements C:\drmdemo\variablereplacements.json
    #>


    [CmdletBinding()]    
    PARAM(
        [parameter(Position=1, Mandatory=$true)]
        [ValidateScript({
            if(-NOT ($_ | Test-Path) ){
                throw "Cannot find unpacked solution at $_"
            }
            return $true 
        })]
        [System.IO.FileInfo]$UnpackedSolutionFolder,    
        [parameter(Position=2, Mandatory=$false,ParameterSetName="hashtable")]
        [Hashtable]$VariableReplacements,
        [parameter(Position=3, Mandatory=$false,ParameterSetName="json")]
        [ValidateScript({
            if(-Not ($_ | Test-Path) ){
                throw "File or folder does not exist" 
            }
            if(-Not ($_ | Test-Path -PathType Leaf) ){
                throw "The Path argument must be a file. Folder paths are not allowed."
            }
            return $true 
        })]
        [System.IO.FileInfo]$JsonVariableReplacements
    )

    # get schema definitions from folder names
    $variableSchemaNames = Get-ChildItem -Path $UnpackedSolutionFolder/environmentvariabledefinitions -Recurse -Directory -Force | Select -ExpandProperty Name

    # use the hashtable or json file and update the entries.
    if($variableSchemaNames.count -ne 0){
        
        if($JsonVariableReplacements){
            # read json into $VariableReplacements
            Write-Host "Reading variable replacements from json file..."
            $jsonReplacements = Get-Content -Raw -Path $JsonVariableReplacements | ConvertFrom-Json
        
            $VariableReplacements = @{}
            $jsonReplacements.psobject.properties | foreach{$VariableReplacements[$_.Name]= $_.Value}
        } else {
            Write-Host "Reading variable replacements from object..."
        }

        foreach($envReplacementKey in $VariableReplacements.keys){
            # find env definition in $variableSchemaNames list
            if($variableSchemaNames.Contains($envReplacementKey)){
                Write-Host "`nUpdating environment variable definition: '$($envReplacementKey)'"

                $envvarPath = "$UnpackedSolutionFolder/environmentvariabledefinitions/$envReplacementKey/environmentvariablevalues.json"
                $envdefinitionPath = "$UnpackedSolutionFolder/environmentvariabledefinitions/$envReplacementKey/environmentvariabledefinition.xml"

                # check for json file, if exists, update it.
                if(Test-Path -Path $envvarPath -PathType Leaf){
                  $json = Get-Content -Raw -Path $envvarPath | ConvertFrom-Json
                  Write-Host "...changing current value from : '$($json.environmentvariablevalues.environmentvariablevalue.value)' to '$($VariableReplacements[$envReplacementKey])'"
                  $json.environmentvariablevalues.environmentvariablevalue.value = $VariableReplacements[$envReplacementKey]
                  $json | ConvertTo-Json -depth 100 | Set-Content $envvarPath
                } elseif(Test-Path -Path $envdefinitionPath -PathType Leaf){

                  [xml]$xmlElm = Get-Content -Path $envdefinitionPath
                  
                  $testDefaultNodeExists = $xmlElm.SelectSingleNode("./environmentvariabledefinition/defaultvalue")

                  if($testDefaultNodeExists){
                    Write-Host "...changing default value from: '$($xmlElm.environmentvariabledefinition.defaultvalue)' to '$($VariableReplacements[$envReplacementKey])'"

                    $xmlElm.environmentvariabledefinition.defaultvalue = $VariableReplacements[$envReplacementKey]
                    $xmlElm.Save($envdefinitionPath)
                  } else {
                    Write-Host "...adding default value: '$($VariableReplacements[$envReplacementKey])'"

                    $child = $xmlElm.CreateElement("defaultvalue")
                    $out = $xmlElm.DocumentElement.AppendChild($child)
                    $xmlElm.environmentvariabledefinition.defaultvalue = $VariableReplacements[$envReplacementKey]
                    $xmlElm.Save($envdefinitionPath)
                  }
                }
            } else {
                Write-Host "`nNo environment variable definition was found in the solution for: '$($envReplacementKey)'"
            }
        }
    } else {
        throw "No variable definitions found. Please check the unpacked solution contains environmentvariable definitions."
    }
}

function New-DrmTemplate{
    <#
    .SYNOPSIS
        Generates a new DRM Template.
    .DESCRIPTION
        Connect to a Dynamics environment and use the Web API to build a barebones DRM template for use in automation.
    .PARAMETER Url
        Optional: [string] The Dynamics environment to connect.
    .PARAMETER EntityName
        Required: Select the entity you want to target here https://learn.microsoft.com/en-us/power-apps/developer/data-platform/webapi/reference/entitytypes?view=dataverse-latest
    .PARAMETER Filter
        Optional: Add your web api filter for example '$select=name'
    .PARAMETER SetupTemplateForAutomation
        Optional: If set, it will generate the template for connecting to a dynamics environment using application credentials.
    .PARAMETER SubscriptionId
        Required: Enter your subscrtiption id.
    .PARAMETER OutputToFile
        Optional: Custom path of location to save the template
    .EXAMPLE
        New-DrmTemplate -Url https://demo.crm11.com -Entity queues -Filter '$select=name' -SetupTemplateForAutomation -SubscriptionId 'xxxxxx'
    #>

    [CmdletBinding()]
    PARAM(
        [parameter(Position=1, Mandatory=$false)]
        [ValidatePattern('([\w-]+).crm([0-9]*).(microsoftdynamics|dynamics|crm[\w-]*).(com|de|us|cn)')]
        [string]$Url,
        [parameter(Position=2, Mandatory=$true)]
        [string]$EntityName, 
        [parameter(Position=3, Mandatory=$false)]
        [string]$Filter,
        [parameter(Position=4, Mandatory=$false)]
        [switch]$SetupTemplateForAutomation,
        #[parameter(Position=5, Mandatory=$false)]
        #[string]$SubscriptionId,
        [parameter(Position=6, Mandatory=$false)]
        [ValidateScript({
            if(-Not ($_.DirectoryName | Test-Path) ){
                throw "Folder location does not exist"
            }
            if((Get-Item -Path $_).PSIsContainer) {
                throw "You must include the file name e.g. 'template.json'"
            }
            return $true 
        })]
        [System.IO.FileInfo]$OutputToFile
        #[parameter(Position=7, DontShow=$true)]
        #[switch]$UseBetaEnvironment
    )

    #DynamicParam {
    # if ([string]::IsNullOrEmpty($drm.SubscriptionId)) {
    # $subAttribute = New-Object System.Management.Automation.ParameterAttribute
    # $subAttribute.Position = 5
    # $subAttribute.Mandatory = $true
    #
    # $attributeCollection = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
    # $attributeCollection.Add($subAttribute)
    # $SubscriptionId = New-Object System.Management.Automation.RuntimeDefinedParameter('SubscriptionId', [string], $attributeCollection)
    # $paramDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
    # $paramDictionary.Add('SubscriptionId', $SubscriptionId)
    # return $paramDictionary
    # } else {
    # $SubscriptionId = $drm.SubscriptionId
    # }
    #}

    process {

        if($conn.CurrentAccessToken) {
                           
            [hashtable]$postParams = @{}

            if($Url) {
                $postParams.Add('url', $Url)
                Write-Host("Connecting to Dynamics Instance: " + $Url)
            }
            else {
                # try and get the url from the connection.
                if($conn.ConnectedOrgPublishedEndpoints.Get_Item("WebApplication")) {
                    $dynamicsUrl = $conn.ConnectedOrgPublishedEndpoints.Get_Item("WebApplication")

                    $postParams.Add('url', $dynamicsUrl)
                    Write-Host("Connecting to Dynamics Instance: " + $dynamicsUrl)
                }
                else {
                    throw "Unable to set the Dynamics url to fetch the data, published endpoint web application not available in connection details."
                }
            }

            $postParams.Add('entityname', $EntityName)
            Write-Verbose "Entityname set to $EntityName" 

            if($Filter) {
                $postParams.Add('filter', $Filter)
                Write-Verbose "Filter set to $Filter" 
            }

            if($SetupTemplateForAutomation.IsPresent) {
                $postParams.Add('setupTemplateForAutomation', $true)
                Write-Verbose "SetupTemplateForAutomation set to 'true'"
            } 
            else {
                $postParams.Add('setupTemplateForAutomation', $false)
                Write-Verbose "SetupTemplateForAutomation set to 'false'"
            }

            $postParams.Add('token', $conn.CurrentAccessToken)
            Write-Verbose "Using token from Connect-CrmOnline connection object."

            try
            {
                Write-Host("Generating Template...")
                    
                $verboseLogging = $false

                if($PSCmdlet.MyInvocation.BoundParameters["Verbose"].IsPresent)
                {
                    $verboseLogging = $true
                }

                $templateManager= New-Object drm.Powershell.DrmTemplates.PowershellGenerateTemplate -ArgumentList $verboseLogging

                $response = $templateManager.GenerateTemplateAsync(($postParams|ConvertTo-Json)).GetAwaiter().GetResult() | ConvertFrom-Json

                if($response.Data.Template) {

                    if($OutputToFile) {
                        $templateManager.WriteTemplateToFile($OutputToFile)
                        
                        Write-Host "Template created at " $OutputToFile
                    }
                    else {
                        $pathInfo = Get-Location

                        $outputPath = Join-Path -Path $pathInfo.Path -ChildPath "\template.json"

                        $templateManager.WriteTemplateToFile($outputPath)
                        Write-Host "Template created at " $outputPath
                    }
                }
                else {
                    $JoinedString = $response.Error -join ","
                    Write-Error $JoinedString
                }

            }
            catch {
                throw $_
            }
        }
        else {
            throw "No connection to CRM online. Please connect to a Dynamics environment using the 'Connect-CrmOnline' cmdlet."
        }
    }
}

function Connect-CrmOnline{
    [CmdletBinding()]
    PARAM( 
        [parameter(Position=1, Mandatory=$true, ParameterSetName="connectionstring")]
        [string]$ConnectionString, 
        [parameter(Position=1, Mandatory=$true, ParameterSetName="Secret")]
        [Parameter(Position=1,Mandatory=$true, ParameterSetName="Creds")]
        [Parameter(Position=1,Mandatory=$true, ParameterSetName="NoCreds")]
        [ValidatePattern('([\w-]+).crm([0-9]*).(microsoftdynamics|dynamics|crm[\w-]*).(com|de|us|cn)')]
        [string]$ServerUrl, 
        [parameter(Position=2, Mandatory=$true, ParameterSetName="Creds")]
        [PSCredential]$Credential,
        [Parameter(Position=4,Mandatory=$false, ParameterSetName="Creds")]
        [Parameter(Position=3,Mandatory=$false, ParameterSetName="NoCreds")]
        [switch]$ForceOAuth,
        [parameter(Position=2, Mandatory=$true, ParameterSetName="Secret")]
        [Parameter(Position=5,Mandatory=$false, ParameterSetName="Creds")]
        [Parameter(Position=4,Mandatory=$false, ParameterSetName="NoCreds")]
        [ValidateScript({
            try {
                [System.Guid]::Parse($_) | Out-Null
                $true
            } catch {
                $false
            }
        })]
        [string]$OAuthClientId,
        [parameter(Position=3, Mandatory=$false, ParameterSetName="Secret")]
        [Parameter(Position=6,Mandatory=$false, ParameterSetName="Creds")]
        [Parameter(Position=5,Mandatory=$false, ParameterSetName="NoCreds")]
        [string]$OAuthRedirectUri, 
        [parameter(Position=4, Mandatory=$true, ParameterSetName="Secret")]
        [string]$ClientSecret, 
        [parameter(Position=5, Mandatory=$false, ParameterSetName="NoCreds")]
        [string]$Username, 
        [int]$ConnectionTimeoutInSeconds,
        [string]$LogWriteDirectory, 
        [switch]$BypassTokenCache,
        [parameter(Position=7, Mandatory=$false, ParameterSetName="Interactive")]
        [switch]$InteractiveMode
    )

    if($InteractiveMode){
        $global:conn = Get-CrmConnection -InteractiveMode
        return $global:conn
    }

    if(-not [string]::IsNullOrEmpty($ServerUrl) -and $ServerUrl.StartsWith("https://","CurrentCultureIgnoreCase") -ne $true){
        Write-Verbose "ServerUrl is missing https, fixing URL: https://$ServerUrl"
        $ServerUrl = "https://" + $ServerUrl
    }

    #starting default connection string with require new instance and server url
    $cs = "RequireNewInstance=True"
    $cs += ";Url=$ServerUrl"
    if($BypassTokenCache){
        $cs += ";TokenCacheStorePath="
    }

    if($ConnectionTimeoutInSeconds -and $ConnectionTimeoutInSeconds -gt 0){
        $newTimeout = New-Object System.TimeSpan -ArgumentList 0,0,$ConnectionTimeoutInSeconds
        Write-Verbose "Setting new connection timeout of $newTimeout"
        #set the timeout on the MaxConnectionTimeout static
        [Microsoft.Xrm.Tooling.Connector.CrmServiceClient]::MaxConnectionTimeout = $newTimeout
    }

    if($ConnectionString){
        if(!$ConnectionString -or $ConnectionString.Length -eq 0){
            throw "Cannot create the CrmServiceClient, the connection string is null"
        }
        Write-Verbose "ConnectionString provided - skipping all helpers/known parameters"
        
        $global:conn = New-Object Microsoft.Xrm.Tooling.Connector.CrmServiceClient -ArgumentList $ConnectionString
        if($global:conn){
            ApplyCrmServiceClientObjectTemplate($global:conn)  #applyObjectTemplateFormat
        }
        return $global:conn
    }
    elseif($ClientSecret){
        $cs += ";AuthType=ClientSecret"
        $cs += ";ClientId=$OAuthClientId"
        if(-not [string]::IsNullOrEmpty($OAuthRedirectUri)){
            $cs += ";redirecturi=$OAuthRedirectUri"
        }
        $cs += ";ClientSecret='$ClientSecret'"
        Write-Verbose ($cs.Replace($ClientSecret, "*******"))
        try
        {
            if(!$cs -or $cs.Length -eq 0){
                throw "Cannot create the CrmServiceClient, the connection string is null"
            }

            #$global:conn = [Microsoft.Xrm.Tooling.Connector.CrmServiceClient]::new($cs)
            $global:conn = Get-CrmConnection -ConnectionString $cs
            
            #ApplyCrmServiceClientObjectTemplate($global:conn) #applyObjectTemplateFormat
            $global:conn
            return
        }
        catch
        {
            throw $_
        }   
    }
    else{
        if(-not [string]::IsNullOrEmpty($Username) -and $ForceOAuth -eq $false){
            $cs += ";Username=$UserName"
            Write-Warning "UserName parameter is only compatible with oAuth, forcing auth mode to oAuth"
            $ForceOAuth = $true
        }
        #Default to Office365 Auth, allow oAuth to be used
        if(!$OAuthClientId -and !$ForceOAuth){
            Write-Verbose "Using AuthType=Office365"
            if(-not $Credential){
                #user did not provide a credential
                Write-Warning "Cannot create the CrmServiceClient, no credentials were provided. Credentials are required for an AuthType of Office365."
                $Credential = Get-Credential 
                if(-not $Credential){
                    throw "Cannot create the CrmServiceClient, no credentials were provided. Credentials are required for an AuthType of Office365."
                }
            }
            $cs+= ";AuthType=Office365"
            $cs+= ";Username=$($Credential.UserName)"
            $cs+= ";Password='$($Credential.GetNetworkCredential().Password)'"
        }
        elseif($ForceOAuth){
            #use oAuth if requested -ForceOAuth
            Write-Verbose "Params Provided -> ForceOAuth: {$ForceOAuth} ClientId: {$OAuthClientId} RedirectUri: {$OAuthRedirectUri}"
            #try to use the credentials if they're provided
            if($Credential){
                Write-Verbose "Using provided credentials for oAuth"
                $cs+= ";Username=$($Credential.UserName)"
                $cs+= ";Password='$($Credential.GetNetworkCredential().Password)'"
            }else{
                Write-Verbose "No credential provided, attempting single sign on with no credentials in the connectionstring"
            }

            if($OAuthClientId){
                #use the clientid if provided, else use a provided clientid
                Write-Verbose "Using provided oAuth clientid"
                $cs += ";AuthType=OAuth;ClientId=$OAuthClientId"
                if($OAuthRedirectUri){
                    $cs += ";redirecturi=$OAuthRedirectUri"
                }
            }
            else{
                #else fallback to a known clientid
                $cs+=";AuthType=OAuth;ClientId=2ad88395-b77d-4561-9441-d0e40824f9bc"
                $cs+=";redirecturi=app://5d3e90d6-aa8e-48a8-8f2c-58b45cc67315"
            }
        }

        try
        {
            if(!$cs -or $cs.Length -eq 0){
                throw "Cannot create the CrmServiceClient, the connection string is null"
            }
            #log the connection string to be helpful
            $loggedConnectionString = $cs
            if($Credential){
                $loggedConnectionString = $cs.Replace($Credential.GetNetworkCredential().Password, "*******") 
            }
            Write-Verbose "ConnectionString:{$loggedConnectionString}"

            #$global:conn = New-Object Microsoft.Xrm.Tooling.Connector.CrmServiceClient -ArgumentList $cs
            $global:conn = Get-CrmConnection -ConnectionString $cs

            #ApplyCrmServiceClientObjectTemplate($global:conn) #applyObjectTemplateFormat

            if($global:conn.LastCrmError -and $global:conn.LastCrmError -match "forbidden with client authentication scheme 'Anonymous'"){
                Write-Error "Warning: Exception encountered when authenticating, if you're using oAuth you might want to include the -username paramter to disambiguate the identity used for authenticate"
            }

            return $global:conn
        }
        catch
        {
            throw $_
        }  
    }
}