// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. package app import ( "database/sql" "encoding/json" "net/http" "sort" "github.com/mattermost/mattermost/server/public/model" "github.com/mattermost/mattermost/server/public/shared/mlog" "github.com/mattermost/mattermost/server/v8/channels/store" "github.com/pkg/errors" ) const ( CustomProfileAttributesFieldLimit = 20 ) // ensureCPAFieldAttrs ensures that PropertyField.Attrs has default values for CPA fields // that were created before default attrs were enforced. This provides backward compatibility. func ensureCPAFieldAttrs(field *model.PropertyField) { if field == nil { return } if field.Attrs == nil { field.Attrs = model.StringInterface{} } // Ensure visibility has a default value (this is the most critical attr) if visibility, ok := field.Attrs[model.CustomProfileAttributesPropertyAttrsVisibility]; !ok || visibility == "" { field.Attrs[model.CustomProfileAttributesPropertyAttrsVisibility] = model.CustomProfileAttributesVisibilityDefault } // Ensure sort_order exists (defaults to 0 if missing, which is fine) if _, ok := field.Attrs[model.CustomProfileAttributesPropertyAttrsSortOrder]; !ok { field.Attrs[model.CustomProfileAttributesPropertyAttrsSortOrder] = float64(0) } } var cpaGroupID string // ToDo: we should explore moving this to the database cache layer // instead of maintaining the ID cached at the application level func (a *App) CpaGroupID() (string, error) { if cpaGroupID != "" { return cpaGroupID, nil } cpaGroup, err := a.Srv().propertyService.RegisterPropertyGroup(model.CustomProfileAttributesPropertyGroupName) if err != nil { return "", errors.Wrap(err, "cannot register Custom Profile Attributes property group") } cpaGroupID = cpaGroup.ID return cpaGroupID, nil } func (a *App) GetCPAField(fieldID string) (*model.PropertyField, *model.AppError) { groupID, err := a.CpaGroupID() if err != nil { return nil, model.NewAppError("GetCPAField", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } field, err := a.Srv().propertyService.GetPropertyField(groupID, fieldID) if err != nil { switch { case errors.Is(err, sql.ErrNoRows): return nil, model.NewAppError("GetCPAField", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("GetCPAField", "app.custom_profile_attributes.get_property_field.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } ensureCPAFieldAttrs(field) return field, nil } func (a *App) ListCPAFields() ([]*model.PropertyField, *model.AppError) { groupID, err := a.CpaGroupID() if err != nil { return nil, model.NewAppError("GetCPAFields", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } opts := model.PropertyFieldSearchOpts{ GroupID: groupID, PerPage: CustomProfileAttributesFieldLimit, } fields, err := a.Srv().propertyService.SearchPropertyFields(groupID, opts) if err != nil { return nil, model.NewAppError("GetCPAFields", "app.custom_profile_attributes.search_property_fields.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } for _, field := range fields { ensureCPAFieldAttrs(field) } sort.Slice(fields, func(i, j int) bool { return model.CPASortOrder(fields[i]) < model.CPASortOrder(fields[j]) }) return fields, nil } func (a *App) CreateCPAField(field *model.CPAField) (*model.PropertyField, *model.AppError) { groupID, err := a.CpaGroupID() if err != nil { return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } fieldCount, err := a.Srv().propertyService.CountActivePropertyFieldsForGroup(groupID) if err != nil { return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.count_property_fields.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if fieldCount >= CustomProfileAttributesFieldLimit { return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.limit_reached.app_error", nil, "", http.StatusUnprocessableEntity).Wrap(err) } field.GroupID = groupID if appErr := field.SanitizeAndValidate(); appErr != nil { return nil, appErr } newField, err := a.Srv().propertyService.CreatePropertyField(field.ToPropertyField()) if err != nil { var appErr *model.AppError switch { case errors.As(err, &appErr): return nil, appErr default: return nil, model.NewAppError("CreateCPAField", "app.custom_profile_attributes.create_property_field.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } message := model.NewWebSocketEvent(model.WebsocketEventCPAFieldCreated, "", "", "", nil, "") message.Add("field", newField) a.Publish(message) return newField, nil } func (a *App) PatchCPAField(fieldID string, patch *model.PropertyFieldPatch) (*model.PropertyField, *model.AppError) { existingField, appErr := a.GetCPAField(fieldID) if appErr != nil { return nil, appErr } shouldDeleteValues := false if patch.Type != nil && *patch.Type != existingField.Type { shouldDeleteValues = true } // custom profile attributes doesn't use targets patch.TargetID = nil patch.TargetType = nil existingField.Patch(patch) cpaField, err := model.NewCPAFieldFromPropertyField(existingField) if err != nil { return nil, model.NewAppError("PatchCPAField", "app.custom_profile_attributes.property_field_conversion.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if appErr := cpaField.SanitizeAndValidate(); appErr != nil { return nil, appErr } groupID, err := a.CpaGroupID() if err != nil { return nil, model.NewAppError("PatchCPAField", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } patchedField, err := a.Srv().propertyService.UpdatePropertyField(groupID, cpaField.ToPropertyField()) if err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return nil, model.NewAppError("PatchCPAField", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound).Wrap(err) default: return nil, model.NewAppError("PatchCPAField", "app.custom_profile_attributes.property_field_update.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } if shouldDeleteValues { if dErr := a.Srv().propertyService.DeletePropertyValuesForField(groupID, patchedField.ID); dErr != nil { a.Log().Error("Error deleting property values when updating field", mlog.String("fieldID", patchedField.ID), mlog.Err(dErr), ) } } message := model.NewWebSocketEvent(model.WebsocketEventCPAFieldUpdated, "", "", "", nil, "") message.Add("field", patchedField) message.Add("delete_values", shouldDeleteValues) a.Publish(message) return patchedField, nil } func (a *App) DeleteCPAField(id string) *model.AppError { groupID, err := a.CpaGroupID() if err != nil { return model.NewAppError("DeleteCPAField", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if err := a.Srv().propertyService.DeletePropertyField(groupID, id); err != nil { var nfErr *store.ErrNotFound switch { case errors.As(err, &nfErr): return model.NewAppError("DeleteCPAField", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound).Wrap(err) default: return model.NewAppError("DeleteCPAField", "app.custom_profile_attributes.property_field_delete.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } } message := model.NewWebSocketEvent(model.WebsocketEventCPAFieldDeleted, "", "", "", nil, "") message.Add("field_id", id) a.Publish(message) return nil } func (a *App) ListCPAValues(userID string) ([]*model.PropertyValue, *model.AppError) { groupID, err := a.CpaGroupID() if err != nil { return nil, model.NewAppError("GetCPAFields", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } values, err := a.Srv().propertyService.SearchPropertyValues(groupID, model.PropertyValueSearchOpts{ TargetIDs: []string{userID}, PerPage: CustomProfileAttributesFieldLimit, }) if err != nil { return nil, model.NewAppError("ListCPAValues", "app.custom_profile_attributes.list_property_values.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return values, nil } func (a *App) GetCPAValue(valueID string) (*model.PropertyValue, *model.AppError) { groupID, err := a.CpaGroupID() if err != nil { return nil, model.NewAppError("GetCPAValue", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } value, err := a.Srv().propertyService.GetPropertyValue(groupID, valueID) if err != nil { return nil, model.NewAppError("GetCPAValue", "app.custom_profile_attributes.get_property_field.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } return value, nil } func (a *App) PatchCPAValue(userID string, fieldID string, value json.RawMessage, allowSynced bool) (*model.PropertyValue, *model.AppError) { values, appErr := a.PatchCPAValues(userID, map[string]json.RawMessage{fieldID: value}, allowSynced) if appErr != nil { return nil, appErr } return values[0], nil } func (a *App) PatchCPAValues(userID string, fieldValueMap map[string]json.RawMessage, allowSynced bool) ([]*model.PropertyValue, *model.AppError) { groupID, err := a.CpaGroupID() if err != nil { return nil, model.NewAppError("PatchCPAValues", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } valuesToUpdate := []*model.PropertyValue{} for fieldID, rawValue := range fieldValueMap { // make sure field exists in this group existingField, appErr := a.GetCPAField(fieldID) if appErr != nil { return nil, model.NewAppError("PatchCPAValues", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound).Wrap(appErr) } else if existingField.DeleteAt > 0 { return nil, model.NewAppError("PatchCPAValues", "app.custom_profile_attributes.property_field_not_found.app_error", nil, "", http.StatusNotFound) } cpaField, fErr := model.NewCPAFieldFromPropertyField(existingField) if fErr != nil { return nil, model.NewAppError("PatchCPAValues", "app.custom_profile_attributes.property_field_conversion.app_error", nil, "", http.StatusInternalServerError).Wrap(fErr) } if !allowSynced && cpaField.IsSynced() { return nil, model.NewAppError("PatchCPAValues", "app.custom_profile_attributes.property_field_is_synced.app_error", nil, "", http.StatusBadRequest) } sanitizedValue, sErr := model.SanitizeAndValidatePropertyValue(cpaField, rawValue) if sErr != nil { return nil, model.NewAppError("PatchCPAValues", "app.custom_profile_attributes.validate_value.app_error", nil, "", http.StatusBadRequest).Wrap(sErr) } value := &model.PropertyValue{ GroupID: groupID, TargetType: model.PropertyValueTargetTypeUser, TargetID: userID, FieldID: fieldID, Value: sanitizedValue, } valuesToUpdate = append(valuesToUpdate, value) } updatedValues, err := a.Srv().propertyService.UpsertPropertyValues(valuesToUpdate) if err != nil { return nil, model.NewAppError("PatchCPAValues", "app.custom_profile_attributes.property_value_upsert.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } updatedFieldValueMap := map[string]json.RawMessage{} for _, value := range updatedValues { updatedFieldValueMap[value.FieldID] = value.Value } message := model.NewWebSocketEvent(model.WebsocketEventCPAValuesUpdated, "", "", "", nil, "") message.Add("user_id", userID) message.Add("values", updatedFieldValueMap) a.Publish(message) return updatedValues, nil } func (a *App) DeleteCPAValues(userID string) *model.AppError { groupID, err := a.CpaGroupID() if err != nil { return model.NewAppError("DeleteCPAValues", "app.custom_profile_attributes.cpa_group_id.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } if err := a.Srv().propertyService.DeletePropertyValuesForTarget(groupID, "user", userID); err != nil { return model.NewAppError("DeleteCPAValues", "app.custom_profile_attributes.delete_property_values_for_user.app_error", nil, "", http.StatusInternalServerError).Wrap(err) } message := model.NewWebSocketEvent(model.WebsocketEventCPAValuesUpdated, "", "", "", nil, "") message.Add("user_id", userID) message.Add("values", map[string]json.RawMessage{}) a.Publish(message) return nil }