mattermost/server/public/model/preference.go
Guillermo Vayá 8cd48d4651
[MM-67880] Add /mobile-logs slash command (#35658)
* [MM-67880] Add /mobile-logs slash command with E2E tests

Add a new /mobile-logs slash command that allows users to manage the
attach_app_logs preference for themselves or other users (admin-only).
Includes unit tests for all code paths and Playwright E2E tests covering
self-management, admin cross-user management, permission denial, and
error handling.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix i18n error key suffixes and format E2E tests

Rename error i18n keys to use .app_error suffix matching upstream
convention (no_permission, update_error, user_not_found). Run prettier
on the E2E test file to fix formatting.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* Fix E2E test type error: use getUserPreferences instead of getMyPreferences

The getAttachLogsPreference helper was using getMyPreferences() which returns
PreferenceType (not an array), causing TS2345 errors. Switch to
getUserPreferences(userId) which returns the expected array type.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* [MM-67880] Move unreachable usage fallback into switch default case

The return after the switch was unreachable because action is validated
earlier to be "on", "off", or "status". Move it into an explicit default
case with a defensive comment.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* allow up to 2 arguments

* Add audit logging for mobile logs slash command actions

Implement a new function to log audit records when users enable or disable the attach_app_logs preference via the /mobile-logs command. This includes capturing relevant metadata such as user IDs, session information, and the action taken. The logging occurs in both the enable and disable command paths, enhancing traceability and accountability for user preference changes.

* Enhance mobile logs command to handle cross-user permission checks

Add a new response function for cases where a regular user attempts to access mobile log settings for another user, ensuring they receive a neutral error message instead of specific user information. Update the command logic to incorporate this response for both nonexistent users and deactivated accounts. Additionally, modify related tests and internationalization keys to reflect these changes, improving security and user experience.

* Update E2E test for /mobile-logs command to verify permission denial for nonexistent users

Enhance the existing E2E test for the /mobile-logs command by adding assertions to check that users receive a permission denial message when attempting to change mobile log settings for a nonexistent user. This improves test coverage and ensures proper error handling in the application.

* update i18n strings

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Mattermost Build <build@mattermost.com>
2026-04-20 12:46:36 +02:00

190 lines
7.9 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
package model
import (
"encoding/json"
"net/http"
"regexp"
"strconv"
"strings"
"unicode/utf8"
)
const (
// The primary key for the preference table is the combination of User.Id, Category, and Name.
// PreferenceCategoryDirectChannelShow and PreferenceCategoryGroupChannelShow
// are used to store the user's preferences for which channels to show in the sidebar.
// The Name field is the channel ID.
PreferenceCategoryDirectChannelShow = "direct_channel_show"
PreferenceCategoryGroupChannelShow = "group_channel_show"
// PreferenceCategoryTutorialStep is used to store the user's progress in the tutorial.
// The Name field is the user ID again (for whatever reason).
PreferenceCategoryTutorialSteps = "tutorial_step"
// PreferenceCategoryAdvancedSettings has settings for the user's advanced settings.
// The Name field is the setting name. Possible values are:
// - "formatting"
// - "send_on_ctrl_enter"
// - "join_leave"
// - "unread_scroll_position"
// - "sync_drafts"
// - "attach_app_logs"
// - "feature_enabled_markdown_preview" <- deprecated in favor of "formatting"
PreferenceCategoryAdvancedSettings = "advanced_settings"
// PreferenceCategoryFlaggedPost is used to store the user's saved posts.
// The Name field is the post ID.
PreferenceCategoryFlaggedPost = "flagged_post"
// PreferenceCategoryFavoriteChannel is used to store the user's favorite channels to be
// shown in the sidebar. The Name field is the channel ID.
PreferenceCategoryFavoriteChannel = "favorite_channel"
// PreferenceCategorySidebarSettings is used to store the user's sidebar settings.
// The Name field is the setting name. (ie. PreferenceNameShowUnreadSection or PreferenceLimitVisibleDmsGms)
PreferenceCategorySidebarSettings = "sidebar_settings"
// PreferenceCategoryDisplaySettings is used to store the user's various display settings.
// The possible Name fields are:
// - PreferenceNameUseMilitaryTime
// - PreferenceNameCollapseSetting
// - PreferenceNameMessageDisplay
// - PreferenceNameCollapseConsecutive
// - PreferenceNameColorizeUsernames
// - PreferenceNameChannelDisplayMode
// - PreferenceNameNameFormat
PreferenceCategoryDisplaySettings = "display_settings"
// PreferenceCategorySystemNotice is used store system admin notices.
// Possible Name values are not defined here. It can be anything with the notice name.
PreferenceCategorySystemNotice = "system_notice"
// Deprecated: PreferenceCategoryLast is not used anymore.
PreferenceCategoryLast = "last"
// PreferenceCategoryCustomStatus is used to store the user's custom status preferences.
// Possible Name values are:
// - PreferenceNameRecentCustomStatuses
// - PreferenceNameCustomStatusTutorialState
// - PreferenceCustomStatusModalViewed
PreferenceCategoryCustomStatus = "custom_status"
// PreferenceCategoryNotifications is used to store the user's notification settings.
// Possible Name values are:
// - PreferenceNameEmailInterval
PreferenceCategoryNotifications = "notifications"
// Deprecated: PreferenceRecommendedNextSteps is not used anymore.
// Use PreferenceCategoryRecommendedNextSteps instead.
// PreferenceRecommendedNextSteps is actually a Category. The only possible
// Name vaule is PreferenceRecommendedNextStepsHide for now.
PreferenceRecommendedNextSteps = PreferenceCategoryRecommendedNextSteps
PreferenceCategoryRecommendedNextSteps = "recommended_next_steps"
// PreferenceCategoryTheme has the name for the team id where theme is set.
PreferenceCategoryTheme = "theme"
PreferenceNameAttachAppLogs = "attach_app_logs"
PreferenceNameCollapsedThreadsEnabled = "collapsed_reply_threads"
PreferenceNameChannelDisplayMode = "channel_display_mode"
PreferenceNameCollapseSetting = "collapse_previews"
PreferenceNameMessageDisplay = "message_display"
PreferenceNameCollapseConsecutive = "collapse_consecutive_messages"
PreferenceNameColorizeUsernames = "colorize_usernames"
PreferenceNameNameFormat = "name_format"
PreferenceNameUseMilitaryTime = "use_military_time"
PreferenceNameShowUnreadSection = "show_unread_section"
PreferenceLimitVisibleDmsGms = "limit_visible_dms_gms"
PreferenceMaxLimitVisibleDmsGmsValue = 40
MaxPreferenceValueLength = 20000
PreferenceCategoryAuthorizedOAuthApp = "oauth_app"
// the name for oauth_app is the client_id and value is the current scope
// Deprecated: PreferenceCategoryLastChannel is not used anymore.
PreferenceNameLastChannel = "channel"
// Deprecated: PreferenceCategoryLastTeam is not used anymore.
PreferenceNameLastTeam = "team"
PreferenceNameRecentCustomStatuses = "recent_custom_statuses"
PreferenceNameCustomStatusTutorialState = "custom_status_tutorial_state"
PreferenceCustomStatusModalViewed = "custom_status_modal_viewed"
PreferenceNameEmailInterval = "email_interval"
PreferenceEmailIntervalNoBatchingSeconds = "30" // the "immediate" setting is actually 30s
PreferenceEmailIntervalBatchingSeconds = "900" // fifteen minutes is 900 seconds
PreferenceEmailIntervalImmediately = "immediately"
PreferenceEmailIntervalFifteen = "fifteen"
PreferenceEmailIntervalFifteenAsSeconds = "900"
PreferenceEmailIntervalHour = "hour"
PreferenceEmailIntervalHourAsSeconds = "3600"
PreferenceCloudUserEphemeralInfo = "cloud_user_ephemeral_info"
PreferenceNameRecommendedNextStepsHide = "hide"
)
type Preference struct {
UserId string `json:"user_id"`
Category string `json:"category"`
Name string `json:"name"`
Value string `json:"value"`
}
type Preferences []Preference
func (o *Preference) IsValid() *AppError {
if !IsValidId(o.UserId) {
return NewAppError("Preference.IsValid", "model.preference.is_valid.id.app_error", nil, "user_id="+o.UserId, http.StatusBadRequest)
}
if o.Category == "" || len(o.Category) > 32 {
return NewAppError("Preference.IsValid", "model.preference.is_valid.category.app_error", nil, "category="+o.Category, http.StatusBadRequest)
}
if len(o.Name) > 32 {
return NewAppError("Preference.IsValid", "model.preference.is_valid.name.app_error", nil, "name="+o.Name, http.StatusBadRequest)
}
if utf8.RuneCountInString(o.Value) > MaxPreferenceValueLength {
return NewAppError("Preference.IsValid", "model.preference.is_valid.value.app_error", nil, "value="+o.Value, http.StatusBadRequest)
}
if o.Category == PreferenceCategoryTheme {
var unused map[string]string
if err := json.NewDecoder(strings.NewReader(o.Value)).Decode(&unused); err != nil {
return NewAppError("Preference.IsValid", "model.preference.is_valid.theme.app_error", nil, "value="+o.Value, http.StatusBadRequest).Wrap(err)
}
}
if o.Category == PreferenceCategorySidebarSettings && o.Name == PreferenceLimitVisibleDmsGms {
visibleDmsGmsValue, convErr := strconv.Atoi(o.Value)
if convErr != nil || visibleDmsGmsValue < 1 || visibleDmsGmsValue > PreferenceMaxLimitVisibleDmsGmsValue {
return NewAppError("Preference.IsValid", "model.preference.is_valid.limit_visible_dms_gms.app_error", nil, "value="+o.Value, http.StatusBadRequest)
}
}
return nil
}
var preUpdateColorPattern = regexp.MustCompile(`^#[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$`)
func (o *Preference) PreUpdate() {
if o.Category == PreferenceCategoryTheme {
// decode the value of theme (a map of strings to string) and eliminate any invalid values
var props map[string]string
// just continue, the invalid preference value should get caught by IsValid before saving
json.NewDecoder(strings.NewReader(o.Value)).Decode(&props)
// blank out any invalid theme values
for name, value := range props {
if name == "image" || name == "type" || name == "codeTheme" {
continue
}
if !preUpdateColorPattern.MatchString(value) {
props[name] = "#ffffff"
}
}
if b, err := json.Marshal(props); err == nil {
o.Value = string(b)
}
}
}