mirror of
https://github.com/hanzoai/team.git
synced 2026-09-30 01:57:35 +00:00
* fix: only match root-level JSONL files when importing a zip When importing a Mattermost export zip, the code iterated over all files to find the first .jsonl by extension. Exported attachments under data/ could themselves be .jsonl files, causing the import to pick an attachment as the manifest instead of the actual root-level JSONL file. Extract an IsRootJsonlFile helper in the imports package and use it in the import process worker, mmctl validator, and bulk import test to restrict the search to files with no directory component. * test: add integration test for import with nested JSONL decoy file Verify that BulkImportWithPath correctly picks the root-level JSONL manifest and ignores a decoy .jsonl inside a subdirectory, covering the fix from ad7f230f06.
880 lines
38 KiB
Go
880 lines
38 KiB
Go
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
package imports
|
|
|
|
import (
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"strings"
|
|
"unicode/utf8"
|
|
|
|
"github.com/mattermost/mattermost/server/public/model"
|
|
"github.com/mattermost/mattermost/server/public/shared/mlog"
|
|
)
|
|
|
|
// IsRootJsonlFile reports whether the given zip entry name refers to a .jsonl
|
|
// file at the root of the archive (no directory component). This is used to
|
|
// locate the import manifest while ignoring .jsonl files that may exist as
|
|
// exported attachments in subdirectories.
|
|
func IsRootJsonlFile(name string) bool {
|
|
return filepath.Ext(name) == ".jsonl" && filepath.Dir(name) == "."
|
|
}
|
|
|
|
func ValidateSchemeImportData(data *SchemeImportData) *model.AppError {
|
|
if data.Scope == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.null_scope.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
switch *data.Scope {
|
|
case model.SchemeScopeTeam:
|
|
if data.DefaultTeamAdminRole == nil || data.DefaultTeamUserRole == nil || data.DefaultChannelAdminRole == nil || data.DefaultChannelUserRole == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.wrong_roles_for_scope.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
case model.SchemeScopeChannel:
|
|
if data.DefaultTeamAdminRole != nil || data.DefaultTeamUserRole != nil || data.DefaultChannelAdminRole == nil || data.DefaultChannelUserRole == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.wrong_roles_for_scope.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
default:
|
|
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.unknown_scheme.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Name == nil || !model.IsValidSchemeName(*data.Name) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.name_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.DisplayName == nil || *data.DisplayName == "" || len(*data.DisplayName) > model.SchemeDisplayNameMaxLength {
|
|
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.display_name_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Description != nil && len(*data.Description) > model.SchemeDescriptionMaxLength {
|
|
return model.NewAppError("BulkImport", "app.import.validate_scheme_import_data.description_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.DefaultTeamAdminRole != nil {
|
|
if err := ValidateRoleImportData(data.DefaultTeamAdminRole); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if data.DefaultTeamUserRole != nil {
|
|
if err := ValidateRoleImportData(data.DefaultTeamUserRole); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if data.DefaultTeamGuestRole != nil {
|
|
if err := ValidateRoleImportData(data.DefaultTeamGuestRole); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if data.DefaultChannelAdminRole != nil {
|
|
if err := ValidateRoleImportData(data.DefaultChannelAdminRole); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if data.DefaultChannelUserRole != nil {
|
|
if err := ValidateRoleImportData(data.DefaultChannelUserRole); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if data.DefaultChannelGuestRole != nil {
|
|
if err := ValidateRoleImportData(data.DefaultChannelGuestRole); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidateRoleImportData(data *RoleImportData) *model.AppError {
|
|
if data.Name == nil || !model.IsValidRoleName(*data.Name) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_role_import_data.name_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.DisplayName == nil || *data.DisplayName == "" || len(*data.DisplayName) > model.RoleDisplayNameMaxLength {
|
|
return model.NewAppError("BulkImport", "app.import.validate_role_import_data.display_name_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Description != nil && len(*data.Description) > model.RoleDescriptionMaxLength {
|
|
return model.NewAppError("BulkImport", "app.import.validate_role_import_data.description_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Permissions != nil {
|
|
for _, permission := range *data.Permissions {
|
|
permissionValidated := false
|
|
for _, p := range append(model.AllPermissions, model.DeprecatedPermissions...) {
|
|
if permission == p.Id {
|
|
permissionValidated = true
|
|
break
|
|
}
|
|
}
|
|
|
|
if !permissionValidated {
|
|
return model.NewAppError("BulkImport", "app.import.validate_role_import_data.invalid_permission.error", nil, "permission"+permission, http.StatusBadRequest)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidateTeamImportData(data *TeamImportData) *model.AppError {
|
|
if data.Name == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.name_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if len(*data.Name) > model.TeamNameMaxLength {
|
|
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.name_length.error", nil, "", http.StatusBadRequest)
|
|
} else if model.IsReservedTeamName(*data.Name) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.name_reserved.error", nil, "", http.StatusBadRequest)
|
|
} else if !model.IsValidTeamName(strings.ToLower(*data.Name)) { // uppercase letters are not allowed in team names, but for import path we are more forgiving
|
|
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.name_characters.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.DisplayName == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.display_name_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if utf8.RuneCountInString(*data.DisplayName) == 0 || utf8.RuneCountInString(*data.DisplayName) > model.TeamDisplayNameMaxRunes {
|
|
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.display_name_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Type == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.type_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if *data.Type != model.TeamOpen && *data.Type != model.TeamInvite {
|
|
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.type_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Description != nil && len(*data.Description) > model.TeamDescriptionMaxLength {
|
|
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.description_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Scheme != nil && !model.IsValidSchemeName(*data.Scheme) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_team_import_data.scheme_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidateChannelImportData(data *ChannelImportData) *model.AppError {
|
|
if data.Team == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.team_missing.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Name == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.name_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if len(*data.Name) > model.ChannelNameMaxLength {
|
|
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.name_length.error", nil, "", http.StatusBadRequest)
|
|
} else if !model.IsValidChannelIdentifier(strings.ToLower(*data.Name)) { // uppercase letters are not allowed in channel names, but for import path we are more forgiving
|
|
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.name_characters.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.DisplayName == nil || utf8.RuneCountInString(*data.DisplayName) == 0 {
|
|
data.DisplayName = data.Name // when displayName is missing we use name instead for displaying so we might as well convert it here.
|
|
} else if utf8.RuneCountInString(*data.DisplayName) > model.ChannelDisplayNameMaxRunes {
|
|
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.display_name_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Type == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.type_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if *data.Type != model.ChannelTypeOpen && *data.Type != model.ChannelTypePrivate {
|
|
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.type_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Header != nil && utf8.RuneCountInString(*data.Header) > model.ChannelHeaderMaxRunes {
|
|
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.header_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Purpose != nil && utf8.RuneCountInString(*data.Purpose) > model.ChannelPurposeMaxRunes {
|
|
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.purpose_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Scheme != nil && !model.IsValidSchemeName(*data.Scheme) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_channel_import_data.scheme_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidateUserImportData(data *UserImportData) *model.AppError {
|
|
if data.ProfileImage != nil && data.ProfileImageData == nil {
|
|
// Check if the resolved path is within the expected base path.
|
|
if _, valid := ValidateAttachmentPathForImport(*data.ProfileImage, model.ExportDataDir); !valid {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.invalid_image_path.error", map[string]any{"Path": *data.ProfileImage}, "", http.StatusBadRequest)
|
|
}
|
|
if _, err := os.Stat(*data.ProfileImage); os.IsNotExist(err) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.profile_image.error", nil, "", http.StatusNotFound).Wrap(err)
|
|
} else if err != nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.profile_image.error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
}
|
|
|
|
if data.Username == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.username_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if !model.IsValidUsername(model.NormalizeUsername(*data.Username)) { // we already lowercase the username while saving and querying so we are more forgiving here
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.username_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Email == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.email_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if *data.Email == "" || len(*data.Email) > model.UserEmailMaxLength {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.email_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.AuthData != nil && data.Password != nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.auth_data_and_password.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.AuthData != nil && len(*data.AuthData) > model.UserAuthDataMaxLength {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.auth_data_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
blank := func(str *string) bool {
|
|
if str == nil {
|
|
return true
|
|
}
|
|
return *str == ""
|
|
}
|
|
|
|
if (!blank(data.AuthService) && blank(data.AuthData)) || (blank(data.AuthService) && !blank(data.AuthData)) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.auth_data_and_service_dependency.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if appErr := validateAuthService(data.AuthService); appErr != nil {
|
|
return appErr
|
|
}
|
|
|
|
if data.Password != nil && *data.Password == "" {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.password_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Password != nil && len(*data.Password) > model.UserPasswordMaxLength {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.password_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Nickname != nil && utf8.RuneCountInString(*data.Nickname) > model.UserNicknameMaxRunes {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.nickname_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.FirstName != nil && utf8.RuneCountInString(*data.FirstName) > model.UserFirstNameMaxRunes {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.first_name_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.LastName != nil && utf8.RuneCountInString(*data.LastName) > model.UserLastNameMaxRunes {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.last_name_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Position != nil && utf8.RuneCountInString(*data.Position) > model.UserPositionMaxRunes {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.position_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Roles != nil && !model.IsValidUserRoles(*data.Roles) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.roles_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if err := validateGuestRoles(*data); err != nil {
|
|
return err
|
|
}
|
|
|
|
if data.NotifyProps != nil {
|
|
if data.NotifyProps.Desktop != nil && !isValidUserNotifyLevel(*data.NotifyProps.Desktop) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_desktop_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.NotifyProps.DesktopSound != nil && !isValidTrueOrFalseString(*data.NotifyProps.DesktopSound) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_desktop_sound_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.NotifyProps.Email != nil && !isValidTrueOrFalseString(*data.NotifyProps.Email) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_email_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.NotifyProps.Mobile != nil && !isValidUserNotifyLevel(*data.NotifyProps.Mobile) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_mobile_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.NotifyProps.MobilePushStatus != nil && !isValidPushStatusNotifyLevel(*data.NotifyProps.MobilePushStatus) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_mobile_push_status_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.NotifyProps.ChannelTrigger != nil && !isValidTrueOrFalseString(*data.NotifyProps.ChannelTrigger) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_channel_trigger_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.NotifyProps.CommentsTrigger != nil && !isValidCommentsNotifyLevel(*data.NotifyProps.CommentsTrigger) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.notify_props_comments_trigger_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
if data.UseMarkdownPreview != nil && !isValidTrueOrFalseString(*data.UseMarkdownPreview) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.advanced_props_feature_markdown_preview.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.UseFormatting != nil && !isValidTrueOrFalseString(*data.UseFormatting) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.advanced_props_formatting.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.ShowUnreadSection != nil && !isValidTrueOrFalseString(*data.ShowUnreadSection) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.advanced_props_show_unread_section.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.EmailInterval != nil && !isValidEmailBatchingInterval(*data.EmailInterval) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.advanced_props_email_interval.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Teams != nil {
|
|
return ValidateUserTeamsImportData(data.Teams)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidateBotImportData(data *BotImportData) *model.AppError {
|
|
if data.ProfileImage != nil && data.ProfileImageData == nil {
|
|
// Check if the resolved path is within the expected base path.
|
|
if _, valid := ValidateAttachmentPathForImport(*data.ProfileImage, model.ExportDataDir); !valid {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.invalid_image_path.error", map[string]any{"Path": *data.ProfileImage}, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if _, err := os.Stat(*data.ProfileImage); os.IsNotExist(err) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.profile_image.error", nil, "", http.StatusNotFound).Wrap(err)
|
|
} else if err != nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.profile_image.error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
}
|
|
|
|
if data.Username == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.username_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if !model.IsValidUsername(model.NormalizeUsername(*data.Username)) { // we already lowercase the username while saving and querying so we are more forgiving here
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.username_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.DisplayName != nil && utf8.RuneCountInString(*data.DisplayName) > model.UserFirstNameMaxRunes {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.first_name_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Owner == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_bot_import_data.owner_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if !model.IsValidUsername(*data.Owner) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.username_invalid.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
var validAuthServices = []string{
|
|
"",
|
|
model.UserAuthServiceEmail,
|
|
model.UserAuthServiceGitlab,
|
|
model.UserAuthServiceSaml,
|
|
model.UserAuthServiceLdap,
|
|
model.ServiceGoogle,
|
|
model.ServiceOffice365,
|
|
model.ServiceOpenid,
|
|
}
|
|
|
|
func validateAuthService(authService *string) *model.AppError {
|
|
if authService == nil {
|
|
return nil
|
|
}
|
|
if slices.Contains(validAuthServices, *authService) {
|
|
return nil
|
|
}
|
|
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_teams_import_data.invalid_auth_service.error", map[string]any{"AuthService": *authService}, "", http.StatusBadRequest)
|
|
}
|
|
|
|
func ValidateUserTeamsImportData(data *[]UserTeamImportData) *model.AppError {
|
|
if data == nil {
|
|
return nil
|
|
}
|
|
|
|
for _, tdata := range *data {
|
|
if tdata.Name == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_teams_import_data.team_name_missing.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if tdata.Roles != nil && !model.IsValidUserRoles(*tdata.Roles) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_teams_import_data.invalid_roles.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if tdata.Channels != nil {
|
|
if err := ValidateUserChannelsImportData(tdata.Channels); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if tdata.Theme != nil && strings.Trim(*tdata.Theme, " \t\r") != "" {
|
|
var unused map[string]string
|
|
if err := json.NewDecoder(strings.NewReader(*tdata.Theme)).Decode(&unused); err != nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_teams_import_data.invalid_team_theme.error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidateUserChannelsImportData(data *[]UserChannelImportData) *model.AppError {
|
|
if data == nil {
|
|
return nil
|
|
}
|
|
|
|
for _, cdata := range *data {
|
|
if cdata.Name == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.channel_name_missing.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if cdata.Roles != nil && !model.IsValidUserRoles(*cdata.Roles) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.invalid_roles.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if cdata.NotifyProps != nil {
|
|
if cdata.NotifyProps.Desktop != nil && !model.IsChannelNotifyLevelValid(*cdata.NotifyProps.Desktop) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.invalid_notify_props_desktop.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if cdata.NotifyProps.Mobile != nil && !model.IsChannelNotifyLevelValid(*cdata.NotifyProps.Mobile) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.invalid_notify_props_mobile.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if cdata.NotifyProps.MarkUnread != nil && !model.IsChannelMarkUnreadLevelValid(*cdata.NotifyProps.MarkUnread) {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_channels_import_data.invalid_notify_props_mark_unread.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidateReactionImportData(data *ReactionImportData, parentCreateAt int64) *model.AppError {
|
|
if data.User == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.user_missing.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.EmojiName == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.emoji_name_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if utf8.RuneCountInString(*data.EmojiName) > model.EmojiNameMaxLength {
|
|
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.emoji_name_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.CreateAt == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.create_at_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if *data.CreateAt == 0 {
|
|
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.create_at_zero.error", nil, "", http.StatusBadRequest)
|
|
} else if *data.CreateAt < parentCreateAt {
|
|
return model.NewAppError("BulkImport", "app.import.validate_reaction_import_data.create_at_before_parent.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidateReplyImportData(data *ReplyImportData, parentCreateAt int64, maxPostSize int) *model.AppError {
|
|
if data.User == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.user_missing.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Message == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.message_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if utf8.RuneCountInString(*data.Message) > maxPostSize {
|
|
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.message_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.CreateAt == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.create_at_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if *data.CreateAt == 0 {
|
|
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.create_at_zero.error", nil, "", http.StatusBadRequest)
|
|
} else if *data.CreateAt < parentCreateAt {
|
|
mlog.Warn("Reply CreateAt is before parent post CreateAt", mlog.Int("reply_create_at", *data.CreateAt), mlog.Int("parent_create_at", parentCreateAt))
|
|
}
|
|
|
|
if data.Props != nil && utf8.RuneCountInString(model.StringInterfaceToJSON(*data.Props)) > model.PostPropsMaxRunes {
|
|
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.props_too_large.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Reactions != nil {
|
|
for _, reaction := range *data.Reactions {
|
|
if err := ValidateReactionImportData(&reaction, *data.CreateAt); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if data.Attachments != nil {
|
|
for _, attachment := range *data.Attachments {
|
|
if err := ValidateAttachmentImportData(&attachment); err != nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_reply_import_data.attachment.error", nil, "", http.StatusNotFound).Wrap(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidatePostImportData(data *PostImportData, maxPostSize int) *model.AppError {
|
|
if data.Team == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.team_missing.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Channel == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.channel_missing.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.User == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.user_missing.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Message == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.message_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if utf8.RuneCountInString(*data.Message) > maxPostSize {
|
|
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.message_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.CreateAt == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.create_at_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if *data.CreateAt == 0 {
|
|
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.create_at_zero.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Reactions != nil {
|
|
for _, reaction := range *data.Reactions {
|
|
if err := ValidateReactionImportData(&reaction, *data.CreateAt); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if data.Replies != nil {
|
|
for _, reply := range *data.Replies {
|
|
if err := ValidateReplyImportData(&reply, *data.CreateAt, maxPostSize); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if data.Props != nil && utf8.RuneCountInString(model.StringInterfaceToJSON(*data.Props)) > model.PostPropsMaxRunes {
|
|
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.props_too_large.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Attachments != nil {
|
|
for _, attachment := range *data.Attachments {
|
|
if err := ValidateAttachmentImportData(&attachment); err != nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.attachment.error", nil, "", http.StatusNotFound).Wrap(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if data.ThreadFollowers != nil {
|
|
for _, follower := range *data.ThreadFollowers {
|
|
if err := ValidateThreadFollowerImportData(&follower); err != nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_post_import_data.thread_follower.error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidateDirectChannelImportData(data *DirectChannelImportData) *model.AppError {
|
|
if data.Participants == nil && data.Members == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.members_required.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Participants != nil && len(data.Participants) != 2 {
|
|
if len(data.Participants) < model.ChannelGroupMinUsers {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.members_too_few.error", nil, "", http.StatusBadRequest)
|
|
} else if len(data.Participants) > model.ChannelGroupMaxUsers {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.members_too_many.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
if data.Members != nil && len(*data.Members) != 2 {
|
|
if len(*data.Members) < model.ChannelGroupMinUsers {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.members_too_few.error", nil, "", http.StatusBadRequest)
|
|
} else if len(*data.Members) > model.ChannelGroupMaxUsers {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.members_too_many.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
if data.Header != nil && utf8.RuneCountInString(*data.Header) > model.ChannelHeaderMaxRunes {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.header_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.FavoritedBy != nil {
|
|
for _, favoriter := range *data.FavoritedBy {
|
|
found := false
|
|
for _, member := range data.Participants {
|
|
if favoriter == *member.Username {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if data.Members != nil {
|
|
if slices.Contains(*data.Members, favoriter) {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_channel_import_data.unknown_favoriter.error", map[string]any{"Username": favoriter}, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidateDirectPostImportData(data *DirectPostImportData, maxPostSize int) *model.AppError {
|
|
if data.ChannelMembers == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.channel_members_required.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if len(*data.ChannelMembers) != 2 {
|
|
if len(*data.ChannelMembers) < model.ChannelGroupMinUsers {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.channel_members_too_few.error", nil, "", http.StatusBadRequest)
|
|
} else if len(*data.ChannelMembers) > model.ChannelGroupMaxUsers {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.channel_members_too_many.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
|
|
if data.User == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.user_missing.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Message == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.message_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if utf8.RuneCountInString(*data.Message) > maxPostSize {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.message_length.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.CreateAt == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.create_at_missing.error", nil, "", http.StatusBadRequest)
|
|
} else if *data.CreateAt == 0 {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.create_at_zero.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.FlaggedBy != nil {
|
|
for _, flagger := range *data.FlaggedBy {
|
|
found := slices.Contains(*data.ChannelMembers, flagger)
|
|
if !found {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.unknown_flagger.error", map[string]any{"Username": flagger}, "", http.StatusBadRequest)
|
|
}
|
|
}
|
|
}
|
|
|
|
if data.Reactions != nil {
|
|
for _, reaction := range *data.Reactions {
|
|
if err := ValidateReactionImportData(&reaction, *data.CreateAt); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if data.Replies != nil {
|
|
for _, reply := range *data.Replies {
|
|
if err := ValidateReplyImportData(&reply, *data.CreateAt, maxPostSize); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if data.Attachments != nil {
|
|
for _, attachment := range *data.Attachments {
|
|
if err := ValidateAttachmentImportData(&attachment); err != nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.attachment.error", nil, "", http.StatusNotFound).Wrap(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
if data.ThreadFollowers != nil {
|
|
for _, follower := range *data.ThreadFollowers {
|
|
if err := ValidateThreadFollowerImportData(&follower); err != nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_direct_post_import_data.thread_follower.error", nil, "", http.StatusBadRequest).Wrap(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateEmojiImportData validates emoji data and returns if the import name
|
|
// conflicts with a system emoji.
|
|
func ValidateEmojiImportData(data *EmojiImportData) *model.AppError {
|
|
if data == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.empty.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Name == nil || *data.Name == "" {
|
|
return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.name_missing.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.Image == nil || *data.Image == "" {
|
|
return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.image_missing.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
// Check if the resolved path is within the expected base path.
|
|
if _, valid := ValidateAttachmentPathForImport(*data.Image, model.ExportDataDir); !valid {
|
|
return model.NewAppError("BulkImport", "app.import.validate_emoji_import_data.invalid_image_path.error", map[string]any{"Path": *data.Image}, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if err := model.IsValidEmojiName(*data.Name); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func ValidateThreadFollowerImportData(data *ThreadFollowerImportData) *model.AppError {
|
|
if data == nil {
|
|
return model.NewAppError("BulkImport", "app.import.validate_thread_follower_data.empty.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
if data.User == nil || *data.User == "" {
|
|
return model.NewAppError("BulkImport", "app.import.validate_thread_follower_data.user_missing.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func isValidTrueOrFalseString(value string) bool {
|
|
return value == "true" || value == "false"
|
|
}
|
|
|
|
func isValidUserNotifyLevel(notifyLevel string) bool {
|
|
return notifyLevel == model.ChannelNotifyAll ||
|
|
notifyLevel == model.ChannelNotifyMention ||
|
|
notifyLevel == model.ChannelNotifyNone
|
|
}
|
|
|
|
func isValidPushStatusNotifyLevel(notifyLevel string) bool {
|
|
return notifyLevel == model.StatusOnline ||
|
|
notifyLevel == model.StatusAway ||
|
|
notifyLevel == model.StatusOffline
|
|
}
|
|
|
|
func isValidCommentsNotifyLevel(notifyLevel string) bool {
|
|
return notifyLevel == model.CommentsNotifyAny ||
|
|
notifyLevel == model.CommentsNotifyRoot ||
|
|
notifyLevel == model.CommentsNotifyNever
|
|
}
|
|
|
|
func isValidEmailBatchingInterval(emailInterval string) bool {
|
|
return emailInterval == model.PreferenceEmailIntervalImmediately ||
|
|
emailInterval == model.PreferenceEmailIntervalFifteen ||
|
|
emailInterval == model.PreferenceEmailIntervalHour
|
|
}
|
|
|
|
// validateGuestRoles checks if the user has guest roles consistently across system, team, and channel levels.
|
|
// At this point, we assume that the user has a valid role scheme.
|
|
func validateGuestRoles(data UserImportData) *model.AppError {
|
|
if data.Roles == nil {
|
|
return nil
|
|
}
|
|
isSystemGuest := model.IsInRole(*data.Roles, model.SystemGuestRoleId)
|
|
|
|
// If user has no teams, they can still be a system guest without issue
|
|
if data.Teams == nil || len(*data.Teams) == 0 {
|
|
return nil
|
|
}
|
|
|
|
var isTeamGuest, isChannelGuest bool
|
|
var hasChannels bool // hasChannels indicates if the user has any channels within their teams
|
|
|
|
var teamGuestCount, channelGuestCount int
|
|
var totalTeams, totalChannels int
|
|
|
|
totalTeams = len(*data.Teams)
|
|
for _, team := range *data.Teams {
|
|
if team.Roles != nil && model.IsInRole(*team.Roles, model.TeamGuestRoleId) {
|
|
teamGuestCount++
|
|
}
|
|
|
|
if len(model.SafeDereference(team.Channels)) > 0 {
|
|
hasChannels = true
|
|
totalChannels += len(*team.Channels)
|
|
|
|
for _, channel := range *team.Channels {
|
|
if channel.Roles != nil && model.IsInRole(*channel.Roles, model.ChannelGuestRoleId) {
|
|
channelGuestCount++
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Set flags based on whether all available teams/channels have guest roles
|
|
if totalTeams > 0 && teamGuestCount == totalTeams {
|
|
isTeamGuest = true
|
|
}
|
|
|
|
if hasChannels && channelGuestCount == totalChannels {
|
|
isChannelGuest = true
|
|
}
|
|
|
|
// If the user is a system guest, they must have consistent guest roles in any teams/channels they belong to
|
|
if isSystemGuest {
|
|
// If they have teams, they must be a team guest in all teams
|
|
if totalTeams > 0 && !isTeamGuest {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.system_guest_missing_team_guest_roles.error", map[string]any{"TeamGuestCount": teamGuestCount, "TotalTeams": totalTeams}, "", http.StatusBadRequest)
|
|
}
|
|
|
|
// If they have channels, they must be a channel guest in all channels
|
|
if hasChannels && !isChannelGuest {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.system_guest_missing_channel_guest_roles.error", map[string]any{"ChannelGuestCount": channelGuestCount, "TotalChannels": totalChannels}, "", http.StatusBadRequest)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// If not a system guest, ensure consistency in the other direction
|
|
// If they're a team or channel guest, they must be a system guest
|
|
if (isTeamGuest || isChannelGuest) && !isSystemGuest {
|
|
if isTeamGuest {
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.team_guest_missing_system_guest_role.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
return model.NewAppError("BulkImport", "app.import.validate_user_import_data.channel_guest_missing_system_guest_role.error", nil, "", http.StatusBadRequest)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// ValidateAttachmentPathForImport joins 'path' to 'basePath' (defaulting to "." if empty) and ensures
|
|
// the result does not escape the base directory. Returns the cleaned joined path (and true),
|
|
// or an empty string (and false) if the result escapes the base.
|
|
func ValidateAttachmentPathForImport(path, basePath string) (string, bool) {
|
|
if basePath == "" {
|
|
basePath = "."
|
|
}
|
|
|
|
joined := filepath.Join(basePath, path)
|
|
|
|
// Check if the resolved joined path is within basePath
|
|
rel, err := filepath.Rel(basePath, joined)
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
if strings.HasPrefix(rel, ".."+string(filepath.Separator)) || rel == ".." {
|
|
return "", false
|
|
}
|
|
|
|
return joined, true
|
|
}
|
|
|
|
func ValidateAttachmentImportData(data *AttachmentImportData) *model.AppError {
|
|
if data == nil {
|
|
return nil
|
|
}
|
|
|
|
if data.Path == nil || *data.Path == "" {
|
|
return nil
|
|
}
|
|
|
|
// Check if the resolved path is within the expected base path.
|
|
if _, valid := ValidateAttachmentPathForImport(*data.Path, model.ExportDataDir); !valid {
|
|
return model.NewAppError("BulkImport", "app.import.validate_attachment_import_data.invalid_path.error", map[string]any{"Path": *data.Path}, "", http.StatusBadRequest)
|
|
}
|
|
|
|
return nil
|
|
}
|