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)]
        [ValidateNotNullOrEmpty()]
        [alias('ServerInstance')]
        [string]$Server,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$Database,
        [PSCredential]$SqlCredentials,
        [string]$ConfigurationFile = 'connectionstrings.config',
        [string]$ConnectionStringName = 'SiteSqlServer'
    )

    if ($SqlCredentials){
        $connectionString = "Server=$Server;Database=$Database;uid=$($SqlCredentials.UserName);pwd=$($SqlCredentials.Password);"
    }
    else {
        $connectionString = "Server=$Server;Database=$Database;Trusted_Connection=yes;"
    }
    
    $path = Join-Path $WebsitePath connectionstrings.config | Resolve-Path | select -ExpandProperty ProviderPath
    $connectionStrings = [xml](gc $path)  
    $connectionStrings.connectionStrings.add |
        ? { $_.name -eq $ConnectionStringName} |
        % { $_.connectionString = $connectionString}
    $connectionStrings.Save($path)     
}

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 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 7) {
        Add-TelligentOverrideChangeAttribute $WebsitePath `
            -XPath "/CommunityServer/CentralizedFileStorage/fileStoreGroup[@name='default']" `
            -Name basePath `
            -Value $FilestoragePath
    }
    else {
        $csConfig = Merge-CommunityConfigurationFile $WebsitePath communityserver
        $csConfig.CommunityServer.CentralizedFileStorage.fileStore.name |% {
            Add-TelligentOverrideChangeAttribute $WebsitePath `
                -XPath "/CommunityServer/CentralizedFileStorage/fileStore[@name='$_']" `
                -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
    )

    Write-Progress "Configuration" "Updating Solr Url"
    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 = @"
insert into cs_Licenses (LicenseID, LicenseValue, InstallDate)
values ('$LicenseId', N'$LicenseContent', getdate())
"@

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

function Register-TelligentTasksInWebProcess {
    <#
    .SYNOPSIS
        Registers the Job Scheduler tasks in the web process of the Telligent Community instance in the current directory for a development environment
    .DESCRIPTION
        Do NOT use this in a production environment
         
        In production environments, the Job Scheduler must be used to offload tasks from the Web Server and to ensure tasks continue to run through Application Pool recycles, as well as to avoid conflicts in a multi server environment
    .PARAMETER WebsitePath
        The path of the Telligent Community website. If not specified, defaults to the current directory.
    .PARAMETER Package
        The path to the zip package containing the Telligent Community installation files from Telligent Support
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({ Test-TelligentPath $_ })]
        [string]$WebsitePath,
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript({Test-Zip $_ })]
        [string]$Package
    )
    $info = Get-TelligentCommunity $WebsitePath
    if($info.PlatformVersion -lt 5.6){
        Write-Error 'Job Scheduler not supported on 5.5 or below'
    }
    elseif($info.PlatformVersion.Major -ge 8) { 
        Write-Error 'Jobs can no longer be run in the web process in 8.0 or higher'
    }
    else {
        Write-Warning "Registering Tasks in the web process is not supported for production environments. Only do this in non production environments"

        $tempDir = Join-Path $env:temp ([guid]::NewGuid())
        Expand-Zip -Path $Package -destination $tempDir -ZipDirectory Tasks -zipFile tasks.config
        $webTasksPath = Join-Path $WebsitePath tasks.config | Resolve-Path
        if ($webTasksPath) {
            $webTasks = [xml](gc $webTasksPath)
            if($webTasks.jobs.cron){
                #6.0+
                $jsTasks = [xml](Join-Path $tempDir tasks.config | Resolve-Path | Get-Content)
                $jsTasks.jobs.cron.jobs.job |% {
                    $webTasks.jobs.cron.jobs.AppendChild($webTasks.ImportNode($_, $true)) | Out-Null
                }
                $webTasks.jobs.dynamic.mode = 'Server'
            }
            else {
                #5.6
                $tasks = [xml]$tasks5x
                $tasks.jobs.job |% {
                    $webTasks.scheduler.jobs.AppendChild($webTasks.ImportNode($_, $true)) | out-null
                }
                $version = (Get-TelligentCommunity $WebsitePath).PlatformVersion
                if($version.Revision -ge 17537) {
                    $reindexNode = [xml]'<job schedule="30 * * * * ? *" type="CommunityServer.Search.SiteReindexJob, CommunityServer.Search" />'
                    $webTasks.scheduler.jobs.AppendChild($webTasks.ImportNode($reindexNode.job, $true)) | out-null
                }
            }

            $webTasks.Save($webTasksPath)
        }
    }
}

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-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)
}

$tasks5x = data {@"
<jobs>
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Components.AnonymousUserJob, CommunityServer.Components" />
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Components.ReferralsJob, CommunityServer.Components" />
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Components.ViewsJob, CommunityServer.Components" />
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Wikis.Components.PageViewsJob, CommunityServer.Wikis" />
 
    <job schedule="0 */1 * * * ? *" type="CommunityServer.MailRoom.Components.EmailJob, CommunityServer.MailGateway.MailRoom">
        <settings>
            <add key="failureInterval" value="1" />
            <add key="numberOfTries" value="10" />
        </settings>
    </job>
    <job schedule="0 */1 * * * ? *" type="CommunityServer.MailRoom.Components.BulkEmailJob, CommunityServer.MailGateway.MailRoom">
        <settings>
            <add key="failureInterval" value="1" />
            <add key="numberOfTries" value="10" />
        </settings>
    </job>
    <job schedule="0 */1 * * * ? *" type="CommunityServer.Search.Tasks.SearchIndexingContentHandlerTask, CommunityServer.Search">
        <settings>
            <add key="documentsPerRun" value="100" />
            <add key="handler-1" value="CommunityServer.Search.UserContentHandler, CommunityServer.Search" />
            <add key="handler-2" value="CommunityServer.Search.GroupContentHandler, CommunityServer.Search" />
            <add key="handler-3" value="CommunityServer.Search.ForumContentHandler, CommunityServer.Search" />
            <add key="handler-4" value="CommunityServer.Search.WeblogContentHandler, CommunityServer.Search" />
            <add key="handler-5" value="CommunityServer.Search.ContentFragmentPageContentHandler, CommunityServer.Search" />
            <add key="handler-6" value="CommunityServer.Search.MediaGalleryContentHandler, CommunityServer.Search" />
            <add key="handler-7" value="CommunityServer.Search.WikiContentHandler, CommunityServer.Search" />
            <add key="handler-8" value="CommunityServer.Search.WeblogPostContentHandler, CommunityServer.Search" />
            <add key="handler-9" value="CommunityServer.Search.MediaGalleryPostContentHandler, CommunityServer.Search" />
            <add key="handler-10" value="CommunityServer.Search.WikiPageContentHandler, CommunityServer.Search" />
            <add key="handler-11" value="CommunityServer.Search.ForumPostContentHandler, CommunityServer.Search" />
        </settings>
    </job>
    <job schedule="0 0 3 ? * SUN,WED" type="CommunityServer.Search.Solr.IndexOptimizationJob, CommunityServer.Search.Providers" />
    <job schedule="0 */2 * * * ? *" type="CommunityServer.Components.TagCleanupJob, CommunityServer.Components">
        <settings>
            <add key="applications" value="forum" />
        </settings>
    </job>
    <job schedule="0 */2 * * * ? *" type="CommunityServer.Blogs.Components.RecentContentJob, CommunityServer.Blogs" />
    <job schedule="0 */2 * * * ? *" type="CommunityServer.Components.CalculateTagCountsJob, CommunityServer.Components" />
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Components.SiteStatisticsJob, CommunityServer.Components" />
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Components.EventLogJob, CommunityServer.Components" />
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Blogs.Components.ModeratedFeedbackNotificationJob, CommunityServer.Blogs" />
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Blogs.Components.GenerateWeblogYearMonthDayListJob, CommunityServer.Blogs" />
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Components.TemporaryUserTokenExpirationTask, CommunityServer.Components" />
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Components.TemporaryStoreExpirationTask, CommunityServer.Components" />
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Components.CalculateSectionTotalsJob, CommunityServer.Components" />
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Blogs.Components.RollerBlogUpdater, CommunityServer.Blogs" />
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Components.PostAttachmentCleanupJob, CommunityServer.Components">
        <settings>
            <add key="expiresAfterHours" value="2" />
        </settings>
    </job>
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Components.ThemeConfigurationPreviewCleanupJob, CommunityServer.Components">
        <settings>
            <add key="expiresAfterHours" value="2" />
        </settings>
    </job>
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Components.UserInvitationExpirationJob, CommunityServer.Components">
        <settings>
            <add key="expirationDays" value="30" />
        </settings>
    </job>
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Blogs.Components.DeleteStaleSpamCommentsJob, CommunityServer.Blogs">
        <settings>
            <add key="expirationDays" value="2" />
        </settings>
    </job>
    <job schedule="0 */3 * * * ? *" type="CommunityServer.Components.MultipleFileUploadCleanupJob, CommunityServer.Components">
        <settings>
            <add key="expiresAfterHours" value="2" />
        </settings>
    </job>
    <job schedule="0 */5 * * * ? *" type="CommunityServer.Components.LdapSyncJob, CommunityServer.Components" />
    <!-- Enable this task to enable background deletion of old activity messages.
                    expirationDays (int) = messages older than this number of days can be deleted
                    minUserMessages (int) = the minimum number of messages a user should retain. -->
    <!--<job schedule="0 */3 * * * ? *" type="CommunityServer.Messages.Tasks.MessageRemovalTask, CommunityServer.Messages">
            <settings>
                <add key="expirationDays" value="30" />
                <add key="minUserMessages" value="30" />
            </settings>
        </job>-->
 
 
</jobs>
"@
}