123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554 |
- // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
- // See LICENSE.txt for license information.
- package api4
- import (
- "net/http"
- "os"
- "strings"
- "testing"
- "github.com/mattermost/mattermost-server/v5/model"
- "github.com/stretchr/testify/assert"
- "github.com/stretchr/testify/require"
- )
- func TestGetConfig(t *testing.T) {
- th := Setup(t)
- defer th.TearDown()
- Client := th.Client
- _, resp := Client.GetConfig()
- CheckForbiddenStatus(t, resp)
- th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
- cfg, resp := client.GetConfig()
- CheckNoError(t, resp)
- require.NotEqual(t, "", cfg.TeamSettings.SiteName)
- if *cfg.LdapSettings.BindPassword != model.FAKE_SETTING && len(*cfg.LdapSettings.BindPassword) != 0 {
- require.FailNow(t, "did not sanitize properly")
- }
- require.Equal(t, model.FAKE_SETTING, *cfg.FileSettings.PublicLinkSalt, "did not sanitize properly")
- if *cfg.FileSettings.AmazonS3SecretAccessKey != model.FAKE_SETTING && len(*cfg.FileSettings.AmazonS3SecretAccessKey) != 0 {
- require.FailNow(t, "did not sanitize properly")
- }
- if *cfg.EmailSettings.SMTPPassword != model.FAKE_SETTING && len(*cfg.EmailSettings.SMTPPassword) != 0 {
- require.FailNow(t, "did not sanitize properly")
- }
- if *cfg.GitLabSettings.Secret != model.FAKE_SETTING && len(*cfg.GitLabSettings.Secret) != 0 {
- require.FailNow(t, "did not sanitize properly")
- }
- require.Equal(t, model.FAKE_SETTING, *cfg.SqlSettings.DataSource, "did not sanitize properly")
- require.Equal(t, model.FAKE_SETTING, *cfg.SqlSettings.AtRestEncryptKey, "did not sanitize properly")
- if !strings.Contains(strings.Join(cfg.SqlSettings.DataSourceReplicas, " "), model.FAKE_SETTING) && len(cfg.SqlSettings.DataSourceReplicas) != 0 {
- require.FailNow(t, "did not sanitize properly")
- }
- if !strings.Contains(strings.Join(cfg.SqlSettings.DataSourceSearchReplicas, " "), model.FAKE_SETTING) && len(cfg.SqlSettings.DataSourceSearchReplicas) != 0 {
- require.FailNow(t, "did not sanitize properly")
- }
- })
- }
- func TestReloadConfig(t *testing.T) {
- th := Setup(t)
- defer th.TearDown()
- Client := th.Client
- t.Run("as system user", func(t *testing.T) {
- ok, resp := Client.ReloadConfig()
- CheckForbiddenStatus(t, resp)
- require.False(t, ok, "should not Reload the config due no permission.")
- })
- t.Run("as system admin", func(t *testing.T) {
- ok, resp := th.SystemAdminClient.ReloadConfig()
- CheckNoError(t, resp)
- require.True(t, ok, "should Reload the config")
- })
- t.Run("as restricted system admin", func(t *testing.T) {
- th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
- ok, resp := Client.ReloadConfig()
- CheckForbiddenStatus(t, resp)
- require.False(t, ok, "should not Reload the config due no permission.")
- })
- }
- func TestUpdateConfig(t *testing.T) {
- th := Setup(t)
- defer th.TearDown()
- Client := th.Client
- cfg, resp := th.SystemAdminClient.GetConfig()
- CheckNoError(t, resp)
- _, resp = Client.UpdateConfig(cfg)
- CheckForbiddenStatus(t, resp)
- th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
- SiteName := th.App.Config().TeamSettings.SiteName
- *cfg.TeamSettings.SiteName = "MyFancyName"
- cfg, resp = client.UpdateConfig(cfg)
- CheckNoError(t, resp)
- require.Equal(t, "MyFancyName", *cfg.TeamSettings.SiteName, "It should update the SiteName")
- //Revert the change
- cfg.TeamSettings.SiteName = SiteName
- cfg, resp = client.UpdateConfig(cfg)
- CheckNoError(t, resp)
- require.Equal(t, SiteName, cfg.TeamSettings.SiteName, "It should update the SiteName")
- t.Run("Should set defaults for missing fields", func(t *testing.T) {
- _, appErr := th.SystemAdminClient.DoApiPut(th.SystemAdminClient.GetConfigRoute(), `{"ServiceSettings":{}}`)
- require.Nil(t, appErr)
- })
- t.Run("Should fail with validation error if invalid config setting is passed", func(t *testing.T) {
- //Revert the change
- badcfg := cfg.Clone()
- badcfg.PasswordSettings.MinimumLength = model.NewInt(4)
- badcfg.PasswordSettings.MinimumLength = model.NewInt(4)
- _, resp = client.UpdateConfig(badcfg)
- CheckBadRequestStatus(t, resp)
- CheckErrorMessage(t, resp, "model.config.is_valid.password_length.app_error")
- })
- t.Run("Should not be able to modify PluginSettings.EnableUploads", func(t *testing.T) {
- oldEnableUploads := *th.App.Config().PluginSettings.EnableUploads
- *cfg.PluginSettings.EnableUploads = !oldEnableUploads
- cfg, resp = client.UpdateConfig(cfg)
- CheckNoError(t, resp)
- assert.Equal(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads)
- assert.Equal(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads)
- cfg.PluginSettings.EnableUploads = nil
- cfg, resp = client.UpdateConfig(cfg)
- CheckNoError(t, resp)
- assert.Equal(t, oldEnableUploads, *cfg.PluginSettings.EnableUploads)
- assert.Equal(t, oldEnableUploads, *th.App.Config().PluginSettings.EnableUploads)
- })
- t.Run("Should not be able to modify PluginSettings.SignaturePublicKeyFiles", func(t *testing.T) {
- oldPublicKeys := th.App.Config().PluginSettings.SignaturePublicKeyFiles
- cfg.PluginSettings.SignaturePublicKeyFiles = append(cfg.PluginSettings.SignaturePublicKeyFiles, "new_signature")
- cfg, resp = client.UpdateConfig(cfg)
- CheckNoError(t, resp)
- assert.Equal(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles)
- assert.Equal(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles)
- cfg.PluginSettings.SignaturePublicKeyFiles = nil
- cfg, resp = client.UpdateConfig(cfg)
- CheckNoError(t, resp)
- assert.Equal(t, oldPublicKeys, cfg.PluginSettings.SignaturePublicKeyFiles)
- assert.Equal(t, oldPublicKeys, th.App.Config().PluginSettings.SignaturePublicKeyFiles)
- })
- })
- t.Run("System Admin should not be able to clear Site URL", func(t *testing.T) {
- siteURL := cfg.ServiceSettings.SiteURL
- defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = siteURL })
- nonEmptyURL := "http://localhost"
- cfg.ServiceSettings.SiteURL = &nonEmptyURL
- // Set the SiteURL
- cfg, resp = th.SystemAdminClient.UpdateConfig(cfg)
- CheckNoError(t, resp)
- require.Equal(t, nonEmptyURL, *cfg.ServiceSettings.SiteURL)
- // Check that the Site URL can't be cleared
- cfg.ServiceSettings.SiteURL = sToP("")
- cfg, resp = th.SystemAdminClient.UpdateConfig(cfg)
- CheckBadRequestStatus(t, resp)
- CheckErrorMessage(t, resp, "api.config.update_config.clear_siteurl.app_error")
- // Check that the Site URL wasn't cleared
- cfg, resp = th.SystemAdminClient.GetConfig()
- CheckNoError(t, resp)
- require.Equal(t, nonEmptyURL, *cfg.ServiceSettings.SiteURL)
- })
- }
- func TestUpdateConfigMessageExportSpecialHandling(t *testing.T) {
- th := Setup(t)
- defer th.TearDown()
- messageExportEnabled := *th.App.Config().MessageExportSettings.EnableExport
- messageExportTimestamp := *th.App.Config().MessageExportSettings.ExportFromTimestamp
- defer th.App.UpdateConfig(func(cfg *model.Config) {
- *cfg.MessageExportSettings.EnableExport = messageExportEnabled
- *cfg.MessageExportSettings.ExportFromTimestamp = messageExportTimestamp
- })
- th.App.UpdateConfig(func(cfg *model.Config) {
- *cfg.MessageExportSettings.EnableExport = false
- *cfg.MessageExportSettings.ExportFromTimestamp = int64(0)
- })
- // Turn it on, timestamp should be updated.
- cfg, resp := th.SystemAdminClient.GetConfig()
- CheckNoError(t, resp)
- *cfg.MessageExportSettings.EnableExport = true
- cfg, resp = th.SystemAdminClient.UpdateConfig(cfg)
- CheckNoError(t, resp)
- assert.True(t, *th.App.Config().MessageExportSettings.EnableExport)
- assert.NotEqual(t, int64(0), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
- // Turn it off, timestamp should be cleared.
- cfg, resp = th.SystemAdminClient.GetConfig()
- CheckNoError(t, resp)
- *cfg.MessageExportSettings.EnableExport = false
- cfg, resp = th.SystemAdminClient.UpdateConfig(cfg)
- CheckNoError(t, resp)
- assert.False(t, *th.App.Config().MessageExportSettings.EnableExport)
- assert.Equal(t, int64(0), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
- // Set a value from the config file.
- th.App.UpdateConfig(func(cfg *model.Config) {
- *cfg.MessageExportSettings.EnableExport = false
- *cfg.MessageExportSettings.ExportFromTimestamp = int64(12345)
- })
- // Turn it on, timestamp should *not* be updated.
- cfg, resp = th.SystemAdminClient.GetConfig()
- CheckNoError(t, resp)
- *cfg.MessageExportSettings.EnableExport = true
- cfg, resp = th.SystemAdminClient.UpdateConfig(cfg)
- CheckNoError(t, resp)
- assert.True(t, *th.App.Config().MessageExportSettings.EnableExport)
- assert.Equal(t, int64(12345), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
- // Turn it off, timestamp should be cleared.
- cfg, resp = th.SystemAdminClient.GetConfig()
- CheckNoError(t, resp)
- *cfg.MessageExportSettings.EnableExport = false
- cfg, resp = th.SystemAdminClient.UpdateConfig(cfg)
- CheckNoError(t, resp)
- assert.False(t, *th.App.Config().MessageExportSettings.EnableExport)
- assert.Equal(t, int64(0), *th.App.Config().MessageExportSettings.ExportFromTimestamp)
- }
- func TestUpdateConfigRestrictSystemAdmin(t *testing.T) {
- th := Setup(t)
- defer th.TearDown()
- th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ExperimentalSettings.RestrictSystemAdmin = true })
- t.Run("Restrict flag should be honored for sysadmin", func(t *testing.T) {
- originalCfg, resp := th.SystemAdminClient.GetConfig()
- CheckNoError(t, resp)
- cfg := originalCfg.Clone()
- *cfg.TeamSettings.SiteName = "MyFancyName" // Allowed
- *cfg.ServiceSettings.SiteURL = "http://example.com" // Ignored
- returnedCfg, resp := th.SystemAdminClient.UpdateConfig(cfg)
- CheckNoError(t, resp)
- require.Equal(t, "MyFancyName", *returnedCfg.TeamSettings.SiteName)
- require.Equal(t, *originalCfg.ServiceSettings.SiteURL, *returnedCfg.ServiceSettings.SiteURL)
- actualCfg, resp := th.SystemAdminClient.GetConfig()
- CheckNoError(t, resp)
- require.Equal(t, returnedCfg, actualCfg)
- })
- t.Run("Restrict flag should be ignored by local mode", func(t *testing.T) {
- originalCfg, resp := th.LocalClient.GetConfig()
- CheckNoError(t, resp)
- cfg := originalCfg.Clone()
- *cfg.TeamSettings.SiteName = "MyFancyName" // Allowed
- *cfg.ServiceSettings.SiteURL = "http://example.com" // Ignored
- returnedCfg, resp := th.LocalClient.UpdateConfig(cfg)
- CheckNoError(t, resp)
- require.Equal(t, "MyFancyName", *returnedCfg.TeamSettings.SiteName)
- require.Equal(t, "http://example.com", *returnedCfg.ServiceSettings.SiteURL)
- })
- }
- func TestGetEnvironmentConfig(t *testing.T) {
- os.Setenv("MM_SERVICESETTINGS_SITEURL", "http://example.mattermost.com")
- os.Setenv("MM_SERVICESETTINGS_ENABLECUSTOMEMOJI", "true")
- defer os.Unsetenv("MM_SERVICESETTINGS_SITEURL")
- defer os.Unsetenv("MM_SERVICESETTINGS_ENABLECUSTOMEMOJI")
- th := Setup(t)
- defer th.TearDown()
- t.Run("as system admin", func(t *testing.T) {
- SystemAdminClient := th.SystemAdminClient
- envConfig, resp := SystemAdminClient.GetEnvironmentConfig()
- CheckNoError(t, resp)
- serviceSettings, ok := envConfig["ServiceSettings"]
- require.True(t, ok, "should've returned ServiceSettings")
- serviceSettingsAsMap, ok := serviceSettings.(map[string]interface{})
- require.True(t, ok, "should've returned ServiceSettings as a map")
- siteURL, ok := serviceSettingsAsMap["SiteURL"]
- require.True(t, ok, "should've returned ServiceSettings.SiteURL")
- siteURLAsBool, ok := siteURL.(bool)
- require.True(t, ok, "should've returned ServiceSettings.SiteURL as a boolean")
- require.True(t, siteURLAsBool, "should've returned ServiceSettings.SiteURL as true")
- enableCustomEmoji, ok := serviceSettingsAsMap["EnableCustomEmoji"]
- require.True(t, ok, "should've returned ServiceSettings.EnableCustomEmoji")
- enableCustomEmojiAsBool, ok := enableCustomEmoji.(bool)
- require.True(t, ok, "should've returned ServiceSettings.EnableCustomEmoji as a boolean")
- require.True(t, enableCustomEmojiAsBool, "should've returned ServiceSettings.EnableCustomEmoji as true")
- _, ok = envConfig["TeamSettings"]
- require.False(t, ok, "should not have returned TeamSettings")
- })
- t.Run("as team admin", func(t *testing.T) {
- TeamAdminClient := th.CreateClient()
- th.LoginTeamAdminWithClient(TeamAdminClient)
- _, resp := TeamAdminClient.GetEnvironmentConfig()
- CheckForbiddenStatus(t, resp)
- })
- t.Run("as regular user", func(t *testing.T) {
- Client := th.Client
- _, resp := Client.GetEnvironmentConfig()
- CheckForbiddenStatus(t, resp)
- })
- t.Run("as not-regular user", func(t *testing.T) {
- Client := th.CreateClient()
- _, resp := Client.GetEnvironmentConfig()
- CheckUnauthorizedStatus(t, resp)
- })
- }
- func TestGetOldClientConfig(t *testing.T) {
- th := Setup(t)
- defer th.TearDown()
- testKey := "supersecretkey"
- th.App.UpdateConfig(func(cfg *model.Config) { *cfg.ServiceSettings.GoogleDeveloperKey = testKey })
- t.Run("with session", func(t *testing.T) {
- th.App.UpdateConfig(func(cfg *model.Config) {
- *cfg.ServiceSettings.GoogleDeveloperKey = testKey
- })
- Client := th.Client
- config, resp := Client.GetOldClientConfig("")
- CheckNoError(t, resp)
- require.NotEmpty(t, config["Version"], "config not returned correctly")
- require.Equal(t, testKey, config["GoogleDeveloperKey"])
- })
- t.Run("without session", func(t *testing.T) {
- th.App.UpdateConfig(func(cfg *model.Config) {
- *cfg.ServiceSettings.GoogleDeveloperKey = testKey
- })
- Client := th.CreateClient()
- config, resp := Client.GetOldClientConfig("")
- CheckNoError(t, resp)
- require.NotEmpty(t, config["Version"], "config not returned correctly")
- require.Empty(t, config["GoogleDeveloperKey"], "config should be missing developer key")
- })
- t.Run("missing format", func(t *testing.T) {
- Client := th.Client
- _, err := Client.DoApiGet("/config/client", "")
- require.NotNil(t, err)
- require.Equal(t, http.StatusNotImplemented, err.StatusCode)
- })
- t.Run("invalid format", func(t *testing.T) {
- Client := th.Client
- _, err := Client.DoApiGet("/config/client?format=junk", "")
- require.NotNil(t, err)
- require.Equal(t, http.StatusBadRequest, err.StatusCode)
- })
- }
- func TestPatchConfig(t *testing.T) {
- th := Setup(t)
- defer th.TearDown()
- t.Run("config is missing", func(t *testing.T) {
- _, response := th.Client.PatchConfig(nil)
- CheckBadRequestStatus(t, response)
- })
- t.Run("user is not system admin", func(t *testing.T) {
- _, response := th.Client.PatchConfig(&model.Config{})
- CheckForbiddenStatus(t, response)
- })
- t.Run("should not update the restricted fields when restrict toggle is on for sysadmin", func(t *testing.T) {
- *th.App.Config().ExperimentalSettings.RestrictSystemAdmin = true
- config := model.Config{LogSettings: model.LogSettings{
- ConsoleLevel: model.NewString("INFO"),
- }}
- updatedConfig, _ := th.SystemAdminClient.PatchConfig(&config)
- assert.Equal(t, "DEBUG", *updatedConfig.LogSettings.ConsoleLevel)
- })
- t.Run("should not bypass the restrict toggle if local client", func(t *testing.T) {
- *th.App.Config().ExperimentalSettings.RestrictSystemAdmin = true
- config := model.Config{LogSettings: model.LogSettings{
- ConsoleLevel: model.NewString("INFO"),
- }}
- oldConfig, _ := th.LocalClient.GetConfig()
- updatedConfig, _ := th.LocalClient.PatchConfig(&config)
- assert.Equal(t, "INFO", *updatedConfig.LogSettings.ConsoleLevel)
- // reset the config
- _, resp := th.LocalClient.UpdateConfig(oldConfig)
- CheckNoError(t, resp)
- })
- th.TestForSystemAdminAndLocal(t, func(t *testing.T, client *model.Client4) {
- t.Run("check if config is valid", func(t *testing.T) {
- config := model.Config{PasswordSettings: model.PasswordSettings{
- MinimumLength: model.NewInt(4),
- }}
- _, response := client.PatchConfig(&config)
- assert.Equal(t, http.StatusBadRequest, response.StatusCode)
- assert.NotNil(t, response.Error)
- assert.Equal(t, "model.config.is_valid.password_length.app_error", response.Error.Id)
- })
- t.Run("should patch the config", func(t *testing.T) {
- *th.App.Config().ExperimentalSettings.RestrictSystemAdmin = false
- th.App.UpdateConfig(func(cfg *model.Config) { cfg.TeamSettings.ExperimentalDefaultChannels = []string{"some-channel"} })
- oldConfig, _ := client.GetConfig()
- assert.False(t, *oldConfig.PasswordSettings.Lowercase)
- assert.NotEqual(t, 15, *oldConfig.PasswordSettings.MinimumLength)
- assert.Equal(t, "DEBUG", *oldConfig.LogSettings.ConsoleLevel)
- assert.True(t, oldConfig.PluginSettings.PluginStates["com.mattermost.nps"].Enable)
- states := make(map[string]*model.PluginState)
- states["com.mattermost.nps"] = &model.PluginState{Enable: *model.NewBool(false)}
- config := model.Config{PasswordSettings: model.PasswordSettings{
- Lowercase: model.NewBool(true),
- MinimumLength: model.NewInt(15),
- }, LogSettings: model.LogSettings{
- ConsoleLevel: model.NewString("INFO"),
- },
- TeamSettings: model.TeamSettings{
- ExperimentalDefaultChannels: []string{"another-channel"},
- },
- PluginSettings: model.PluginSettings{
- PluginStates: states,
- },
- }
- _, response := client.PatchConfig(&config)
- updatedConfig, _ := client.GetConfig()
- assert.True(t, *updatedConfig.PasswordSettings.Lowercase)
- assert.Equal(t, "INFO", *updatedConfig.LogSettings.ConsoleLevel)
- assert.Equal(t, []string{"another-channel"}, updatedConfig.TeamSettings.ExperimentalDefaultChannels)
- assert.False(t, updatedConfig.PluginSettings.PluginStates["com.mattermost.nps"].Enable)
- assert.Equal(t, "no-cache, no-store, must-revalidate", response.Header.Get("Cache-Control"))
- // reset the config
- _, resp := client.UpdateConfig(oldConfig)
- CheckNoError(t, resp)
- })
- t.Run("should sanitize config", func(t *testing.T) {
- config := model.Config{PasswordSettings: model.PasswordSettings{
- Symbol: model.NewBool(true),
- }}
- updatedConfig, _ := client.PatchConfig(&config)
- assert.Equal(t, model.FAKE_SETTING, *updatedConfig.SqlSettings.DataSource)
- })
- t.Run("not allowing to toggle enable uploads for plugin via api", func(t *testing.T) {
- config := model.Config{PluginSettings: model.PluginSettings{
- EnableUploads: model.NewBool(true),
- }}
- updatedConfig, _ := client.PatchConfig(&config)
- assert.Equal(t, false, *updatedConfig.PluginSettings.EnableUploads)
- })
- })
- t.Run("System Admin should not be able to clear Site URL", func(t *testing.T) {
- cfg, resp := th.SystemAdminClient.GetConfig()
- CheckNoError(t, resp)
- siteURL := cfg.ServiceSettings.SiteURL
- defer th.App.UpdateConfig(func(cfg *model.Config) { cfg.ServiceSettings.SiteURL = siteURL })
- // Set the SiteURL
- nonEmptyURL := "http://localhost"
- config := model.Config{
- ServiceSettings: model.ServiceSettings{
- SiteURL: model.NewString(nonEmptyURL),
- },
- }
- updatedConfig, resp := th.SystemAdminClient.PatchConfig(&config)
- CheckNoError(t, resp)
- require.Equal(t, nonEmptyURL, *updatedConfig.ServiceSettings.SiteURL)
- // Check that the Site URL can't be cleared
- config = model.Config{
- ServiceSettings: model.ServiceSettings{
- SiteURL: model.NewString(""),
- },
- }
- updatedConfig, resp = th.SystemAdminClient.PatchConfig(&config)
- CheckBadRequestStatus(t, resp)
- CheckErrorMessage(t, resp, "api.config.update_config.clear_siteurl.app_error")
- // Check that the Site URL wasn't cleared
- cfg, resp = th.SystemAdminClient.GetConfig()
- CheckNoError(t, resp)
- require.Equal(t, nonEmptyURL, *cfg.ServiceSettings.SiteURL)
- })
- }
|