Install.ps1

Set-StrictMode -Version 2

function Install-TelligentCommunity {
    <#
    .Synopsis
        Sets up a new Telligent Community.
    .Description
        The Install-TelligentCommunity cmdlet automates the process of creating a new Telligent Evolution community.
         
        It takes the installation package, and from it deploys the website to IIS and a creates a new database using
        the scripts from the package. It also sets permissions automatically.
         
        Optionally, you can specify a path to a License file and this will be installed into your community.
         
        By default, authentication between the web and databases uses the Applicaiton Pool Identity, but SQL authentiation
        is used if an explicit username & password are provided.
    .Parameter Name
        The name of the community to create
    .Parameter Package
        The path to the zip package containing the Telligent Community installation files from Telligent Support
    .Parameter Hotfix
        If specified applys the hotfix from the referenced zip file.
    .Parameter WebsitePath
        The directory to place the Telligent Community website in
    .Parameter WebDomain
        The domain name the community will be accessible at
    .Parameter Port
        The port the community will be accessible at. If unspecified, defaults to 80.
    .Parameter ApplicationPool
        The name of the Application Pool to use the community with. If not specified creates a new apppool.
    .Parameter DatabaseServer
        The DNS name of your SQL server. Must be able to connect using Windows Authentication from the current machine. Only supports Default instances.
    .Parameter DatabaseName
        The name of the database to locate your community in. Defaults to the value provided for name.
    .Parameter SqlCredential
        Specifies the SQL Authenticaiton credential the community will use to connect to the database. If not specified, then Windows Authentication is used (prefered). Note, the installer will always connect using Windows Authentication to create the database. These credentials are only used post-installation.
    .Parameter SolrBaseUrl
        The url the base Solr instance to create the new core in.
    .Parameter SolrCoreDir
        TODO.
    .Parameter SolrCoreName
        The name of the Solr Core to create.
    .Parameter AdminPassword
        The password to use for the admin user created during installation.
    .Parameter ApiKey
        When specified, a REST Api Key is created for the admin user with the given value. This is useful for automation scenarios where you want to go and automate creation of content after installation.
    .Parameter FilestoragePath
        The location to install Filestorage to. If not specified, will use the default location ~/filestorage/ in the website.
    .Parameter JobSchedulerPath
        The location to install the Job Server to. If unspecified, the Job server is not installed.
    .Parameter License
        The path to the License XML file to install in the community
    .Example
        Install-TelligentCommunity -name 'Telligent Evolution' -package d:\temp\TelligentCommunity-7.0.1824.27400.zip -webDir "d:\inetpub\TelligentEvolution\" -webdomain "mydomain.com" -searchUrl "http://localhost:8080/solr/"
         
        Description
        -----------
        Local install using Windows Auth to connect to DB
 
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidatePattern('^[a-z0-9\-\._ ]+$')]
        [ValidateNotNullOrEmpty()]
        [string]$Name,

        #Packages
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({Test-Zip $_ })]
        [string]$Package,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [ValidateScript({!$_ -or (Test-Zip $_) })]
        [string]$Hotfix,

        #Web Settings
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ -IsValid })]
        [string]$WebsitePath,

        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$WebDomain,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [uint16]$Port= 80,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$ApplicationPool = $name,

        #Database Connection
        [switch] $NoDatabase,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ ($NoDatabase) -or (Test-SqlServer $_ )})]
        [string]$DatabaseServer = '(local)',

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [ValidatePattern('^[a-z0-9\-\._ ]+$')]
        [ValidateNotNullOrEmpty()]
        [string]$DatabaseName = $name,
        
        #Database Auth
        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [PSCredential]$SqlCredential,

        #Solr Params
        [Parameter(ParameterSetName='SolrCore', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [switch]$SolrCore,

        [Parameter(ParameterSetName='SolrCore', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Invoke-WebRequest $_ -UseBasicParsing -Method HEAD })]
        [uri]$SolrBaseUrl,

        [Parameter(ParameterSetName='SolrCore', Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$SolrCoreDir,

        [Parameter(ParameterSetName='SolrCore', ValueFromPipelineByPropertyName=$true)]
        [ValidatePattern('^[a-z0-9\-\._ ]+$')]
        [ValidateNotNullOrEmpty()]
        [string]$SolrCoreName = $Name,


        #Misc
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
        [ValidatePattern('^[a-z0-9\-\._ ]+$')]
        [ValidateNotNullOrEmpty()]
        [string]$AdminPassword ,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [string]$ApiKey,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [ValidateScript({!$_ -or (Test-Path $_ -PathType Container -IsValid)})]
        [string]$FilestoragePath,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [ValidateScript({ Test-TelligentPath $_ -IsValid -AllowEmpty })]
        [string]$JobSchedulerPath,

        [Parameter(ValueFromPipelineByPropertyName=$true)]
        [ValidateScript({!$_ -or (Test-Path $_ -PathType Leaf)})]
        [string]$License
    )   

    if($JobSchedulerPath -and -not $FilestoragePath) {
        throw 'FilestoragePath must be specified when using JobSchedulerPath'
    }

    $sqlConnectionSettings = @{
        Server = $DatabaseServer
        Database = $DatabaseName
    }

    if(!($NoDatabase) -and (Test-SqlServer @sqlConnectionSettings -EA SilentlyContinue)) {
        throw "Database '$DatabaseName' already exists on server '$DatabaseServer'"
    }


    New-TelligentWebsite `
        -Name $name `
        -Path $WebsitePath `
        -Package $package `
        -HostName $webDomain `
        -ApplicationPool $ApplicationPool `
        -Port $Port `
        -FilestoragePath $FilestoragePath

    if(!$NoDatabase) {
    $sqlAuthSettings = @{}
    if($SqlCredential) {
        $sqlAuthSettings.username = $SqlCredential.Username
        $sqlAuthSettings.password = $SqlCredential.Password
    }
    else {
        $sqlAuthSettings.username = Get-IISAppPoolIdentity $name
    }

        Write-Progress 'Configuration' 'Setting Connection Strings'
        Set-ConnectionString $WebsitePath @sqlConnectionSettings $SqlCredential
    
        New-TelligentDatabase -Package $Package -WebDomain $webDomain -AdminPassword $AdminPassword @sqlConnectionSettings        

        Grant-TelligentDatabaseAccess -CommunityPath $WebsitePath @sqlAuthSettings
    }

    if($Hotfix) {
        Install-TelligentHotfix -WebsitePath $WebsitePath -Package $Hotfix
    }    


    if ($License) {
        Write-Progress 'Configuration' 'Installing License'
        Install-TelligentLicense $WebsitePath $License
    }
    else {
        Write-Warning 'No License installed.'
    }

    $info = Get-TelligentCommunity $WebsitePath 
    if(!$SolrCore) {
        Write-Warning 'No search url specified. Many features will not work until search is configured.'
    }
    else {
        $solrUrl = $SolrBaseUrl.AbsoluteUri.TrimEnd('/')
        Write-Progress 'Search' 'Setting Up Search'
        $solrCoreParams = @{}
        if($info.PlatformVersion.Major -ge 8) {
            $solrCoreParams.ModernCore = $true
        }
        Add-SolrCore $SolrCoreName `
            -package $Package `
            -coreBaseDir $SolrCoreDir `
            -coreAdmin "$solrUrl/admin/cores" `
            @solrCoreParams

        Set-TelligentSolrUrl $WebsitePath "$solrUrl/$SolrCoreName/"
    }

    if($ApiKey) {
        New-CommunityApiKey $WebsitePath $ApiKey -UserId 2100 -Name "Well Known API Key $(Get-Date -f g)"
    }

    if($JobSchedulerPath -and $info.PlatformVersion.Major -ge 6) {
        Install-TelligentJobScheduler -JobSchedulerPath $JobSchedulerPath -Package $Package -WebsitePath $WebsitePath
    }
    else {
        $JobSchedulerPath = ''
    }

    # Ensure Community Info has the latest configuration changes
    Get-TelligentCommunity $WebsitePath |
        Add-Member JobSchedulerPath $JobSchedulerPath -PassThru |
        Add-Member AdminApiKey $ApiKey -PassThru
}

function Install-TelligentHotfix {
    <#
    .SYNOPSIS
        Installs a Telligent Community hotfix
    .Description
        Applies a hotfix to a Telligent Community. It updates the web files, pulls the Database conneciton information from the website and updates the database. If a Job Scheduler path is specified, it also updates the Job Scheduler.
    .PARAMETER WebsitePath
        The path to the Telligent Community website files to apply the hotfix against.
    .PARAMETER Package
        The path to the Telligent Community hotfix installation packgae.
    .PARAMETER JobSchedulerPath
        The path to the community's Job Scheduler.
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ -Web })]
        [string]$WebsitePath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Package,
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ -JobScheduler -AllowEmpty})]
        [string]$JobSchedulerPath
    )
   
    #TODO: Verify hotfix version is higher than current version
    #TODO: Verify major versions of hotfix & existing community are the same
    
    Write-Progress 'Applying Hotfix' 'Updating Web'
    Expand-Zip -Path $Package -destination $WebsitePath -ZipDirectory "Web"
   
    Write-Progress 'Applying Hotfix' 'Updating Database'
    $tempDir = join-path ([System.IO.Path]::GetFullPath($env:TEMP)) ([guid]::NewGuid())
    @('update.sql', 'updates.sql') |% {
        Expand-Zip -Path $package -destination $tempDir -zipFile $_
        $sqlFile = join-path $tempDir $_

        if (Test-Path $sqlFile -PathType Leaf) {
            Write-ProgressFromVerbose 'Applying Hotfix' 'Updating Database' {
                Invoke-TelligentSqlCmd -WebsitePath $WebsitePath -File $sqlFile 
            }
        }
    }

    if($JobSchedulerPath) {
        Write-Progress 'Applying Hotfix' 'Updating Job Scheduler'
        Update-TelligentJobSchedulerFromWeb $WebsitePath $JobSchedulerPath
    }

    Write-Progress 'Applying Hotfix' 'Cleanup'
    Remove-Item $tempDir -Recurse -Force | Out-Null
}

function Uninstall-TelligentCommunity {
    <#
    .SYNOPSIS
        Uninstalls a Telligent Community
    .PARAMETER WebsitePath
        The path to the Telligent Community website
    .PARAMETER JobSchedulerPath
        The path to the Telligent Community Job Scheduler.
    #>

    [CmdletBinding(DefaultParameterSetName='NoService')]
    param(
        [Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true, ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ -Web})]
        [string]$WebsitePath,
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ -JobScheduler })]
        [string]$JobschedulerPath
    )
    process {
        $WebsitePath
        $info = Get-TelligentCommunity $WebsitePath

        #Delete JS
        if($JobSchedulerPath) {
            Write-Progress 'Uninstalling Telligent Community' 'Removing Job Scheduler'
            Remove-Item $JobSchedulerPath -Recurse -Force
        }

        #Delete Solr
        $safeSolrUrl = $info.SolrUrl.TrimEnd('/')
        Write-Progress 'Uninstalling Telligent Community' "Removing Solr instance '$safeSolrUrl'"

        if (Invoke-WebRequest "$safeSolrUrl/admin/" -ErrorAction SilentlyContinue) {
            #Can't auto detect solr location, but we can submit a delete all query to reduce disk usage
            Invoke-WebRequest "$safeSolrUrl/update?Commit=true" `
                -Method POST `
                -Body '<delete><query>*:*</query></delete>' `
                -Headers @{'Content-Type'='application/xml'} | Out-Null

            Write-Warning "All documents deleted from Solr, but instance '$safeSolrUrl' needs to be manually deleted"
        }


        #Delete Filestorage
        Write-Progress 'Uninstalling Telligent Community' 'Removing Filestorage'
        $info.CfsPath |? {Test-Path $_} | Remove-Item -Recurse -Force

        #Delete Web
        Write-Progress 'Uninstalling Telligent Community' 'Removing Website'
        $iisSites = Get-IISWebsite $WebsitePath 
        $appPools = $iisSites | select -ExpandProperty applicationpool -Unique

        $iisSites | Remove-Website
        Get-ChildItem IIS:\AppPools |? Name -in $appPools | Remove-WebAppPool

        Remove-Item $WebsitePath -Recurse -Force

        #Delete SQL
        Write-Progress 'Uninstalling Telligent Community' 'Removing Database'
        Remove-Database -Server $info.DatabaseServer -Database $info.DatabaseName 

        #TODO: Remove from hosts file
    }
}