Configuration.ps1

Set-StrictMode -Version 2

function Test-TelligentPath {
    <#
    .SYNOPSIS
        Tests if a SQL Server exists and can be connected to. Optonally checks for a specific database or table.
    .PARAMETER Path
        The path to test for a Teligent Community
    .PARAMETER AllowEmpty
        Specifies that an empty path should be considered as valid
    .PARAMETER IsValid
        Only test if the path is syntaticly valid, not that it actually contains a valid Telligent Community instance
    .PARAMETER Web
        Only pass if the path contains a Telligent Community website, not a Job Server
    .PARAMETER JobScheduler
        Only pass if the path contains a Telligent Community Job Server, not a website
    #>
    [CmdletBinding(DefaultParameterSetName='Either')]
    param(
        [parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)]
        [AllowEmptyString()]
        [string]$Path,
        [switch]$AllowEmpty,
        [parameter(ParameterSetName='Valid', Mandatory=$true)]
        [switch]$IsValid,
        [parameter(ParameterSetName='Web', Mandatory=$true)]
        [switch]$Web,
        [parameter(ParameterSetName='JobScheduler', Mandatory=$true)]
        [switch]$JobScheduler
    )
    if(!($Path)) {
        if(!$AllowEmpty) {
            throw 'Argument must not be null'
        }
    }
    elseif ($IsValid) {
        if (!(Test-Path $Path -PathType Container -IsValid -ErrorAction SilentlyContinue)) {
            throw "'$Path' is not a valid path"
        }
    }
    else {
        if (!(Test-Path $Path -PathType Container -ErrorAction SilentlyContinue)) {
            throw "'$Path' does not exist"
        }
        if (!(Join-Path $Path communityserver.config | Test-Path  -ErrorAction SilentlyContinue)) {
            throw "'$Path' does not contain a valid Telligent Community community"
        }
        if ($Web -and !(Join-Path $Path web.config | Test-Path  -ErrorAction SilentlyContinue)) {
            throw "'$Path' does not contain a valid Telligent Community website"
        }
        elseif ($JobScheduler -and !((Join-Path $Path Telligent.JobScheduler.Service.exe | Test-Path) -or (Join-Path $Path Telligent.Jobs.Server.exe | Test-Path))) {
            throw "'$Path' does not contain a valid Telligent Community Job Server"
        }
    }
    return $true
    
}

function Set-ConnectionString {
    <#
    .SYNOPSIS
        Sets the Connection Strings in the .net configurationf ile
    .PARAMETER WebsitePath
        The path to the application you want to set connection strings for
    .PARAMETER Server
        The SQL Server the connection string will point to
    .PARAMETER Database
        The database to use
    .PARAMETER SqlCredentials
        If using SQL Authenticaiton, specifies the username nad password to use in the connection string. If not specified, the connection string uses Integrated Security.
    .PARAMETER ConfigurationFile
        The configuration file to set the connection strings in.
    .PARAMETER ConnectionStringName
        The name of the connection string to set.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ })]
        [string]$WebsitePath,
        [Parameter(Mandatory=$true)]
        [string]$Name,
        [Parameter(Mandatory=$true)]
        [string]$Value,
        [string]$ConfigurationFile = 'connectionstrings.config'
    )
    
    $path = Join-Path $WebsitePath $ConfigurationFile | Resolve-Path | select -ExpandProperty ProviderPath
    $connectionStrings = [xml](gc $path)  
    $connectionStrings.connectionStrings.add |
        ? { $_.name -eq $Name} |
        % { $_.connectionString = $Value}
    $connectionStrings.Save($path)     
}

function Set-DatabaseConnectionString {
    <#
    .SYNOPSIS
        Sets the Connection Strings in the .net configurationf ile
    .PARAMETER WebsitePath
        The path to the application you want to set connection strings for
    .PARAMETER Server
        The SQL Server the connection string will point to
    .PARAMETER Database
        The database to use
    .PARAMETER SqlCredentials
        If using SQL Authenticaiton, specifies the username nad password to use in the connection string. If not specified, the connection string uses Integrated Security.
    .PARAMETER ConfigurationFile
        The configuration file to set the connection strings in.
    .PARAMETER ConnectionStringName
        The name of the connection string to set.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ })]
        [string]$WebsitePath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [alias('ServerInstance')]
        [string]$Server,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Database,
        [PSCredential]$SqlCredentials
    )

    if ($SqlCredentials){
        $connectionString = "Server=$Server;Database=$Database;uid=$($SqlCredentials.UserName);pwd=$($SqlCredentials.Password);"
    }
    else {
        $connectionString = "Server=$Server;Database=$Database;Trusted_Connection=yes;"
    }
    
    Set-ConnectionString $WebsitePath -Name 'SiteSqlServer' -Value $connectionString
}

function Get-ConnectionString {
    <#
    .SYNOPSIS
        Sets the Connection Strings in the .net configurationf ile
    .PARAMETER Database
        The database
    .PARAMETER Server
        The SQL Server the connection string will point to
    .PARAMETER SqlCredentials
        If using SQL Authenticaiton, specifies the username nad password to use in the connection string. If not specified, the connection string uses Integrated Security.
    .PARAMETER ConfigurationFile
        The configuration file containing the connection string to read.
    .PARAMETER ConnectionStringName
        The name of the connection string to set.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ })]
        [string]$Directory,
        [ValidateScript({Test-Path $_ -PathType Leaf})]
        [string]$ConfigurationFile = 'connectionStrings.Config',
        [string]$ConnectionStringName = 'SiteSqlServer'

    )
    #Load connection string info
    $connectionStrings = [xml](get-content (Join-Path $Directory $ConfigurationFile) -ErrorAction SilentlyContinue)
    $siteSqlConnectionString = $connectionStrings.connectionStrings.add |
        ? name -eq $ConnectionStringName |
        select -ExpandProperty connectionString

    if(!$siteSqlConnectionString) {
        Write-Error "'$ConnectionStringName' connection string not found in $ConfigFile"
    }
    try {
        $connectionString = New-Object System.Data.SqlClient.SqlConnectionStringBuilder $siteSqlConnectionString -EA SilentlyContinue
    }
    catch{}
    $connectionInfo = @{
        ServerInstance = $connectionString.DataSource
        Database = $connectionString.InitialCatalog
    }
    if(!$connectionString.IntegratedSecurity) {
        $connectionInfo.Username = $connectionString.UserID
        $connectionInfo.Password = $connectionString.Password
    }

    $connectionInfo
}

function Get-ConnectionStrings {
    <#
    .SYNOPSIS
        Gets the values from the ConnectionStrings file for a community.
    .PARAMETER Path
        The path to the community's Website or Job Scheduler.
    .PARAMETER FileName
        The name of the connection strings file to open
    #>

    param(
        [ValidateNotNullOrEmpty()]
        [string]$Path,
        [string]$ConfigurationFile = 'connectionstrings.config'
    )
    
    $config = @{}

    ([xml](Get-Content (Join-Path $Path "$ConfigurationFile"))).connectionStrings.add |% {
        $config[$_.name] = $_.connectionString
    }

    $config
}

function New-CommunityApiKey {
    <#
    .SYNOPSIS
        Creates a new REST API Key
    .PARAMETER ApiKey
        The API Key to Create
    .PARAMETER Name
        The name for the API Key.
    .PARAMETER UserId
        The User to create the API Key for
    .PARAMETER WebsitePath
        The path of the Telligent Community website. If not specified, defaults to the current directory.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ })]
        [string]$WebsitePath,
        [Parameter(Mandatory=$true)]
        [ValidatePattern('^[a-z0-9]+$')]
        [string]$ApiKey,
        [ValidateNotNullOrEmpty()]
        [string]$Name = 'Auto Generated',
        [ValidatePattern('^[a-z0-9\-\._ ]+$')]
        [int]$UserId= 2100
    )

    $createApiKey = "INSERT INTO [dbo].[cs_ApiKeys] ([UserID],[Value],[Name],[DateCreated],[Enabled]) VALUES ($UserId,'$ApiKey','$Name',GETDATE(), 1)"
    Invoke-TelligentSqlCmd $WebsitePath -Query $createApiKey
}

function Add-TelligentOverrideChangeAttribute {
    <#
    .SYNOPSIS
        Adds a Change entry to the communityserver_override.config
    .PARAMETER XPath
        The XPath for the element containing the attribute to manipulate
    .PARAMETER Name
        The Name of the node to modify
    .PARAMETER Value
        The new value of the node
    .PARAMETER WebsitePath
        The path of the Telligent Community website. If not specified, defaults to the current directory.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ })]
        [string]$WebsitePath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$XPath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Name,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Value
    )
    #TODO: Look at original config file to ensure XPath & Node exist
    
    $overridePath = join-path $WebsitePath communityserver_override.config
    if (!(test-path $overridePath)) {
        '<?xml version="1.0" ?><Overrides />' |out-file $overridePath    
    }

    $overrides = [xml](gc $overridePath)
    $override = $overrides.CreateElement('Override')
    $override.SetAttribute('xpath', $XPath)
    $override.SetAttribute('mode', 'change')
    $override.SetAttribute('name', $Name)
    $override.SetAttribute('value', $Value)
    $overrides.DocumentElement.AppendChild($override) |out-null
    $overrides.Save(($overridePath | Resolve-Path).ProviderPath)
}

function Set-TelligentFilestorage {
    <#
    .SYNOPSIS
        Sets the Filestorage location for a Telligent Community
    .PARAMETER WebsitePath
        The path of the Telligent Community website.
    .PARAMETER FilestoragePath
        The Filestorage Location to use. The Filestorage should already have been moved to this location.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ })]
        [string]$WebsitePath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({Test-Path $_ -PathType Container})]
        [string]$FilestoragePath
    )

    $version = Get-TelligentCommunity $WebsitePath | select -ExpandProperty PlatformVersion

    if ($Version.Major -ge 10) {
        Set-ConnectionString $WebsitePath -Name FileStorage -Value $FilestoragePath
    }
    else {
        Add-TelligentOverrideChangeAttribute $WebsitePath `
            -XPath "/CommunityServer/CentralizedFileStorage/fileStoreGroup[@name='default']" `
            -Name basePath `
            -Value $FilestoragePath
    }
}

function Set-TelligentSolrUrl {
    <#
    .SYNOPSIS
        Updates the Search Url used by the Telligent Community in the current directory
    .PARAMETER Url
        The url of the Solr instance to use
    .PARAMETER WebsitePath
        The path of the Telligent Community website. If not specified, defaults to the current directory.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ })]
        [string]$WebsitePath,
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        ##No longer works for solr 4.0
        #[ValidateScript({Invoke-WebRequest ($_.AbsoluteUri.TrimEnd('/') + "/admin/") -Method HEAD -UseBasicParsing})]
        [ValidateNotNullOrEmpty()]
        [uri]$Url
    )

    $version = Get-TelligentCommunity $WebsitePath | select -ExpandProperty PlatformVersion

    Write-Progress "Configuration" "Updating Solr Url"
    if ($Version.Major -ge 10) {
        Set-ConnectionString $WebsitePath -Name SearchContentUrl -Value $Url
        Set-ConnectionString $WebsitePath -Name SearchConversationsUrl -Value $Url
    }
    else{
        Add-TelligentOverrideChangeAttribute `
        -XPath /CommunityServer/Search/Solr `
        -Name host `
        -Value $Url `
        -WebsitePath $WebsitePath
    }
}

function Install-TelligentLicense {
    <#
    .SYNOPSIS
        Installs a License file into a Telligent Community
    .PARAMETER LicenseFile
        The XML License file
    .PARAMETER WebsitePath
        The path of the Telligent Community website. If not specified, defaults to the current directory.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ })]
        [string]$WebsitePath,
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({Test-Path $_ -PathType Leaf})]
        [string]$LicenseFile
    )

    $LicenseContent = (gc $LicenseFile) -join [Environment]::NewLine
    $LicenseId = ([xml]$LicenseContent).document.licenseId
    
    $sql = @"
delete from cs_Licenses
GO
insert into cs_Licenses (LicenseID, LicenseValue, InstallDate)
values ('$LicenseId', N'$LicenseContent', getdate())
"@

    Invoke-TelligentSqlCmd -WebsitePath $WebsitePath -Query $sql
}

function Disable-CustomErrors {
    <#
    .SYNOPSIS
        Disables Custom Errors for the ASP.Net website in the specified directory
    .PARAMETER WebsitePath
        The path of the ASP.Net Web Application. If not specified, defaults to the current directory.
    .EXAMPLE
        Disable-CustomErrors
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ })]
        [string]$WebsitePath
    )
    
    Write-Warning 'Disabling Custom Errors poses a security risk. Only do this in non production environments'
    $configPath = Join-Path $WebsitePath web.config | Resolve-Path
    $webConfig = [xml] (get-content $configPath )
    
    $webConfig.configuration.{system.web}.customErrors.mode = 'Off'
    $webConfig.Save($configPath)
}

function Enable-DeveloperMode {
    <#
    .SYNOPSIS
        Enables developer mode for Telligent Community 9.x and above
    .PARAMETER WebsitePath
        The path of the ASP.Net Web Application. If not specified, defaults to the current directory.
    .EXAMPLE
        Enable-DeveloperMode
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ })]
        [string]$WebsitePath
    )
    
    $configPath = Join-Path $WebsitePath web.config | Resolve-Path
    $webConfig = [xml] (Get-Content $configPath )
    
    $webConfig.configuration.appSettings.add |
        ? { $_.key -eq 'EnableDeveloperMode'} |
        % { $_.value = 'true'}        
    
    $webConfig.Save($configPath)
}

function Enable-InternalJobs {
    <#
    .SYNOPSIS
        Enables job server to run as an internal process of the website.
    .PARAMETER WebsitePath
        The path of the ASP.Net Web Application. If not specified, defaults to the current directory.
    .EXAMPLE
        Enable-InternalJobs
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ })]
        [string]$WebsitePath
    )
    
    $configPath = Join-Path $WebsitePath web.config | Resolve-Path
    $webConfig = [xml] (Get-Content $configPath )
    
    $jobSection = [xml]'<add key="RunJobsInternally" value="true" />'
    $webConfig.configuration.appSettings.AppendChild($webConfig.ImportNode($jobSection.DocumentElement, $true)) | out-null
    
    $webConfig.Save($configPath)
}

function Enable-TelligentWindowsAuth {
    <#
    .SYNOPSIS
        Configures IIS to use Windows Authentication for the ASP.Net website
        in the current directory
    .PARAMETER AdminWindowsGroup
        The name of the windows group who should be automatically made Administrators in the community. Defaults to the local Administrators group.
    .PARAMETER EmailDomain
        The email domain to append to a user's username to get their email address if it's not found in Active Directory (USERNAME@EmailDomain).
    .PARAMETER ProfileRefreshInterval
        The interval (in days) at which a user's profile should be updated.
    .PARAMETER WebsitePath
        The path of the Telligent Community website. If not specified, defaults to the current directory.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ })]
        [string]$WebsitePath,
        [ValidateNotNullOrEmpty()]
        [string]$AdminWindowsGroup = "$env:ComputerName\Administrators",
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$EmailDomain,
        [ValidateNotNullOrEmpty()]
        [byte]$ProfileRefreshInterval = 7
    )
    $configPath = Join-Path $WebsitePath web.config | resolve-path 
    $webConfig = [xml] (get-content $configPath )
    
    $webConfig.configuration.{system.web}.authentication.mode = 'Windows'
    $webConfig.Save($configPath)
    
    Add-TelligentOverrideChangeAttribute $WebsitePath `
        -XPath /CommunityServer/Core/extensionModules `
        -Name enabled `
        -Value true


    @{
        adminWindowsGroup = $AdminWindowsGroup;
        emailDomain = "@$($EmailDomain.TrimStart('@'))";
        profileRefreshInterval = $ProfileRefreshInterval
    }.GetEnumerator() |%{
        Add-TelligentOverrideChangeAttribute $WebsitePath `
            -XPath "/CommunityServer/Core/extensionModules/add[@name='WindowsAuthentication']" `
            -Name $_.Key `
            -Value $_.Value
    }

    #If the following fails, ensure default .net version in IIS is set to 4.0
    Get-IISWebsite $WebsitePath |% {
        Set-WebConfigurationProperty -Filter /system.webServer/security/authentication/* `
            -Name enabled `
            -Value false `
            -PSPath IIS:\ `
            -Location $_.Name

        Set-WebConfigurationProperty -Filter /system.webServer/security/authentication/windowsAuthentication `
            -Name enabled `
            -Value true `
            -PSPath IIS:\ `
            -Location $_.Name
    }
}

function Enable-TelligentLdap {
    <#
    .SYNOPSIS
        Enables LDAP integration in a Telligent Community
    .PARAMETER WebsitePath
        The path of the Telligent Community website. If not specified, defaults to the current directory.
    .PARAMETER Server
        The server to use for LDAP. Defaults to the Global Catalog of the AD Forest the server is in.
    .PARAMETER AuthenticationType
        The email domain to append to a user's username to get their email address if it's not found in Active Directory (USERNAME@EmailDomain).
    .PARAMETER Port
        The port to connect to ldap. Defaults to the Global Catalog port.
    .PARAMETER Username
        The username port to connect to ldap with . Defaults to the credntials of Application Pool Identiy.
    .PARAMETER Password
        The password port to connect to ldap with . Defaults to the credntials of Application Pool Identiy.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ })]
        [string]$WebsitePath,
        [string]$Server = 'GC://',
        [string]$AuthenticationType = 'Secure',
        [int]$Port = 3268,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Username,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Password
    )

    #Install Package
    $packagesPath = join-Path $WebsitePath packages.config | Resolve-Path
    $packages = [xml](gc $packagesPath)
    $date = get-date -format yyyy-MM-dd
    $package = [xml]"<Package Name=""Ldap"" Version=""1.0"" DateInstalled=""$date"" Id=""4BF1091D-376C-42b2-B375-E2FE9480E845"" />"
    $packages.DocumentElement.AppendChild($packages.ImportNode($package.DocumentElement, $true)) | out-null
    $packages.Save($packagesPath)

    #Configure Web.config
    $webConfigPath = Join-Path $WebsitePath web.config | resolve-path
    $webConfig = [xml] (get-content $webConfigPath )

    $ldapSection = [xml]'<section name="LdapConnection" type="System.Configuration.NameValueSectionHandler" />'
    $webConfig.configuration.configSections.AppendChild($webConfig.ImportNode($ldapSection.DocumentElement, $true)) | out-null
    $ldapConfiguration= $webConfig.CreateElement("LdapConnection")
    @{
        Server=$Server
        Port=$Port
        UserDN=$Username
        Password = $Password
        Authentication = $AuthenticationType
    }.GetEnumerator() |% {
        $add = $ldapConfiguration.OwnerDocument.CreateElement("add")
        $add.SetAttribute('key', $_.Key)
        $add.SetAttribute('value', $_.Value)
        $ldapConfiguration.AppendChild($add) | out-null
    }
    $webConfig.configuration.AppendChild($ldapConfiguration) | out-null   

    $webConfig.Save($webConfigPath)
}