Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 79 additions & 15 deletions server/.golangci.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,84 @@
version: "2"
linters:
default: none
enable:
- bidichk
- errcheck
- govet
- ineffassign
- makezero
- misspell
- modernize
- revive
- staticcheck
- unconvert
- unqueryvet
- unused
- whitespace
default: all
disable:
- bodyclose
- canonicalheader
- containedctx # storing context.Context in a struct is an established pattern here
- contextcheck
- cyclop
- depguard
- dogsled # test helpers return many values; blank-heavy destructuring is idiomatic
- dupl
- dupword
- embeddedstructfieldcheck
- err113
- errchkjson
- errname
- errorlint
- exhaustive
- exhaustruct
- forbidigo
- forcetypeassert
- funcorder
- funlen
- gocheckcompilerdirectives # //go:fix is a valid directive in Go 1.24+; linter doesn't know it yet
- gochecknoglobals
- gochecknoinits
- gocognit
- goconst
- gocritic
- gocyclo
- godoclint
- godot
- godox
- gomoddirectives # replace directives in go.mod are intentional forks
- gomodguard # deprecated since v2.12.0; replaced by gomodguard_v2 (enabled via default: all)
- goprintffuncname # Ephemeral → Ephemeralf rename is a plugin API breaking change; deferred
- gosec
- gosmopolitan
- iface # identical job interfaces are intentional — type-safe scheduling without coupling
- inamedparam
- interfacebloat
- intrange
- iotamixing # const blocks intentionally mix iota with explicit values (ABI stability, ASCII)
- ireturn
- lll
- maintidx
- mnd
- musttag
- nakedret
- nestif
- nilerr # intentionally dropping errors is common here (graceful degradation, security non-disclosure, fallbacks)
- nilnil
- nlreturn
- noctx
- noinlineerr
- nolintlint
- nonamedreturns
- paralleltest
- perfsprint
- prealloc
- predeclared # variable named 'copy' is intentional; already suppressed for revive
- promlinter # metric renames are a breaking change; deferred
- protogetter
- recvcheck
- sqlclosecheck # wrapper functions return *sqlx.Rows to callers who close them; not a real leak
- tagalign
- tagliatelle
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unparam
- usestdlibvars
- usetesting
- varnamelen
- wastedassign
- wrapcheck
- wsl
- wsl_v5
settings:
govet:
disable:
Expand Down
2 changes: 1 addition & 1 deletion server/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,7 @@ golang-versions: ## Install Golang versions used for compatibility testing (e.g.
export GO_COMPATIBILITY_TEST_VERSIONS="${GO_COMPATIBILITY_TEST_VERSIONS}"

golangci-lint: setup-go-work ## Run golangci-lint on codebase
$(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.11.4
$(GO) install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.12.2
ifeq ($(BUILD_ENTERPRISE_READY),true)
$(GOBIN)/golangci-lint run ./... ./public/... $(BUILD_ENTERPRISE_DIR)/...
else
Expand Down
6 changes: 3 additions & 3 deletions server/channels/api4/outgoing_oauth_connection_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -942,7 +942,7 @@ func TestHandlerOutgoingOAuthConnectionUpdate(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageOutgoingOAuthConnections.Id, model.SystemUserRoleId)

body := &bytes.Buffer{}
body.Write([]byte(`{/}`))
body.WriteString(`{/}`)

req, err := http.NewRequest("PUT", "/", body)
if err != nil {
Expand Down Expand Up @@ -990,7 +990,7 @@ func TestHandlerOutgoingOAuthConnectionUpdate(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageOutgoingOAuthConnections.Id, model.SystemUserRoleId)

body := &bytes.Buffer{}
body.Write([]byte(`{"Id": "` + model.NewId() + `", "name": "changed name"}`))
body.WriteString(`{"Id": "` + model.NewId() + `", "name": "changed name"}`)

req, err := http.NewRequest("PUT", "/", body)
if err != nil {
Expand Down Expand Up @@ -1133,7 +1133,7 @@ func TestHandlerOutgoingOAuthConnectionHandlerCreate(t *testing.T) {
th.AddPermissionToRole(t, model.PermissionManageOutgoingOAuthConnections.Id, model.SystemUserRoleId)

body := &bytes.Buffer{}
body.Write([]byte(`{/}`))
body.WriteString(`{/}`)

req, err := http.NewRequest("POST", "/", body)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion server/channels/app/channel.go
Original file line number Diff line number Diff line change
Expand Up @@ -4153,7 +4153,7 @@ func (a *App) setSidebarCategoriesForConvertedGroupMessage(rctx request.CTX, gmC
channelsCategory := categories.Categories[0]
_, appErr = a.UpdateSidebarCategories(rctx, user.Id, gmConversionRequest.TeamID, []*model.SidebarCategoryWithChannels{channelsCategory})
if appErr != nil {
rctx.Logger().Error("Failed to add converted GM to default sidebar category for user", mlog.String("user_id", user.Id), mlog.Err(err))
rctx.Logger().Error("Failed to add converted GM to default sidebar category for user", mlog.String("user_id", user.Id), mlog.Err(appErr))
}
}

Expand Down
2 changes: 1 addition & 1 deletion server/channels/app/platform/web_hub.go
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ func (ps *PlatformService) GetHubForUserId(userID string) *Hub {
// https://mattermost.atlassian.net/browse/MM-26629.
var hash maphash.Hash
hash.SetSeed(ps.hashSeed)
_, err := hash.Write([]byte(userID))
_, err := hash.WriteString(userID)
if err != nil {
ps.logger.Error("Unable to write userID to hash", mlog.String("userID", userID), mlog.Err(err))
}
Expand Down
4 changes: 2 additions & 2 deletions server/channels/app/plugin_requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ func (ch *Channels) servePluginRequest(w http.ResponseWriter, r *http.Request, h
session, appErr := app.GetSession(token)
if appErr != nil {
if appErr.StatusCode == http.StatusInternalServerError {
handleInternalServerError(rctx, "Internal server error while loading session", err)
handleInternalServerError(rctx, "Internal server error while loading session", appErr)
return
}
rctx.Logger().Debug("Token in plugin request is invalid. Treating request as unauthenticated",
Expand All @@ -254,7 +254,7 @@ func (ch *Channels) servePluginRequest(w http.ResponseWriter, r *http.Request, h
// If MFA is required and user has not activated it, treat it as unauthenticated
if appErr := app.MFARequired(rctx); appErr != nil {
if appErr.StatusCode == http.StatusInternalServerError {
handleInternalServerError(rctx, "Internal server error during MFA validation", err)
handleInternalServerError(rctx, "Internal server error during MFA validation", appErr)
return
}
rctx.Logger().Warn("Treating session as unauthenticated since MFA required",
Expand Down
2 changes: 1 addition & 1 deletion server/channels/app/reaction.go
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ func (a *App) DeleteReactionForPost(rctx request.CTX, reaction *model.Reaction)

restrictDM, appErr := a.CheckIfChannelIsRestrictedDM(rctx, channel)
if appErr != nil {
return err
return appErr
}

if restrictDM {
Expand Down
2 changes: 1 addition & 1 deletion server/channels/app/report.go
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ func (a *App) compileCSVChunks(prefix string, numberOfChunks int, headers []stri
}
_, writeErr := compiledBuf.Write(chunk)
if writeErr != nil {
return err
return model.NewAppError("compileCSVChunks", "app.compile_csv_chunks.write_error", nil, "", http.StatusInternalServerError).Wrap(writeErr)
}
}

Expand Down
2 changes: 1 addition & 1 deletion server/channels/app/slashcommands/command_custom_status.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ func GetCustomStatus(message string) *model.CustomStatus {

func removeUnicodeSkinTone(unicodeString string) string {
skinToneDetectorRegex := regexp.MustCompile("-(1f3fb|1f3fc|1f3fd|1f3fe|1f3ff)")
skinToneLocations := skinToneDetectorRegex.FindIndex([]byte(unicodeString))
skinToneLocations := skinToneDetectorRegex.FindStringIndex(unicodeString)

if len(skinToneLocations) == 0 {
return unicodeString
Expand Down
20 changes: 12 additions & 8 deletions server/channels/app/slashcommands/command_invite_people.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,17 @@ func (*InvitePeopleProvider) GetCommand(a *app.App, T i18n.TranslateFunc) *model
}
}

func parseEmailList(message string) []string {
var emails []string
for token := range strings.FieldsSeq(message) {
token = strings.Trim(token, ",")
if strings.Contains(token, "@") {
emails = append(emails, token)
}
}
return emails
}

func (*InvitePeopleProvider) DoCommand(a *app.App, rctx request.CTX, args *model.CommandArgs, message string) *model.CommandResponse {
if !a.HasPermissionToTeam(rctx, args.UserId, args.TeamId, model.PermissionInviteUser) {
return &model.CommandResponse{Text: args.T("api.command_invite_people.permission.app_error"), ResponseType: model.CommandResponseTypeEphemeral}
Expand All @@ -62,14 +73,7 @@ func (*InvitePeopleProvider) DoCommand(a *app.App, rctx request.CTX, args *model
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.email_invitations_off")}
}

emailList := strings.Fields(message)

for i := len(emailList) - 1; i >= 0; i-- {
emailList[i] = strings.Trim(emailList[i], ",")
if !strings.Contains(emailList[i], "@") {
emailList = append(emailList[:i], emailList[i+1:]...)
}
}
emailList := parseEmailList(message)

if len(emailList) == 0 {
return &model.CommandResponse{ResponseType: model.CommandResponseTypeEphemeral, Text: args.T("api.command.invite_people.no_email")}
Expand Down
52 changes: 52 additions & 0 deletions server/channels/app/slashcommands/command_invite_people_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,62 @@ import (
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/mattermost/mattermost/server/public/model"
)

func TestParseEmailList(t *testing.T) {
tests := []struct {
name string
input string
expected []string
}{
{
name: "single valid email",
input: "user@example.com",
expected: []string{"user@example.com"},
},
{
name: "multiple valid emails",
input: "a@example.com b@example.com",
expected: []string{"a@example.com", "b@example.com"},
},
{
name: "trailing commas stripped",
input: "a@example.com, b@example.com,",
expected: []string{"a@example.com", "b@example.com"},
},
{
name: "non-email tokens filtered out",
input: "notanemail a@example.com alsoinvalid",
expected: []string{"a@example.com"},
},
{
name: "comma immediately after email treated as one token",
input: "a@example.com,b@example.com",
expected: []string{"a@example.com,b@example.com"},
},
{
name: "empty input",
input: "",
expected: nil,
},
{
name: "all tokens invalid",
input: "notanemail alsoinvalid",
expected: nil,
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
result := parseEmailList(tc.input)
require.Equal(t, tc.expected, result)
})
}
}

func TestInvitePeopleProvider(t *testing.T) {
th := setup(t).initBasic(t)

Expand Down
2 changes: 1 addition & 1 deletion server/channels/app/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -1832,7 +1832,7 @@ func (a *App) CreatePasswordRecoveryToken(rctx request.CTX, userID, email string
// remove any previously created tokens for user
appErr := a.InvalidatePasswordRecoveryTokensForUser(userID)
if appErr != nil {
rctx.Logger().Warn("Error while deleting additional user tokens.", mlog.Err(err))
rctx.Logger().Warn("Error while deleting additional user tokens.", mlog.Err(appErr))
}

token := model.NewToken(model.TokenTypePasswordRecovery, string(jsonData))
Expand Down
2 changes: 1 addition & 1 deletion server/channels/jobs/batch_report_worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ func (worker *BatchReportWorker) processChunk(job *model.Job, reportData []model

appErr := worker.app.SaveReportChunk(worker.reportFormat, job.Id, fileCount, reportData)
if appErr != nil {
return err
return appErr
}

fileCount++
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func getData(app ExportUsersToCSVAppIFace) func(jobData model.StringMap) ([]mode

users, appErr := app.GetUsersForReporting(filter)
if appErr != nil {
return nil, nil, false, errors.Wrapf(err, "failed to get the next batch (column_value=%v, user_id=%v)", filter.FromColumnValue, filter.FromId)
return nil, nil, false, errors.Wrapf(appErr, "failed to get the next batch (column_value=%v, user_id=%v)", filter.FromColumnValue, filter.FromId)
}

if len(users) == 0 {
Expand Down
2 changes: 1 addition & 1 deletion server/channels/store/sqlstore/job_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -447,7 +447,7 @@ func (jss SqlJobStore) Cleanup(expiryTime int64, batchSize int) error {
var rowErr error
rowsAffected, rowErr = sqlResult.RowsAffected()
if rowErr != nil {
return errors.Wrap(err, "unable to delete jobs")
return errors.Wrap(rowErr, "unable to delete jobs")
}

time.Sleep(jobsCleanupDelay)
Expand Down
9 changes: 9 additions & 0 deletions server/channels/store/sqlstore/schema_dump.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,9 @@ func (ss *SqlStore) getTableOptions() (map[string]map[string]string, error) {
// Add option to the table
tableOptions[tableName][key] = value
}
if err := optionsRows.Err(); err != nil {
rErr = multierror.Append(rErr, errors.Wrap(err, "error iterating table options rows"))
}

return tableOptions, rErr.ErrorOrNil()
}
Expand Down Expand Up @@ -253,6 +256,9 @@ func (ss *SqlStore) getTableSchemaInformation() (map[string]*model.DatabaseTable
})
}
}
if err := rows.Err(); err != nil {
rErr = multierror.Append(rErr, errors.Wrap(err, "error iterating schema rows"))
}

return tablesMap, tableCollations, rErr.ErrorOrNil()
}
Expand Down Expand Up @@ -298,6 +304,9 @@ func (ss *SqlStore) getTableIndexes() (map[string][]model.DatabaseIndex, error)

tableIndexes[tableName] = append(tableIndexes[tableName], index)
}
if err := rows.Err(); err != nil {
rErr = multierror.Append(rErr, errors.Wrap(err, "error iterating index rows"))
}

return tableIndexes, rErr.ErrorOrNil()
}
2 changes: 1 addition & 1 deletion server/channels/store/sqlstore/session_store.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,7 +381,7 @@ func (me SqlSessionStore) Cleanup(expiryTime int64, batchSize int64) error {
var rowErr error
rowsAffected, rowErr = sqlResult.RowsAffected()
if rowErr != nil {
return errors.Wrap(err, "unable to delete sessions")
return errors.Wrap(rowErr, "unable to delete sessions")
}

time.Sleep(sessionsCleanupDelay)
Expand Down
5 changes: 4 additions & 1 deletion server/channels/store/sqlstore/sqlx_wrapper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,10 @@ func TestSqlX(t *testing.T) {

query := `SELECT pg_sleep(:timeout);`
arg := struct{ Timeout int }{Timeout: 2}
_, err = tx.NamedQuery(query, arg)
rows, err := tx.NamedQuery(query, arg)
if rows != nil {
defer rows.Close()
}
require.Equal(t, context.DeadlineExceeded, err)
require.NoError(t, tx.Commit())
}
Expand Down
2 changes: 1 addition & 1 deletion server/channels/utils/license.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ func GetAndValidateLicenseFileFromDisk(location string) (*model.License, []byte,

var license model.License
if jsonErr := json.Unmarshal([]byte(licenseStr), &license); jsonErr != nil {
return nil, nil, fmt.Errorf("Found license key at %s but it appears to be invalid: %w", fileName, err)
return nil, nil, fmt.Errorf("Found license key at %s but it appears to be invalid: %w", fileName, jsonErr)
}

return &license, licenseBytes, nil
Expand Down
Loading
Loading