package platform import ( "database/sql" "encoding/json" "net/http" "reflect" "strings" ) func validCanvasFrameExtensions(d map[string]any, v2 bool) bool { for _, key := range []string{"subgraph", "ports", "presentation"} { value, exists := d[key] if !exists { continue } if !v2 || d["kind"] != "frame" { return false } switch key { case "subgraph": m, ok := value.(map[string]any) if !ok || !canvasFields(m, "boardId") { return false } id, ok := m["boardId"].(string) if !ok || !uuidSlugPattern.MatchString(id) { return false } case "ports": ports, ok := value.([]any) if !ok || len(ports) > 4000 { return false } seen := map[string]bool{} for _, v := range ports { p, ok := v.(map[string]any) if !ok || !canvasFields(p, "id", "direction", "nodeId", "handle") { return false } id, ok := p["id"].(string) if !ok || !canvasID.MatchString(id) || seen[id] { return false } seen[id] = true node, ok := p["nodeId"].(string) if !ok || !canvasID.MatchString(node) || !canvasChoice(p["direction"], "input", "output") || !validCanvasPortHandle(p["handle"]) { return false } } case "presentation": m, ok := value.(map[string]any) if !ok || !canvasFields(m, "collapsed", "contentScale", "expandedSize", "collapsedSize") { return false } if v, exists := m["collapsed"]; exists { if _, ok := v.(bool); !ok { return false } } if v, exists := m["contentScale"]; exists && !canvasNumber(v, .05, 4) { return false } for _, key := range []string{"expandedSize", "collapsedSize"} { if v, exists := m[key]; exists { size, ok := v.(map[string]any) if !ok || !canvasFields(size, "width", "height") || !canvasNumber(size["width"], 32, 100000) || !canvasNumber(size["height"], 32, 100000) { return false } } } } } return true } func canvasNodeData(raw json.RawMessage) map[string]map[string]any { var doc map[string]any _ = json.Unmarshal(raw, &doc) result := map[string]map[string]any{} nodes, _ := doc["nodes"].([]any) for _, v := range nodes { n := v.(map[string]any) result[n["id"].(string)] = n["data"].(map[string]any) } return result } // Ordinary saves cannot create, replace, detach or retarget ownership/ports. // Cross-board commands validate these changes while holding the workspace lock. func validateCanvasRelations(r *http.Request, tx *sql.Tx, project, workspace, board string, before, after json.RawMessage) (bool, error) { old, next := canvasNodeData(before), canvasNodeData(after) rows, err := tx.QueryContext(r.Context(), `SELECT frame_id,child_board_id::text FROM canvas_subgraphs WHERE project=$1 AND workspace_id=$2 AND parent_board_id=$3 AND archived_at IS NULL`, project, workspace, board) if err != nil { return false, err } linked := map[string]string{} for rows.Next() { var frame, child string if err = rows.Scan(&frame, &child); err != nil { rows.Close() return false, err } linked[frame] = child } err = rows.Err() rows.Close() if err != nil { return false, err } for frame, child := range linked { d := next[frame] sub, _ := d["subgraph"].(map[string]any) if d["kind"] != "frame" || sub["boardId"] != child || !reflect.DeepEqual(old[frame]["ports"], d["ports"]) { return false, nil } } for id, d := range next { if sub, exists := d["subgraph"]; exists { m, _ := sub.(map[string]any) if linked[id] == "" || m["boardId"] != linked[id] { return false, nil } } if ports, ok := d["ports"].([]any); ok && len(ports) > 0 && linked[id] == "" { return false, nil } if frame, ok := d["frameId"].(string); ok && linked[frame] != "" { return false, nil } } // A child node referenced by its parent's ports must not disappear or change kind. var parentRaw json.RawMessage err = tx.QueryRowContext(r.Context(), `SELECT m.canvas_document FROM canvas_subgraphs s JOIN materials m ON m.project=s.project AND m.workspace_id=s.workspace_id AND m.id=s.parent_board_id WHERE s.project=$1 AND s.workspace_id=$2 AND s.child_board_id=$3 AND s.archived_at IS NULL`, project, workspace, board).Scan(&parentRaw) if err == sql.ErrNoRows { return true, nil } if err != nil { return false, err } for _, d := range canvasNodeData(parentRaw) { sub, _ := d["subgraph"].(map[string]any) if sub["boardId"] != board { continue } ports, _ := d["ports"].([]any) for _, v := range ports { p := v.(map[string]any) id, _ := p["nodeId"].(string) if next[id] == nil || next[id]["kind"] != old[id]["kind"] { return false, nil } } } return true, nil } func validCanvasPortHandle(value any) bool { if canvasChoice(value, "top", "right", "bottom", "left") { return true } h, ok := value.(string) return ok && strings.HasPrefix(h, "port:") && canvasID.MatchString(strings.TrimPrefix(h, "port:")) }