mirror of
https://github.com/mattermost/focalboard.git
synced 2024-12-21 13:38:56 +02:00
5dfd402e26
* Adds limit check for block titles * Adds limit check for the aggregation of the fields * Fix linter * Adds tests * Fix err check method order
293 lines
8.4 KiB
Go
293 lines
8.4 KiB
Go
package model
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"strconv"
|
|
"unicode/utf8"
|
|
|
|
"github.com/mattermost/focalboard/server/services/audit"
|
|
)
|
|
|
|
const (
|
|
BlockTitleMaxBytes = 65535 // Maximum size of a TEXT column in MySQL
|
|
BlockTitleMaxRunes = BlockTitleMaxBytes / 4 // Assume a worst-case representation
|
|
BlockFieldsMaxRunes = 800000
|
|
)
|
|
|
|
var (
|
|
ErrBlockEmptyBoardID = errors.New("boardID is empty")
|
|
ErrBlockTitleSizeLimitExceeded = errors.New("block title size limit exceeded")
|
|
ErrBlockFieldsSizeLimitExceeded = errors.New("block fields size limit exceeded")
|
|
)
|
|
|
|
// Block is the basic data unit
|
|
// swagger:model
|
|
type Block struct {
|
|
// The id for this block
|
|
// required: true
|
|
ID string `json:"id"`
|
|
|
|
// The id for this block's parent block. Empty for root blocks
|
|
// required: false
|
|
ParentID string `json:"parentId"`
|
|
|
|
// The id for user who created this block
|
|
// required: true
|
|
CreatedBy string `json:"createdBy"`
|
|
|
|
// The id for user who last modified this block
|
|
// required: true
|
|
ModifiedBy string `json:"modifiedBy"`
|
|
|
|
// The schema version of this block
|
|
// required: true
|
|
Schema int64 `json:"schema"`
|
|
|
|
// The block type
|
|
// required: true
|
|
Type BlockType `json:"type"`
|
|
|
|
// The display title
|
|
// required: false
|
|
Title string `json:"title"`
|
|
|
|
// The block fields
|
|
// required: false
|
|
Fields map[string]interface{} `json:"fields"`
|
|
|
|
// The creation time in miliseconds since the current epoch
|
|
// required: true
|
|
CreateAt int64 `json:"createAt"`
|
|
|
|
// The last modified time in miliseconds since the current epoch
|
|
// required: true
|
|
UpdateAt int64 `json:"updateAt"`
|
|
|
|
// The deleted time in miliseconds since the current epoch. Set to indicate this block is deleted
|
|
// required: false
|
|
DeleteAt int64 `json:"deleteAt"`
|
|
|
|
// Deprecated. The workspace id that the block belongs to
|
|
// required: false
|
|
WorkspaceID string `json:"-"`
|
|
|
|
// The board id that the block belongs to
|
|
// required: true
|
|
BoardID string `json:"boardId"`
|
|
|
|
// Indicates if the card is limited
|
|
// required: false
|
|
Limited bool `json:"limited,omitempty"`
|
|
}
|
|
|
|
// BlockPatch is a patch for modify blocks
|
|
// swagger:model
|
|
type BlockPatch struct {
|
|
// The id for this block's parent block. Empty for root blocks
|
|
// required: false
|
|
ParentID *string `json:"parentId"`
|
|
|
|
// The schema version of this block
|
|
// required: false
|
|
Schema *int64 `json:"schema"`
|
|
|
|
// The block type
|
|
// required: false
|
|
Type *BlockType `json:"type"`
|
|
|
|
// The display title
|
|
// required: false
|
|
Title *string `json:"title"`
|
|
|
|
// The block updated fields
|
|
// required: false
|
|
UpdatedFields map[string]interface{} `json:"updatedFields"`
|
|
|
|
// The block removed fields
|
|
// required: false
|
|
DeletedFields []string `json:"deletedFields"`
|
|
}
|
|
|
|
// BlockPatchBatch is a batch of IDs and patches for modify blocks
|
|
// swagger:model
|
|
type BlockPatchBatch struct {
|
|
// The id's for of the blocks to patch
|
|
BlockIDs []string `json:"block_ids"`
|
|
|
|
// The BlockPatches to be applied
|
|
BlockPatches []BlockPatch `json:"block_patches"`
|
|
}
|
|
|
|
// BoardModifier is a callback that can modify each board during an import.
|
|
// A cache of arbitrary data will be passed for each call and any changes
|
|
// to the cache will be preserved for the next call.
|
|
// Return true to import the block or false to skip import.
|
|
type BoardModifier func(board *Board, cache map[string]interface{}) bool
|
|
|
|
// BlockModifier is a callback that can modify each block during an import.
|
|
// A cache of arbitrary data will be passed for each call and any changes
|
|
// to the cache will be preserved for the next call.
|
|
// Return true to import the block or false to skip import.
|
|
type BlockModifier func(block *Block, cache map[string]interface{}) bool
|
|
|
|
func BlocksFromJSON(data io.Reader) []*Block {
|
|
var blocks []*Block
|
|
_ = json.NewDecoder(data).Decode(&blocks)
|
|
return blocks
|
|
}
|
|
|
|
// IsValid checks the block for errors before inserting, and makes
|
|
// sure it complies with the requirements of a valid block.
|
|
func (b *Block) IsValid() error {
|
|
if b.BoardID == "" {
|
|
return ErrBlockEmptyBoardID
|
|
}
|
|
|
|
if utf8.RuneCountInString(b.Title) > BlockTitleMaxRunes {
|
|
return ErrBlockTitleSizeLimitExceeded
|
|
}
|
|
|
|
fieldsJSON, err := json.Marshal(b.Fields)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if utf8.RuneCountInString(string(fieldsJSON)) > BlockFieldsMaxRunes {
|
|
return ErrBlockFieldsSizeLimitExceeded
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// LogClone implements the `mlog.LogCloner` interface to provide a subset of Block fields for logging.
|
|
func (b *Block) LogClone() interface{} {
|
|
return struct {
|
|
ID string
|
|
ParentID string
|
|
BoardID string
|
|
Type BlockType
|
|
}{
|
|
ID: b.ID,
|
|
ParentID: b.ParentID,
|
|
BoardID: b.BoardID,
|
|
Type: b.Type,
|
|
}
|
|
}
|
|
|
|
// Patch returns an update version of the block.
|
|
func (p *BlockPatch) Patch(block *Block) *Block {
|
|
if p.ParentID != nil {
|
|
block.ParentID = *p.ParentID
|
|
}
|
|
|
|
if p.Schema != nil {
|
|
block.Schema = *p.Schema
|
|
}
|
|
|
|
if p.Type != nil {
|
|
block.Type = *p.Type
|
|
}
|
|
|
|
if p.Title != nil {
|
|
block.Title = *p.Title
|
|
}
|
|
|
|
for key, field := range p.UpdatedFields {
|
|
block.Fields[key] = field
|
|
}
|
|
|
|
for _, key := range p.DeletedFields {
|
|
delete(block.Fields, key)
|
|
}
|
|
|
|
return block
|
|
}
|
|
|
|
type QueryBlocksOptions struct {
|
|
BoardID string // if not empty then filter for blocks belonging to specified board
|
|
ParentID string // if not empty then filter for blocks belonging to specified parent
|
|
BlockType BlockType // if not empty and not `TypeUnknown` then filter for records of specified block type
|
|
Page int // page number to select when paginating
|
|
PerPage int // number of blocks per page (default=-1, meaning unlimited)
|
|
}
|
|
|
|
// QuerySubtreeOptions are query options that can be passed to GetSubTree methods.
|
|
type QuerySubtreeOptions struct {
|
|
BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt
|
|
AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt
|
|
Limit uint64 // if non-zero then limit the number of returned records
|
|
}
|
|
|
|
// QueryBlockHistoryOptions are query options that can be passed to GetBlockHistory.
|
|
type QueryBlockHistoryOptions struct {
|
|
BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt
|
|
AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt
|
|
Limit uint64 // if non-zero then limit the number of returned records
|
|
Descending bool // if true then the records are sorted by insert_at in descending order
|
|
}
|
|
|
|
// QueryBoardHistoryOptions are query options that can be passed to GetBoardHistory.
|
|
type QueryBoardHistoryOptions struct {
|
|
BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt
|
|
AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt
|
|
Limit uint64 // if non-zero then limit the number of returned records
|
|
Descending bool // if true then the records are sorted by insert_at in descending order
|
|
}
|
|
|
|
// QueryBlockHistoryOptions are query options that can be passed to GetBlockHistory.
|
|
type QueryBlockHistoryChildOptions struct {
|
|
BeforeUpdateAt int64 // if non-zero then filter for records with update_at less than BeforeUpdateAt
|
|
AfterUpdateAt int64 // if non-zero then filter for records with update_at greater than AfterUpdateAt
|
|
Page int // page number to select when paginating
|
|
PerPage int // number of blocks per page (default=-1, meaning unlimited)
|
|
}
|
|
|
|
func StampModificationMetadata(userID string, blocks []*Block, auditRec *audit.Record) {
|
|
if userID == SingleUser {
|
|
userID = ""
|
|
}
|
|
|
|
now := GetMillis()
|
|
for i := range blocks {
|
|
blocks[i].ModifiedBy = userID
|
|
blocks[i].UpdateAt = now
|
|
|
|
if auditRec != nil {
|
|
auditRec.AddMeta("block_"+strconv.FormatInt(int64(i), 10), blocks[i])
|
|
}
|
|
}
|
|
}
|
|
|
|
func (b *Block) ShouldBeLimited(cardLimitTimestamp int64) bool {
|
|
return b.Type == TypeCard &&
|
|
b.UpdateAt < cardLimitTimestamp
|
|
}
|
|
|
|
// Returns a limited version of the block that doesn't contain the
|
|
// contents of the block, only its IDs and type.
|
|
func (b *Block) GetLimited() *Block {
|
|
newBlock := &Block{
|
|
Title: b.Title,
|
|
ID: b.ID,
|
|
ParentID: b.ParentID,
|
|
BoardID: b.BoardID,
|
|
Schema: b.Schema,
|
|
Type: b.Type,
|
|
CreateAt: b.CreateAt,
|
|
UpdateAt: b.UpdateAt,
|
|
DeleteAt: b.DeleteAt,
|
|
WorkspaceID: b.WorkspaceID,
|
|
Limited: true,
|
|
}
|
|
|
|
if iconField, ok := b.Fields["icon"]; ok {
|
|
newBlock.Fields = map[string]interface{}{
|
|
"icon": iconField,
|
|
}
|
|
}
|
|
|
|
return newBlock
|
|
}
|