2019-02-12 13:19:01 -05:00
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
2019-11-29 06:59:40 -05:00
// See LICENSE.txt for license information.
2019-02-12 13:19:01 -05:00
package config
import (
2021-05-19 07:30:26 -04:00
"bytes"
2020-07-15 14:40:36 -04:00
"encoding/json"
2021-05-19 07:30:26 -04:00
"fmt"
2020-09-21 03:28:46 -04:00
"reflect"
2019-02-12 13:19:01 -05:00
"strings"
2023-06-11 01:24:35 -04:00
"github.com/mattermost/mattermost/server/public/model"
"github.com/mattermost/mattermost/server/public/shared/i18n"
"github.com/mattermost/mattermost/server/public/shared/mlog"
"github.com/mattermost/mattermost/server/v8/channels/utils"
2019-02-12 13:19:01 -05:00
)
2021-05-19 07:30:26 -04:00
// marshalConfig converts the given configuration into JSON bytes for persistence.
func marshalConfig ( cfg * model . Config ) ( [ ] byte , error ) {
return json . MarshalIndent ( cfg , "" , " " )
}
2019-02-12 13:19:01 -05:00
// desanitize replaces fake settings with their actual values.
func desanitize ( actual , target * model . Config ) {
2021-07-12 14:05:36 -04:00
if target . LdapSettings . BindPassword != nil && * target . LdapSettings . BindPassword == model . FakeSetting {
2019-02-12 13:19:01 -05:00
* target . LdapSettings . BindPassword = * actual . LdapSettings . BindPassword
}
2021-07-12 14:05:36 -04:00
if * target . FileSettings . PublicLinkSalt == model . FakeSetting {
2019-02-12 13:19:01 -05:00
* target . FileSettings . PublicLinkSalt = * actual . FileSettings . PublicLinkSalt
}
2021-07-12 14:05:36 -04:00
if * target . FileSettings . AmazonS3SecretAccessKey == model . FakeSetting {
2019-02-12 13:19:01 -05:00
target . FileSettings . AmazonS3SecretAccessKey = actual . FileSettings . AmazonS3SecretAccessKey
}
2021-07-12 14:05:36 -04:00
if * target . EmailSettings . SMTPPassword == model . FakeSetting {
2019-02-12 13:19:01 -05:00
target . EmailSettings . SMTPPassword = actual . EmailSettings . SMTPPassword
}
2021-07-12 14:05:36 -04:00
if * target . GitLabSettings . Secret == model . FakeSetting {
2019-02-12 13:19:01 -05:00
target . GitLabSettings . Secret = actual . GitLabSettings . Secret
}
2021-07-12 14:05:36 -04:00
if target . GoogleSettings . Secret != nil && * target . GoogleSettings . Secret == model . FakeSetting {
2020-08-31 02:18:39 -04:00
target . GoogleSettings . Secret = actual . GoogleSettings . Secret
}
2021-07-12 14:05:36 -04:00
if target . Office365Settings . Secret != nil && * target . Office365Settings . Secret == model . FakeSetting {
2020-08-31 02:18:39 -04:00
target . Office365Settings . Secret = actual . Office365Settings . Secret
}
2021-07-12 14:05:36 -04:00
if target . OpenIdSettings . Secret != nil && * target . OpenIdSettings . Secret == model . FakeSetting {
2020-12-08 21:58:37 -05:00
target . OpenIdSettings . Secret = actual . OpenIdSettings . Secret
}
2021-07-12 14:05:36 -04:00
if * target . SqlSettings . DataSource == model . FakeSetting {
2019-02-12 13:19:01 -05:00
* target . SqlSettings . DataSource = * actual . SqlSettings . DataSource
}
2021-07-12 14:05:36 -04:00
if * target . SqlSettings . AtRestEncryptKey == model . FakeSetting {
2020-05-07 08:11:05 -04:00
target . SqlSettings . AtRestEncryptKey = actual . SqlSettings . AtRestEncryptKey
2019-02-12 13:19:01 -05:00
}
2021-07-12 14:05:36 -04:00
if * target . ElasticsearchSettings . Password == model . FakeSetting {
2020-05-07 08:11:05 -04:00
* target . ElasticsearchSettings . Password = * actual . ElasticsearchSettings . Password
2019-02-12 13:19:01 -05:00
}
2020-05-27 09:42:48 -04:00
2020-09-17 14:16:59 -04:00
if len ( target . SqlSettings . DataSourceReplicas ) == len ( actual . SqlSettings . DataSourceReplicas ) {
for i , value := range target . SqlSettings . DataSourceReplicas {
2021-07-12 14:05:36 -04:00
if value == model . FakeSetting {
2020-09-17 14:16:59 -04:00
target . SqlSettings . DataSourceReplicas [ i ] = actual . SqlSettings . DataSourceReplicas [ i ]
}
}
2020-05-27 09:42:48 -04:00
}
2020-09-17 14:16:59 -04:00
if len ( target . SqlSettings . DataSourceSearchReplicas ) == len ( actual . SqlSettings . DataSourceSearchReplicas ) {
for i , value := range target . SqlSettings . DataSourceSearchReplicas {
2021-07-12 14:05:36 -04:00
if value == model . FakeSetting {
2020-09-17 14:16:59 -04:00
target . SqlSettings . DataSourceSearchReplicas [ i ] = actual . SqlSettings . DataSourceSearchReplicas [ i ]
}
}
2020-05-27 09:42:48 -04:00
}
2020-08-20 09:01:22 -04:00
2021-08-12 05:49:16 -04:00
if * target . MessageExportSettings . GlobalRelaySettings . SMTPPassword == model . FakeSetting {
* target . MessageExportSettings . GlobalRelaySettings . SMTPPassword = * actual . MessageExportSettings . GlobalRelaySettings . SMTPPassword
2020-08-20 09:01:22 -04:00
}
2020-08-31 02:18:39 -04:00
2021-07-12 14:05:36 -04:00
if * target . ServiceSettings . SplitKey == model . FakeSetting {
2020-10-29 18:54:39 -04:00
* target . ServiceSettings . SplitKey = * actual . ServiceSettings . SplitKey
}
2024-11-11 05:43:26 -05:00
for id , settings := range target . PluginSettings . Plugins {
for k , v := range settings {
if v == model . FakeSetting {
settings [ k ] = actual . PluginSettings . Plugins [ id ] [ k ]
}
}
}
2019-02-12 13:19:01 -05:00
}
2021-02-12 04:22:27 -05:00
// fixConfig patches invalid or missing data in the configuration.
func fixConfig ( cfg * model . Config ) {
2019-02-12 13:19:01 -05:00
// Ensure SiteURL has no trailing slash.
if strings . HasSuffix ( * cfg . ServiceSettings . SiteURL , "/" ) {
* cfg . ServiceSettings . SiteURL = strings . TrimRight ( * cfg . ServiceSettings . SiteURL , "/" )
}
// Ensure the directory for a local file store has a trailing slash.
2021-07-12 14:05:36 -04:00
if * cfg . FileSettings . DriverName == model . ImageDriverLocal {
2020-08-04 07:58:48 -04:00
if * cfg . FileSettings . Directory != "" && ! strings . HasSuffix ( * cfg . FileSettings . Directory , "/" ) {
2019-02-12 13:19:01 -05:00
* cfg . FileSettings . Directory += "/"
}
}
2024-04-29 05:23:01 -04:00
fixInvalidLocales ( cfg )
2019-02-12 13:19:01 -05:00
}
2024-04-29 05:23:01 -04:00
// fixInvalidLocales checks and corrects the given config for invalid locale-related settings.
func fixInvalidLocales ( cfg * model . Config ) bool {
2019-02-12 13:19:01 -05:00
var changed bool
2021-02-26 02:12:49 -05:00
locales := i18n . GetSupportedLocales ( )
2019-02-12 13:19:01 -05:00
if _ , ok := locales [ * cfg . LocalizationSettings . DefaultServerLocale ] ; ! ok {
2023-09-26 12:49:27 -04:00
mlog . Warn ( "DefaultServerLocale must be one of the supported locales. Setting DefaultServerLocale to en as default value." , mlog . String ( "locale" , * cfg . LocalizationSettings . DefaultServerLocale ) )
2021-07-12 14:05:36 -04:00
* cfg . LocalizationSettings . DefaultServerLocale = model . DefaultLocale
2019-02-12 13:19:01 -05:00
changed = true
}
if _ , ok := locales [ * cfg . LocalizationSettings . DefaultClientLocale ] ; ! ok {
2023-09-26 12:49:27 -04:00
mlog . Warn ( "DefaultClientLocale must be one of the supported locales. Setting DefaultClientLocale to en as default value." , mlog . String ( "locale" , * cfg . LocalizationSettings . DefaultClientLocale ) )
2021-07-12 14:05:36 -04:00
* cfg . LocalizationSettings . DefaultClientLocale = model . DefaultLocale
2019-02-12 13:19:01 -05:00
changed = true
}
2021-01-25 05:15:17 -05:00
if * cfg . LocalizationSettings . AvailableLocales != "" {
2019-02-12 13:19:01 -05:00
isDefaultClientLocaleInAvailableLocales := false
2025-07-18 06:54:51 -04:00
for word := range strings . SplitSeq ( * cfg . LocalizationSettings . AvailableLocales , "," ) {
2019-02-12 13:19:01 -05:00
if _ , ok := locales [ word ] ; ! ok {
* cfg . LocalizationSettings . AvailableLocales = ""
isDefaultClientLocaleInAvailableLocales = true
mlog . Warn ( "AvailableLocales must include DefaultClientLocale. Setting AvailableLocales to all locales as default value." )
changed = true
break
}
if word == * cfg . LocalizationSettings . DefaultClientLocale {
isDefaultClientLocaleInAvailableLocales = true
}
}
availableLocales := * cfg . LocalizationSettings . AvailableLocales
if ! isDefaultClientLocaleInAvailableLocales {
availableLocales += "," + * cfg . LocalizationSettings . DefaultClientLocale
mlog . Warn ( "Adding DefaultClientLocale to AvailableLocales." )
changed = true
}
* cfg . LocalizationSettings . AvailableLocales = strings . Join ( utils . RemoveDuplicatesFromStringArray ( strings . Split ( availableLocales , "," ) ) , "," )
}
return changed
}
2019-03-21 15:46:38 -04:00
// Merge merges two configs together. The receiver's values are overwritten with the patch's
// values except when the patch's values are nil.
func Merge ( cfg * model . Config , patch * model . Config , mergeConfig * utils . MergeConfig ) ( * model . Config , error ) {
2023-08-21 05:29:30 -04:00
return utils . Merge ( cfg , patch , mergeConfig )
2019-03-21 15:46:38 -04:00
}
2019-09-26 01:17:39 -04:00
2020-12-16 14:45:17 -05:00
func IsDatabaseDSN ( dsn string ) bool {
Remove vestigial MySQL support (#34865)
* Remove legacy quoteColumnName() utility
Since Mattermost only supports PostgreSQL, the quoteColumnName() helper
that was designed to handle database-specific column quoting is no longer
needed. The function was a no-op that simply returned the column name
unchanged.
Remove the function from utils.go and update status_store.go to use
the "Manual" column name directly.
* Remove legacy driver checks from store.go
Since Mattermost only supports PostgreSQL, remove conditional checks
for different database drivers:
- Simplify specialSearchChars() to always return PostgreSQL-compatible chars
- Remove driver check from computeBinaryParam()
- Remove driver check from computeDefaultTextSearchConfig()
- Simplify GetDbVersion() to use PostgreSQL syntax directly
- Remove switch statement from ensureMinimumDBVersion()
- Remove unused driver parameter from versionString()
* Remove MySQL alternatives for batch delete operations
Since Mattermost only supports PostgreSQL, remove the MySQL-specific
DELETE...LIMIT syntax and keep only the PostgreSQL array-based approach:
- reaction_store.go: Use PostgreSQL array syntax for PermanentDeleteBatch
- file_info_store.go: Use PostgreSQL array syntax for PermanentDeleteBatch
- preference_store.go: Use PostgreSQL tuple IN subquery for DeleteInvalidVisibleDmsGms
* Remove MySQL alternatives for UPDATE...FROM syntax
Since Mattermost only supports PostgreSQL, remove the MySQL-specific
UPDATE syntax that joins tables differently:
- thread_store.go: Use PostgreSQL UPDATE...FROM syntax in
MarkAllAsReadByChannels and MarkAllAsReadByTeam
- post_store.go: Use PostgreSQL UPDATE...FROM syntax in deleteThreadFiles
* Remove MySQL alternatives for JSON and subquery operations
Since Mattermost only supports PostgreSQL, remove the MySQL-specific
JSON and subquery syntax:
- thread_store.go: Use PostgreSQL JSONB operators for updating participants
- access_control_policy_store.go: Use PostgreSQL JSONB @> operator for
querying JSON imports
- session_store.go: Use PostgreSQL subquery syntax for Cleanup
- job_store.go: Use PostgreSQL subquery syntax for Cleanup
* Remove MySQL alternatives for CTE queries
Since Mattermost only supports PostgreSQL, simplify code that
uses CTEs (Common Table Expressions):
- channel_store.go: Remove MySQL CASE-based fallback in
UpdateLastViewedAt and use PostgreSQL CTE exclusively
- draft_store.go: Remove driver checks in DeleteEmptyDraftsByCreateAtAndUserId,
DeleteOrphanDraftsByCreateAtAndUserId, and determineMaxDraftSize
* Remove driver checks in migrate.go and schema_dump.go
Simplify migration code to use PostgreSQL driver directly since
PostgreSQL is the only supported database.
* Remove driver checks in sqlx_wrapper.go
Always apply lowercase named parameter transformation since PostgreSQL
is the only supported database.
* Remove driver checks in user_store.go
Simplify user store functions to use PostgreSQL-only code paths:
- Remove isPostgreSQL parameter from helper functions
- Use LEFT JOIN pattern instead of subqueries for bot filtering
- Always use case-insensitive LIKE with lower() for search
- Remove MySQL-specific role filtering alternatives
* Remove driver checks in post_store.go
Simplify post_store.go to use PostgreSQL-only code paths:
- Inline getParentsPostsPostgreSQL into getParentsPosts
- Use PostgreSQL TO_CHAR/TO_TIMESTAMP for date formatting in analytics
- Use PostgreSQL array syntax for batch deletes
- Simplify determineMaxPostSize to always use information_schema
- Use PostgreSQL jsonb subtraction for thread participants
- Always execute RefreshPostStats (PostgreSQL materialized views)
- Use materialized views for AnalyticsPostCountsByDay
- Simplify AnalyticsPostCountByTeam to always use countByTeam
* Remove driver checks in channel_store.go
Simplify channel_store.go to use PostgreSQL-only code paths:
- Always use sq.Dollar.ReplacePlaceholders for UNION queries
- Use PostgreSQL LEFT JOIN for retention policy exclusion
- Use PostgreSQL jsonb @> operator for access control policy imports
- Simplify buildLIKEClause to always use LOWER() for case-insensitive search
- Simplify buildFulltextClauseX to always use PostgreSQL to_tsvector/to_tsquery
- Simplify searchGroupChannelsQuery to use ARRAY_TO_STRING/ARRAY_AGG
* Remove driver checks in file_info_store.go
Simplify file_info_store.go to use PostgreSQL-only code paths:
- Always use PostgreSQL to_tsvector/to_tsquery for file search
- Use file_stats materialized view for CountAll()
- Use file_stats materialized view for GetStorageUsage() when not including deleted
- Always execute RefreshFileStats() for materialized view refresh
* Remove driver checks in attributes_store.go
Simplify attributes_store.go to use PostgreSQL-only code paths:
- Always execute RefreshAttributes() for materialized view refresh
- Remove isPostgreSQL parameter from generateSearchQueryForExpression
- Always use PostgreSQL LOWER() LIKE LOWER() syntax for case-insensitive search
* Remove driver checks in retention_policy_store.go
Simplify retention_policy_store.go to use PostgreSQL-only code paths:
- Remove isPostgres parameter from scanRetentionIdsForDeletion
- Always use pq.Array for scanning retention IDs
- Always use pq.Array for inserting retention IDs
- Remove unused json import
* Remove driver checks in property stores
Simplify property_field_store.go and property_value_store.go to use
PostgreSQL-only code paths:
- Always use PostgreSQL type casts (::text, ::jsonb, ::bigint, etc.)
- Remove isPostgres variable and conditionals
* Remove driver checks in channel_member_history_store.go
Simplify PermanentDeleteBatch to use PostgreSQL-only code path:
- Always use ctid-based subquery for DELETE with LIMIT
* Remove remaining driver checks in user_store.go
Simplify user_store.go to use PostgreSQL-only code paths:
- Use LEFT JOIN for bot exclusion in AnalyticsActiveCountForPeriod
- Use LEFT JOIN for bot exclusion in IsEmpty
* Simplify fulltext search by consolidating buildFulltextClause functions
Remove convertMySQLFullTextColumnsToPostgres and consolidate
buildFulltextClause and buildFulltextClauseX into a single function
that takes variadic column arguments and returns sq.Sqlizer.
* Simplify SQL stores leveraging PostgreSQL-only support
- Simplify UpdateMembersRole in channel_store.go and team_store.go
to use UPDATE...RETURNING instead of SELECT + UPDATE
- Simplify GetPostReminders in post_store.go to use DELETE...RETURNING
- Simplify DeleteOrphanedRows queries by removing MySQL workarounds
for subquery locking issues
- Simplify UpdateUserLastSyncAt to use UPDATE...FROM...RETURNING
instead of fetching user first then updating
- Remove MySQL index hint workarounds in ORDER BY clauses
- Update outdated comments referencing MySQL
- Consolidate buildFulltextClause and remove convertMySQLFullTextColumnsToPostgres
* Remove MySQL-specific test artifacts
- Delete unused MySQLStopWords variable and stop_word.go file
- Remove redundant testSearchEmailAddressesWithQuotes test
(already covered by testSearchEmailAddresses)
- Update comment that referenced MySQL query planning
* Remove MySQL references from server code outside sqlstore
- Update config example and DSN parsing docs to reflect PostgreSQL-only support
- Remove mysql:// scheme check from IsDatabaseDSN
- Simplify SanitizeDataSource to only handle PostgreSQL
- Remove outdated MySQL comments from model and plugin code
* Remove MySQL references from test files
- Update test DSNs to use PostgreSQL format
- Remove dead mysql-replica flag and replicaFlag variable
- Simplify tests that had MySQL/PostgreSQL branches
* Update docs and test config to use PostgreSQL
- Update mmctl config set example to use postgres driver
- Update test-config.json to use PostgreSQL DSN format
* Remove MySQL migration scripts, test data, and docker image
Delete MySQL-related files that are no longer needed:
- ESR upgrade scripts (esr.*.mysql.*.sql)
- MySQL schema dumps (mattermost-mysql-*.sql)
- MySQL replication test scripts (replica-*.sh, mysql-migration-test.sh)
- MySQL test warmup data (mysql_migration_warmup.sql)
- MySQL docker image reference from mirror-docker-images.json
* Remove MySQL references from webapp
- Simplify minimumHashtagLength description to remove MySQL-specific configuration note
- Remove unused HIDE_MYSQL_STATS_NOTIFICATION preference constant
- Update en.json i18n source file
* clean up e2e-tests
* rm server/tests/template.load
* Use teamMemberSliceColumns() in UpdateMembersRole RETURNING clause
Refactor to use the existing helper function instead of hardcoding
the column names, ensuring consistency if the columns are updated.
* u.id -> u.Id
* address code review feedback
---------
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-01-20 16:01:59 -05:00
return strings . HasPrefix ( dsn , "postgres://" ) ||
2022-01-12 07:23:56 -05:00
strings . HasPrefix ( dsn , "postgresql://" )
2020-12-16 14:45:17 -05:00
}
2023-05-15 10:37:48 -04:00
func isJSONMap ( data [ ] byte ) bool {
2022-07-05 02:46:50 -04:00
var m map [ string ] any
2023-05-15 10:37:48 -04:00
err := json . Unmarshal ( data , & m )
return err == nil
2020-07-15 14:40:36 -04:00
}
2022-07-05 02:46:50 -04:00
func GetValueByPath ( path [ ] string , obj any ) ( any , bool ) {
2020-09-21 03:28:46 -04:00
r := reflect . ValueOf ( obj )
var val reflect . Value
if r . Kind ( ) == reflect . Map {
val = r . MapIndex ( reflect . ValueOf ( path [ 0 ] ) )
if val . IsValid ( ) {
val = val . Elem ( )
}
} else {
val = r . FieldByName ( path [ 0 ] )
}
if ! val . IsValid ( ) {
return nil , false
}
switch {
case len ( path ) == 1 :
return val . Interface ( ) , true
case val . Kind ( ) == reflect . Struct :
return GetValueByPath ( path [ 1 : ] , val . Interface ( ) )
case val . Kind ( ) == reflect . Map :
remainingPath := strings . Join ( path [ 1 : ] , "." )
mapIter := val . MapRange ( )
for mapIter . Next ( ) {
key := mapIter . Key ( ) . String ( )
if strings . HasPrefix ( remainingPath , key ) {
i := strings . Count ( key , "." ) + 2 // number of dots + a dot on each side
mapVal := mapIter . Value ( )
// if no sub field path specified, return the object
if len ( path [ i : ] ) == 0 {
return mapVal . Interface ( ) , true
}
data := mapVal . Interface ( )
if mapVal . Kind ( ) == reflect . Ptr {
data = mapVal . Elem ( ) . Interface ( ) // if value is a pointer, dereference it
}
// pass subpath
return GetValueByPath ( path [ i : ] , data )
}
}
}
return nil , false
}
2021-05-19 07:30:26 -04:00
func equal ( oldCfg , newCfg * model . Config ) ( bool , error ) {
oldCfgBytes , err := json . Marshal ( oldCfg )
if err != nil {
return false , fmt . Errorf ( "failed to marshal old config: %w" , err )
}
newCfgBytes , err := json . Marshal ( newCfg )
if err != nil {
return false , fmt . Errorf ( "failed to marshal new config: %w" , err )
}
return ! bytes . Equal ( oldCfgBytes , newCfgBytes ) , nil
}