DSCResources/MSFT_SPOSearchResultSource/MSFT_SPOSearchResultSource.psm1


$InfoMapping = @(
    @{
        Protocol   = "Local"
        Type       = "SharePoint"
        ProviderID = "fa947043-6046-4f97-9714-40d4c113963d"
    },
    @{
        Protocol   = "Remote"
        Type       = "SharePoint"
        ProviderID = "1e0c8601-2e5d-4ccb-9561-53743b5dbde7"
    },
    @{
        Protocol   = "Exchange"
        Type       = "SharePoint"
        ProviderID = "3a17e140-1574-4093-bad6-e19cdf1c0122"
    },
    @{
        Protocol   = "OpenSearch"
        Type       = "SharePoint"
        ProviderID = "3a17e140-1574-4093-bad6-e19cdf1c0121"
    },
    @{
        Protocol   = "Local"
        Type       = "People"
        ProviderID = "e4bcc058-f133-4425-8ffc-1d70596ffd33"
    },
    @{
        Protocol   = "Remote"
        Type       = "People"
        ProviderID = "e377caaa-fcaf-4a1b-b7a1-e69a506a07aa"
    }
)
function Get-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Collections.Hashtable])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Name,

        [Parameter()]
        [System.String]
        $Description,

        [Parameter(Mandatory = $true)]
        [ValidateSet("Local", "Remote", "OpenSearch", "Exchange")]
        [System.String]
        $Protocol,

        [Parameter()]
        [System.String]
        $SourceURL,

        [Parameter()]
        [ValidateSet("SharePoint", "People")]
        [System.String]
        $Type = "SharePoint",

        [Parameter()]
        [System.String]
        $QueryTransform,

        [Parameter()]
        [System.Boolean]
        $ShowPartialSearch = $true,

        [Parameter()]
        [System.Boolean]
        $UseAutoDiscover,

        [Parameter()]
        [ValidateSet("Present")]
        [System.String]
        $Ensure = "Present",

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $GlobalAdminAccount,

        [Parameter()]
        [System.String]
        $ApplicationId,

        [Parameter()]
        [System.String]
        $TenantId,

        [Parameter()]
        [System.String]
        $CertificateThumbprint,

        [Parameter()]
        [System.String]
        $CertificatePath,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $CertificatePassword
    )

    Write-Verbose -Message "Setting configuration for Result Source instance $Name"
    $ConnectionMode = New-M365DSCConnection -Platform 'PnP' `
        -InboundParameters $PSBoundParameters

    #region Telemetry
    $ResourceName = $MyInvocation.MyCommand.ModuleName.Replace("MSFT_", "")
    $data = [System.Collections.Generic.Dictionary[[String], [String]]]::new()
    $data.Add("Resource", $ResourceName)
    $data.Add("Method", $MyInvocation.MyCommand)
    $data.Add("Principal", $GlobalAdminAccount.UserName)
    $data.Add("TenantId", $TenantId)
    $data.Add("ConnectionMode", $ConnectionMode)
    Add-M365DSCTelemetryEvent -Data $data
    #endregion

    $nullReturn = $PSBoundParameters
    $nullReturn.Ensure = "Absent"
    try
    {
        if ($null -eq $Script:RecentExtract)
        {
            $Script:RecentExtract = [Xml] (Get-PnPSearchConfiguration -Scope Subscription)
        }
        $source = $Script:RecentExtract.SearchConfigurationSettings.SearchQueryConfigurationSettings.SearchQueryConfigurationSettings.Sources.Source `
        | Where-Object -FilterScript { $_.Name -eq $Name }

        if ($null -eq $source)
        {
            Write-Verbose -Message "The specified Result Source {$($Name)} doesn't already exist."
            return $nullReturn
        }

        $ExoSource = [string] $source.ConnectionUrlTemplate
        $SourceHasAutoDiscover = $false
        if ("http://auto?autodiscover=true" -eq $ExoSource)
        {
            $SourceHasAutoDiscover = $true
        }

        $allowPartial = $source.QueryTransform.OverridePropertiesForSeralization.KeyValueOfstringanyType `
        | Where-Object -FilterScript { $_.Key -eq "AllowPartialResults" }

        $mapping = $InfoMapping | Where-Object -FilterScript { $_.ProviderID -eq $source.ProviderId }

        $returnValue = @{
            Name               = $Name
            Description        = [string] $source.Description
            Protocol           = $mapping.Protocol
            Type               = $mapping.Type
            QueryTransform     = [string] $source.QueryTransform._QueryTemplate
            SourceURL          = [string] $source.ConnectionUrlTemplate
            UseAutoDiscover    = $SourceHasAutoDiscover
            GlobalAdminAccount = $GlobalAdminAccount
            Ensure             = "Present"
        }

        if ($null -ne $allowPartial)
        {
            $returnValue.Add("ShowPartialSearch", [System.Boolean]$allowPartial.Value.InnerText)
        }

        return $returnValue
    }
    catch
    {
        try
        {
            Write-Verbose -Message $_
            $tenantIdValue = ""
            if (-not [System.String]::IsNullOrEmpty($TenantId))
            {
                $tenantIdValue = $TenantId
            }
            elseif ($null -ne $GlobalAdminAccount)
            {
                $tenantIdValue = $GlobalAdminAccount.UserName.Split('@')[1]
            }
            Add-M365DSCEvent -Message $_ -EntryType 'Error' `
                -EventID 1 -Source $($MyInvocation.MyCommand.Source) `
                -TenantId $tenantIdValue
        }
        catch
        {
            Write-Verbose -Message $_
        }
        return $nullReturn
    }
}

function Set-TargetResource
{
    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Name,

        [Parameter()]
        [System.String]
        $Description,

        [Parameter(Mandatory = $true)]
        [ValidateSet("Local", "Remote", "OpenSearch", "Exchange")]
        [System.String]
        $Protocol,

        [Parameter()]
        [System.String]
        $SourceURL,

        [Parameter()]
        [ValidateSet("SharePoint", "People")]
        [System.String]
        $Type = "SharePoint",

        [Parameter()]
        [System.String]
        $QueryTransform,

        [Parameter()]
        [System.Boolean]
        $ShowPartialSearch = $true,

        [Parameter()]
        [System.Boolean]
        $UseAutoDiscover,

        [Parameter()]
        [ValidateSet("Present")]
        [System.String]
        $Ensure = "Present",

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $GlobalAdminAccount,

        [Parameter()]
        [System.String]
        $ApplicationId,

        [Parameter()]
        [System.String]
        $TenantId,

        [Parameter()]
        [System.String]
        $CertificateThumbprint,

        [Parameter()]
        [System.String]
        $CertificatePath,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $CertificatePassword
    )

    Write-Verbose -Message "Setting configuration for Result Source instance $Name"
    #region Telemetry
    $ResourceName = $MyInvocation.MyCommand.ModuleName.Replace("MSFT_", "")
    $data = [System.Collections.Generic.Dictionary[[String], [String]]]::new()
    $data.Add("Resource", $ResourceName)
    $data.Add("Method", $MyInvocation.MyCommand)
    $data.Add("Principal", $GlobalAdminAccount.UserName)
    $data.Add("TenantId", $TenantId)
    Add-M365DSCTelemetryEvent -Data $data
    #endregion

    $ConnectionMode = New-M365DSCConnection -Platform 'PnP' `
        -InboundParameters $PSBoundParameters

    Write-Verbose -Message "Reading SearchConfigurationSettings XML file"
    $SearchConfigTemplatePath = Join-Path -Path $PSScriptRoot `
        -ChildPath "..\..\Dependencies\SearchConfigurationSettings.xml" `
        -Resolve
    $SearchConfigXML = [Xml] (Get-Content $SearchConfigTemplatePath -Raw)

    # Get the result source back if it already exists.
    if ($null -eq $Script:RecentExtract)
    {
        $Script:RecentExtract = [XML] (Get-PnPSearchConfiguration -Scope Subscription)
    }

    $source = $Script:RecentExtract.SearchConfigurationSettings.SearchQueryConfigurationSettings.SearchQueryConfigurationSettings.Sources.Source `
    | Where-Object -FilterScript { $_.Name -eq $Name }
    if ($null -ne $source)
    {
        $currentID = $source.Id
    }

    Write-Verbose -Message "Generating new SearchConfigurationSettings XML file"
    $newSource = $SearchConfigXML.CreateElement("d4p1:Source", `
            "http://schemas.datacontract.org/2004/07/Microsoft.Office.Server.Search.Administration.Query")

    Write-Verbose -Message "Setting ConnectionUrlTemplate"
    $node = $SearchConfigXML.CreateElement("d4p1:ConnectionUrlTemplate", `
            "http://schemas.datacontract.org/2004/07/Microsoft.Office.Server.Search.Administration.Query")
    $node.InnerText = $SourceUrl
    $newSource.AppendChild($node) | Out-Null

    Write-Verbose -Message "Setting CreatedDate"
    $node = $SearchConfigXML.CreateElement("d4p1:CreatedDate", `
            "http://schemas.datacontract.org/2004/07/Microsoft.Office.Server.Search.Administration.Query")
    $node.InnerText = [DateTime]::Now.ToString("yyyy-MM-ddThh:mm:ss.00")
    $newSource.AppendChild($node) | Out-Null

    Write-Verbose -Message "Setting Description"
    $node = $SearchConfigXML.CreateElement("d4p1:Description", `
            "http://schemas.datacontract.org/2004/07/Microsoft.Office.Server.Search.Administration.Query")
    $node.InnerText = $Description
    $newSource.AppendChild($node) | Out-Null

    Write-Verbose -Message "Setting Existing Id"
    $node = $SearchConfigXML.CreateElement("d4p1:Id", `
            "http://schemas.datacontract.org/2004/07/Microsoft.Office.Server.Search.Administration.Query")

    if ($null -ne $currentID)
    {
        $node.InnerText = $currentId
    }
    else
    {
        $node.InnerText = (New-Guid).ToString()
    }
    $newSource.AppendChild($node) | Out-Null

    Write-Verbose -Message "Setting Name"
    $node = $SearchConfigXML.CreateElement("d4p1:Name", `
            "http://schemas.datacontract.org/2004/07/Microsoft.Office.Server.Search.Administration.Query")
    $node.InnerText = $Name
    $newSource.AppendChild($node) | Out-Null

    Write-Verbose -Message "Setting ProviderId"
    $mapping = $InfoMapping | Where-Object -FilterScript { $_.Protocol -eq $Protocol -and $_.Type -eq $Type }
    $node = $SearchConfigXML.CreateElement("d4p1:ProviderId", `
            "http://schemas.datacontract.org/2004/07/Microsoft.Office.Server.Search.Administration.Query")
    $node.InnerText = $mapping.ProviderID
    $catch = $newSource.AppendChild($node)

    Write-Verbose -Message "Setting QueryTransform"
    $queryTransformNode = $SearchConfigXML.CreateElement("d4p1:QueryTransform", `
            "http://schemas.datacontract.org/2004/07/Microsoft.Office.Server.Search.Administration.Query")
    $queryTransformNode.SetAttribute("xmlns:d6p1", "http://www.microsoft.com/sharepoint/search/KnownTypes/2008/08")

    Write-Verbose -Message "Setting QueryTransform:Id"
    $node = $SearchConfigXML.CreateElement("d6p1:Id", `
            "http://www.microsoft.com/sharepoint/search/KnownTypes/2008/08")
    $node.InnerText = (New-Guid).ToString()
    $queryTransformNode.AppendChild($node)

    Write-Verbose -Message "Setting QueryTransform:ParentType"
    $queryTransformNode = $SearchConfigXML.CreateElement("d6p1:ParentType", `
            "http://www.microsoft.com/sharepoint/search/KnownTypes/2008/08")
    $node.InnerText = "Source"
    $queryTransformNode.AppendChild($node)

    Write-Verbose -Message "Setting QueryTransform:QueryPropertyExpressions"
    $QueryPropertyExpressions = $SearchConfigXML.CreateElement("d6p1:QueryPropertyExpressions", `
            "http://www.microsoft.com/sharepoint/search/KnownTypes/2008/08")

    Write-Verbose -Message "Setting QueryTransform:QueryPropertyExpressions:MaxSize"
    $node = $SearchConfigXML.CreateElement("d6p1:MaxSize", `
            "http://www.microsoft.com/sharepoint/search/KnownTypes/2008/08")
    $node.InnerText = "2147483647"
    $QueryPropertyExpressions.AppendChild($node)

    Write-Verbose -Message "Setting QueryTransform:QueryPropertyExpressions:OrderedItems"
    $node = $SearchConfigXML.CreateElement("d6p1:OrderedItems", `
            "http://www.microsoft.com/sharepoint/search/KnownTypes/2008/08")
    $QueryPropertyExpressions.AppendChild($node)

    $queryTransformNode.AppendChild($QueryPropertyExpressions)

    Write-Verbose -Message "Setting QueryTransform:_IsReadOnly"
    $node = $SearchConfigXML.CreateElement("d6p1:_IsReadOnly", `
            "http://www.microsoft.com/sharepoint/search/KnownTypes/2008/08")
    $node.InnerText = "true"
    $queryTransformNode.AppendChild($node)

    Write-Verbose -Message "Setting QueryTransform:_QueryTemplate"
    $node = $SearchConfigXML.CreateElement("d6p1:_QueryTemplate", `
            "http://www.microsoft.com/sharepoint/search/KnownTypes/2008/08")
    $node.InnerText = $QueryTransform
    $queryTransformNode.AppendChild($node) | Out-Null

    Write-Verbose -Message "Setting QueryTransform:_SourceId"
    $node = $SearchConfigXML.CreateElement("d6p1:_SourceId", `
            "http://www.microsoft.com/sharepoint/search/KnownTypes/2008/08")
    $node.SetAttribute("i:nil", "true")
    $queryTransformNode.AppendChild($node)

    Write-Verbose -Message "Inserting QueryTransform"
    $newSource.AppendChild($queryTransformNode) | Out-Null

    Write-Verbose -Message "Inserting new Source Node"
    $xmlNode = $SearchConfigXML.SearchConfigurationSettings.SearchQueryConfigurationSettings.SearchQueryConfigurationSettings.Sources.OwnerDocument.ImportNode($newSource, $true)
    $SearchConfigXML.SearchConfigurationSettings.SearchQueryConfigurationSettings.SearchQueryConfigurationSettings.Sources.AppendChild($xmlNode)

    Write-Verbose -Message "Saving XML file in a temporary location"
    $tempPath = Join-Path -Path $ENV:TEMP `
        -ChildPath ((New-Guid).ToString().Split('-')[0] + ".config")
    $SearchConfigXML.OuterXml | Out-File $tempPath

    # Create the Result Source if it doesn't already exist
    Write-Verbose -Message "Applying new Search Configuration back to the Office365 Tenant"
    Set-PnPSearchConfiguration -Scope Subscription -Path $tempPath
}

function Test-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.Boolean])]
    param
    (
        [Parameter(Mandatory = $true)]
        [System.String]
        $Name,

        [Parameter()]
        [System.String]
        $Description,

        [Parameter(Mandatory = $true)]
        [ValidateSet("Local", "Remote", "OpenSearch", "Exchange")]
        [System.String]
        $Protocol,

        [Parameter()]
        [System.String]
        $SourceURL,

        [Parameter()]
        [ValidateSet("SharePoint", "People")]
        [System.String]
        $Type = "SharePoint",

        [Parameter()]
        [System.String]
        $QueryTransform,

        [Parameter()]
        [System.Boolean]
        $ShowPartialSearch = $true,

        [Parameter()]
        [System.Boolean]
        $UseAutoDiscover,

        [Parameter()]
        [ValidateSet("Present")]
        [System.String]
        $Ensure = "Present",

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $GlobalAdminAccount,

        [Parameter()]
        [System.String]
        $ApplicationId,

        [Parameter()]
        [System.String]
        $TenantId,

        [Parameter()]
        [System.String]
        $CertificateThumbprint,

        [Parameter()]
        [System.String]
        $CertificatePath,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $CertificatePassword
    )
    #region Telemetry
    $ResourceName = $MyInvocation.MyCommand.ModuleName.Replace("MSFT_", "")
    $data = [System.Collections.Generic.Dictionary[[String], [String]]]::new()
    $data.Add("Resource", $ResourceName)
    $data.Add("Method", $MyInvocation.MyCommand)
    $data.Add("Principal", $GlobalAdminAccount.UserName)
    $data.Add("TenantId", $TenantId)
    Add-M365DSCTelemetryEvent -Data $data
    #endregion

    Write-Verbose -Message "Testing configuration for Result Source instance $Name"

    $CurrentValues = Get-TargetResource @PSBoundParameters

    Write-Verbose -Message "Current Values: $(Convert-M365DscHashtableToString -Hashtable $CurrentValues)"
    Write-Verbose -Message "Target Values: $(Convert-M365DscHashtableToString -Hashtable $PSBoundParameters)"

    $TestResult = Test-M365DSCParameterState -CurrentValues $CurrentValues `
        -Source $($MyInvocation.MyCommand.Source) `
        -DesiredValues $PSBoundParameters

    Write-Verbose -Message "Test-TargetResource returned $TestResult"

    return $TestResult
}

function Export-TargetResource
{
    [CmdletBinding()]
    [OutputType([System.String])]
    param
    (
        [Parameter()]
        [System.Management.Automation.PSCredential]
        $GlobalAdminAccount,

        [Parameter()]
        [System.String]
        $ApplicationId,

        [Parameter()]
        [System.String]
        $TenantId,

        [Parameter()]
        [System.String]
        $CertificateThumbprint,

        [Parameter()]
        [System.String]
        $CertificatePath,

        [Parameter()]
        [System.Management.Automation.PSCredential]
        $CertificatePassword
    )
    $ConnectionMode = New-M365DSCConnection -Platform 'PnP' `
        -InboundParameters $PSBoundParameters

    #region Telemetry
    $ResourceName = $MyInvocation.MyCommand.ModuleName.Replace("MSFT_", "")
    $data = [System.Collections.Generic.Dictionary[[String], [String]]]::new()
    $data.Add("Resource", $ResourceName)
    $data.Add("Method", $MyInvocation.MyCommand)
    $data.Add("Principal", $GlobalAdminAccount.UserName)
    $data.Add("TenantId", $TenantId)
    $data.Add("ConnectionMode", $ConnectionMode)
    Add-M365DSCTelemetryEvent -Data $data
    #endregion

    try
    {
        $SearchConfig = [Xml] (Get-PnPSearchConfiguration -Scope Subscription)
        [array]$sources = $SearchConfig.SearchConfigurationSettings.SearchQueryConfigurationSettings.SearchQueryConfigurationSettings.Sources.Source

        $dscContent = ''
        $i = 1
        $sourcesLength = $sources.Length
        Write-Host "`r`n" -NoNewline
        foreach ($source in $sources)
        {
            $mapping = $InfoMapping | Where-Object -FilterScript { $_.ProviderID -eq $source.ProviderId }
            Write-Host " |---[$i/$($sourcesLength)] $($source.Name)" -NoNewline

            $Params = @{
                Name                  = $source.Name
                Protocol              = $mapping.Protocol
                GlobalAdminAccount    = $GlobalAdminAccount
                ApplicationId         = $ApplicationId
                TenantId              = $TenantId
                CertificateThumbprint = $CertificateThumbprint
                CertificatePath       = $CertificatePath
                CertificatePassword   = $CertificatePassword
            }
            $Results = Get-TargetResource @Params
            $Results = Update-M365DSCExportAuthenticationResults -ConnectionMode $ConnectionMode `
                -Results $Results
            $currentDSCBlock = Get-M365DSCExportContentForResource -ResourceName $ResourceName `
                -ConnectionMode $ConnectionMode `
                -ModulePath $PSScriptRoot `
                -Results $Results `
                -GlobalAdminAccount $GlobalAdminAccount
            $dscContent += $currentDSCBlock
            Save-M365DSCPartialExport -Content $currentDSCBlock `
                -FileName $Global:PartialExportFileName
            $i++
            Write-Host $Global:M365DSCEmojiGreenCheckmark
        }
        return $dscContent
    }
    catch
    {
        try
        {
            Write-Verbose -Message $_
            $tenantIdValue = ""
            if (-not [System.String]::IsNullOrEmpty($TenantId))
            {
                $tenantIdValue = $TenantId
            }
            elseif ($null -ne $GlobalAdminAccount)
            {
                $tenantIdValue = $GlobalAdminAccount.UserName.Split('@')[1]
            }
            Add-M365DSCEvent -Message $_ -EntryType 'Error' `
                -EventID 1 -Source $($MyInvocation.MyCommand.Source) `
                -TenantId $tenantIdValue
        }
        catch
        {
            Write-Verbose -Message $_
        }
        return ""
    }
}

Export-ModuleMember -Function *-TargetResource