Files
upstream-mattermost/server/public/model/mm_blocks_actions.go
upstream-mirror 240db4e2a7
Some checks failed
Server CI / Postgres (shard 0) (push) Has been skipped
Server CI / Postgres (shard 3) (push) Has been skipped
BuildEnv Docker Image / build-image (push) Failing after 4s
BuildEnv Docker Image / build-image-fips (push) Failing after 4s
Migration Review & Release Notes / Generate migration review & release notes (push) Failing after 3s
Server CI / Build Environment (push) Failing after 3s
Server CI / Postgres (shard 1) (push) Has been skipped
Tools CI / check-style (mattermost-govet) (push) Failing after 4s
Server CI / Postgres (shard 2) (push) Has been skipped
Tools CI / check-style (mmgotool) (push) Failing after 3s
Server CI / Elasticsearch v8 Compatibility (push) Has been skipped
Server CI / OpenSearch v2 Compatibility (push) Has been skipped
Server CI / Postgres FIPS (shard 0) (push) Has been skipped
Tools CI / Test (mattermost-govet) (push) Failing after 3s
Server CI / Postgres FIPS (shard 1) (push) Has been skipped
Server CI / Postgres FIPS (shard 2) (push) Has been skipped
Tools CI / Test (mmgotool) (push) Failing after 3s
Server CI / Postgres FIPS (shard 3) (push) Has been skipped
Server CI / Run mmctl tests (push) Has been skipped
Server CI / Run mmctl tests (FIPS) (push) Has been skipped
Server CI / Merge Postgres Test Results (push) Has been skipped
Server CI / Merge Postgres FIPS Test Results (push) Has been skipped
Server CI / Check backport migrations (push) Has been skipped
Server CI / Check generated files (push) Has been skipped
Server CI / check-style (push) Has been skipped
Server CI / Vet API (push) Has been skipped
Server CI / Build mattermost server app (push) Has been skipped
Server CI / Server CI Complete (push) Failing after 2s
Server CI / Server CI Artifacts (push) Has been skipped
Server CI / Sentry Upload (push) Has been skipped
Server CI / Server CI Report (push) Failing after 5m1s
Server CI Nightly Race / Race Detector (push) Has been skipped
CodeQL / Analyze (go) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
Webapp CI Cache Warm / Warm Webapp node_modules Cache (push) Has been cancelled
E2E CI Cache Warm / Warm E2E npm Registry Cache (push) Has been cancelled
API / build (push) Has been cancelled
Web App CI / check-types (push) Has been cancelled
Web App CI / check-lint (push) Has been cancelled
Web App CI / check-i18n (push) Has been cancelled
Web App CI / check-external-links (push) Has been cancelled
Web App CI / test (platform) (push) Has been cancelled
Web App CI / test (mattermost-redux) (push) Has been cancelled
Web App CI / test (channels shard 1/4) (push) Has been cancelled
Web App CI / test (channels shard 2/4) (push) Has been cancelled
Web App CI / test (channels shard 3/4) (push) Has been cancelled
Web App CI / test (channels shard 4/4) (push) Has been cancelled
Web App CI / upload-coverage (push) Has been cancelled
Web App CI / build (push) Has been cancelled
YAML Lint / yamllint (push) Has been cancelled
Push mirrored docker images / cd/Push mirrored docker images (push) Has been cancelled
docs-cd / Build and deploy docs (push) Has been cancelled
Docs CI / build (push) Has been cancelled
E2E Cache Warm: Cypress deps / Warm Cypress deps cache (push) Has been cancelled
Scorecards supply-chain security / Scorecard analysis (push) Has been cancelled
Server CI Weekly / Postgres FIPS (push) Has been skipped
Server CI Weekly / Run mmctl tests (FIPS) (push) Has been skipped
Server CI Weekly / Postgres with binary parameters (push) Has been skipped
Server CI Weekly / Build Environment (push) Failing after 1s
Server CI Cache Warm / Warm Go Module and Build Cache (push) Failing after 2s
Server CI Nightly Race / Build Environment (push) Failing after 1s
snapshot of https://github.com/mattermost/mattermost.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 00:29:55 +05:00

299 lines
8.0 KiB
Go

// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Server-side definitions for the post.props.mm_blocks_actions registry that
// underpins the markdown-actions feature.
package model
import (
"encoding/json"
"errors"
"fmt"
"maps"
"net/url"
)
var (
// ErrMmBlocksActionNotFound is returned when the action id is missing or not an executable mm_blocks action.
ErrMmBlocksActionNotFound = errors.New("mm_blocks action not found")
)
// MmBlocksActionResolved is the outcome of resolving an mm_blocks action for execution.
type MmBlocksActionResolved struct {
OpenURLGoto string
ExternalURL string
Context map[string]any
}
const (
MmBlocksActionTypeExternal = "external"
MmBlocksActionTypeOpenURL = "openURL"
)
// MmBlocksActionSpec is the server-side definition for one entry in props.mm_blocks_actions.
type MmBlocksActionSpec struct {
Type string
URL string
Query map[string]string
Context map[string]any
}
// GetMmBlocksActionSpec returns the action definition for actionID from props.mm_blocks_actions, if present.
func (o *Post) GetMmBlocksActionSpec(actionID string) *MmBlocksActionSpec {
raw := o.GetProp(PostPropsMmBlocksActions)
if raw == nil || actionID == "" {
return nil
}
actionsTop, ok := coerceToStringAnyMap(raw)
if !ok {
return nil
}
entry, ok := actionsTop[actionID]
if !ok || entry == nil {
return nil
}
entryMap, ok := coerceToStringAnyMap(entry)
if !ok {
return nil
}
return mmBlocksEntryMapToSpec(entryMap)
}
// mmBlocksEntryMapToSpec maps one props.mm_blocks_actions[actionID] object to MmBlocksActionSpec.
func mmBlocksEntryMapToSpec(entryMap map[string]any) *MmBlocksActionSpec {
typ, _ := entryMap["type"].(string)
if typ == "" {
return nil
}
spec := &MmBlocksActionSpec{Type: typ}
switch typ {
case MmBlocksActionTypeExternal:
spec.URL, _ = entryMap["url"].(string)
spec.Context = contextMapFromProp(entryMap["context"])
spec.Query = stringMapFromPropValue(entryMap["query"])
return spec
case MmBlocksActionTypeOpenURL:
spec.URL, _ = entryMap["url"].(string)
spec.Query = stringMapFromPropValue(entryMap["query"])
return spec
default:
return nil
}
}
// ActionSpec returns the server-side spec for one action id from the cookie actions map.
func (m *MmBlocksActionCookie) ActionSpec(actionID string) *MmBlocksActionSpec {
if m == nil || actionID == "" || m.Actions == nil {
return nil
}
entry, ok := m.Actions[actionID]
if !ok || entry == nil {
return nil
}
entryMap, ok := coerceToStringAnyMap(entry)
if !ok {
return nil
}
return mmBlocksEntryMapToSpec(entryMap)
}
// ResolveMmBlocksAction resolves spec for execution: openURL returns OpenURLGoto; external returns ExternalURL and Context.
func ResolveMmBlocksAction(spec *MmBlocksActionSpec, actionID string, clientQuery map[string]string) (*MmBlocksActionResolved, error) {
if spec == nil {
return nil, fmt.Errorf("mm_blocks action_id=%s: %w", actionID, ErrMmBlocksActionNotFound)
}
switch spec.Type {
case MmBlocksActionTypeOpenURL:
if spec.URL == "" {
return nil, fmt.Errorf("mm_blocks action_id=%s: %w", actionID, ErrMmBlocksActionNotFound)
}
gotoURL, err := MergeQueryIntoURL(spec.URL, spec.Query)
if err != nil {
return nil, err
}
return &MmBlocksActionResolved{OpenURLGoto: gotoURL}, nil
case MmBlocksActionTypeExternal:
if spec.URL == "" {
return nil, fmt.Errorf("mm_blocks action_id=%s: %w", actionID, ErrMmBlocksActionNotFound)
}
upstreamURL, err := MergeQueryIntoURL(spec.URL, spec.Query)
if err != nil {
return nil, err
}
upstreamURL, err = MergeQueryIntoURL(upstreamURL, clientQuery)
if err != nil {
return nil, err
}
return &MmBlocksActionResolved{ExternalURL: upstreamURL, Context: spec.Context}, nil
default:
return nil, fmt.Errorf("mm_blocks action_id=%s: %w", actionID, ErrMmBlocksActionNotFound)
}
}
// MmBlocksContextMap parses a context JSON string or treats a non-JSON string as a single context value.
func MmBlocksContextMap(contextString string) map[string]any {
if contextString == "" {
return nil
}
var m map[string]any
if err := json.Unmarshal([]byte(contextString), &m); err == nil && m != nil {
return m
}
return map[string]any{"context": contextString}
}
// MergeQueryIntoURL merges q into rawURL's query string; existing keys are overwritten by q.
func MergeQueryIntoURL(rawURL string, q map[string]string) (string, error) {
if len(q) == 0 {
return rawURL, nil
}
u, err := url.Parse(rawURL)
if err != nil {
return "", fmt.Errorf("parse url: %w", err)
}
values := u.Query()
for k, v := range q {
values.Set(k, v)
}
u.RawQuery = values.Encode()
return u.String(), nil
}
// ParseDecryptedActionCookiePayload unmarshals decrypted cookie JSON from the client.
// Exactly one of legacy or mmBlocks is non-nil when err is nil (legacy attachment cookie vs mm_blocks cookie).
func ParseDecryptedActionCookiePayload(decrypted string) (legacy *PostActionCookie, mmBlocks *MmBlocksActionCookie, err error) {
var probe struct {
Kind string `json:"kind"`
}
if err := json.Unmarshal([]byte(decrypted), &probe); err != nil {
return nil, nil, err
}
if probe.Kind == MmBlocksActionCookieKind {
var mm MmBlocksActionCookie
if err := json.Unmarshal([]byte(decrypted), &mm); err != nil {
return nil, nil, err
}
return nil, &mm, nil
}
var c PostActionCookie
if err := json.Unmarshal([]byte(decrypted), &c); err != nil {
return nil, nil, err
}
return &c, nil, nil
}
// AddMmBlocksActionCookies encrypts the full mm_blocks_actions map into one cookie string stored in PostPropsMmBlocksActions.
func AddMmBlocksActionCookies(p *Post, secret []byte) {
raw := p.GetProp(PostPropsMmBlocksActions)
if raw == nil {
return
}
actionsTop, ok := coerceToStringAnyMap(raw)
if !ok || len(actionsTop) == 0 {
return
}
retainProps := map[string]any{}
removeProps := []string{}
for _, key := range PostActionRetainPropKeys {
value, ok := p.GetProps()[key]
if ok {
retainProps[key] = value
} else {
removeProps = append(removeProps, key)
}
}
actionsForEnc := make(map[string]map[string]any, len(actionsTop))
for actionID, val := range actionsTop {
entryMap, ok := coerceToStringAnyMap(val)
if !ok {
continue
}
actionsForEnc[actionID] = entryMap
}
rootPostID := p.Id
if p.RootId != "" {
rootPostID = p.RootId
}
mmCookie := MmBlocksActionCookie{
Kind: MmBlocksActionCookieKind,
PostId: p.Id,
RootPostId: rootPostID,
ChannelId: p.ChannelId,
RetainProps: retainProps,
RemoveProps: removeProps,
Actions: actionsForEnc,
}
b, err := json.Marshal(mmCookie)
if err != nil {
return
}
enc, err := EncryptPostActionCookie(string(b), secret)
if err != nil {
return
}
p.AddProp(PostPropsMmBlocksActions, enc)
}
// StripMmBlocksActionSecrets removes server-only fields from props.mm_blocks_actions for wire serialization.
func (o *Post) StripMmBlocksActionSecrets() {
raw := o.GetProp(PostPropsMmBlocksActions)
if raw == nil {
return
}
if _, ok := raw.(string); ok {
// Already replaced with a single opaque encrypted cookie by AddMmBlocksActionCookies.
return
}
o.DelProp(PostPropsMmBlocksActions)
}
// contextMapFromProp normalizes props.mm_blocks_actions[*].context to map[string]any (JSON object or string).
func contextMapFromProp(v any) map[string]any {
if v == nil {
return nil
}
if s, ok := v.(string); ok {
return MmBlocksContextMap(s)
}
if m, ok := coerceToStringAnyMap(v); ok {
// Clone so callers cannot mutate the live post.Props map. A
// nested mutation through the returned map would otherwise race
// with concurrent post.Props readers.
return maps.Clone(m)
}
return nil
}
func stringMapFromPropValue(v any) map[string]string {
m, ok := coerceToStringAnyMap(v)
if !ok || len(m) == 0 {
return nil
}
out := make(map[string]string, len(m))
for k, val := range m {
if s, ok := val.(string); ok {
out[k] = s
}
}
if len(out) == 0 {
return nil
}
return out
}
func coerceToStringAnyMap(v any) (map[string]any, bool) {
if v == nil {
return nil, false
}
m, ok := v.(map[string]any)
if ok {
return m, true
}
return nil, false
}