From 41612c6bc6a8a6424e27e6a66fb0dae30524c768 Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 31 May 2026 20:53:42 +0100 Subject: [PATCH 1/3] chore: disable dependabot - its just noise --- .github/dependabot.yml | 132 ----------------------------------------- 1 file changed, 132 deletions(-) delete mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index e2d773a..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,132 +0,0 @@ -version: 2 -updates: - # npm updates for frontend - targeting "next" branch - - package-ecosystem: "npm" - directory: "/frontend" - schedule: - interval: "weekly" - target-branch: "next" - open-pull-requests-limit: 10 - labels: - - "3k" - - "dependencies" - cooldown: - default-days: 7 - - # npm updates for frontend - targeting "release/2k" branch (security updates only) - - package-ecosystem: "npm" - directory: "/frontend" - schedule: - interval: "weekly" - target-branch: "release/2k" - open-pull-requests-limit: 0 - labels: - - "2k" - - "dependencies" - cooldown: - default-days: 7 - - # npm updates for integration-tests - targeting "next" branch - - package-ecosystem: "npm" - directory: "/integration-tests" - schedule: - interval: "weekly" - target-branch: "next" - open-pull-requests-limit: 10 - labels: - - "3k" - - "dependencies" - cooldown: - default-days: 7 - - # npm updates for integration-tests - targeting "release/2k" branch (security updates only) - - package-ecosystem: "npm" - directory: "/integration-tests" - schedule: - interval: "weekly" - target-branch: "release/2k" - open-pull-requests-limit: 0 - labels: - - "2k" - - "dependencies" - cooldown: - default-days: 7 - - # Go modules updates for service - targeting "next" branch - - package-ecosystem: "gomod" - directory: "/service" - schedule: - interval: "weekly" - target-branch: "next" - open-pull-requests-limit: 10 - labels: - - "3k" - - "dependencies" - cooldown: - default-days: 7 - - # Go modules updates for service - targeting "release/2k" branch (security updates only) - - package-ecosystem: "gomod" - directory: "/service" - schedule: - interval: "weekly" - target-branch: "release/2k" - open-pull-requests-limit: 0 - labels: - - "2k" - - "dependencies" - cooldown: - default-days: 7 - - # Go modules updates for lang - targeting "next" branch - - package-ecosystem: "gomod" - directory: "/lang" - schedule: - interval: "weekly" - target-branch: "next" - open-pull-requests-limit: 10 - labels: - - "3k" - - "dependencies" - cooldown: - default-days: 7 - - # Go modules updates for lang - targeting "release/2k" branch (security updates only) - - package-ecosystem: "gomod" - directory: "/lang" - schedule: - interval: "weekly" - target-branch: "release/2k" - open-pull-requests-limit: 0 - labels: - - "2k" - - "dependencies" - cooldown: - default-days: 7 - - # Docker updates - targeting "next" branch - - package-ecosystem: "docker" - directory: "/" - schedule: - interval: "weekly" - target-branch: "next" - open-pull-requests-limit: 10 - labels: - - "3k" - - "dependencies" - cooldown: - default-days: 7 - - # Docker updates - targeting "release/2k" branch (security updates only) - - package-ecosystem: "docker" - directory: "/" - schedule: - interval: "weekly" - target-branch: "release/2k" - open-pull-requests-limit: 0 - labels: - - "2k" - - "dependencies" - cooldown: - default-days: 7 - From ebffd9f040f791208aee1db2e5a8aecd1e3e603d Mon Sep 17 00:00:00 2001 From: jamesread Date: Sun, 31 May 2026 22:41:16 +0100 Subject: [PATCH 2/3] security: GHSA-prj9-97mp-mwh2 (HIGH) Treat all ot_ system arguments as reserved, preventing RCE Ensure OliveTin-owned arguments are injected only after filtering and validate system values before command execution. --- docs/modules/ROOT/pages/args/types.adoc | 1 + frontend/resources/vue/views/ArgumentForm.vue | 2 +- service/internal/config/config.go | 3 + service/internal/config/sanitize.go | 28 +++ service/internal/config/sanitize_test.go | 53 +++++ service/internal/executor/arguments.go | 1 + service/internal/executor/arguments_test.go | 32 +++ service/internal/executor/executor.go | 102 ++++++--- service/internal/executor/executor_test.go | 210 +++++++++++++++++- 9 files changed, 402 insertions(+), 30 deletions(-) diff --git a/docs/modules/ROOT/pages/args/types.adoc b/docs/modules/ROOT/pages/args/types.adoc index fa10b99..ffdfed0 100644 --- a/docs/modules/ROOT/pages/args/types.adoc +++ b/docs/modules/ROOT/pages/args/types.adoc @@ -10,6 +10,7 @@ A full list of argument types are below; | (default) | xref:args/input.adoc[Textbox] | If a `type:` is not set, and `choices:` is empty, then ascii will be used, and a warning will be logged. It is recommended that you set the type explicitly, rather than relying on defaults. | ascii | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, but no spaces or punctuation | ascii_identifier | xref:args/input.adoc[Textbox] | Like a DNS name, a-Z (case insensitive), 0-9, `-`, `.`, and `_`. +| shell_safe_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but also allows `@` and `+`. Useful for shell-safe usernames and email-style identifiers. | ascii_sentence | xref:args/input.adoc[Textbox] | a-z (case insensitive), 0-9, with spaces, `.` and `,`. | unicode_identifier | xref:args/input.adoc[Textbox] | Like an ascii identifier, but allows unicode characters. This is useful for languages that use non-ascii characters, such as Chinese, Japanese, etc. | email | xref:args/input.adoc[Textbox] | An email address. diff --git a/frontend/resources/vue/views/ArgumentForm.vue b/frontend/resources/vue/views/ArgumentForm.vue index 0ba61ce..925c052 100644 --- a/frontend/resources/vue/views/ArgumentForm.vue +++ b/frontend/resources/vue/views/ArgumentForm.vue @@ -174,7 +174,7 @@ function getInputType(arg) { return 'checkbox' } - if (arg.type === 'ascii_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') { + if (arg.type === 'ascii_identifier' || arg.type === 'shell_safe_identifier' || arg.type === 'ascii' || arg.type === 'ascii_sentence') { return 'text' } diff --git a/service/internal/config/config.go b/service/internal/config/config.go index 9bc6077..b118afa 100644 --- a/service/internal/config/config.go +++ b/service/internal/config/config.go @@ -4,6 +4,9 @@ import ( "fmt" ) +// ReservedArgumentNamePrefix is reserved for OliveTin-injected system arguments. +const ReservedArgumentNamePrefix = "ot_" + // Action represents the core functionality of OliveTin - commands that show up // as buttons in the UI. type Action struct { diff --git a/service/internal/config/sanitize.go b/service/internal/config/sanitize.go index 674fba5..d22cc90 100644 --- a/service/internal/config/sanitize.go +++ b/service/internal/config/sanitize.go @@ -26,6 +26,34 @@ func (cfg *Config) Sanitize() { } cfg.sanitizeDashboardsForInlineActions() + + if err := cfg.validateReservedActionArgumentNames(); err != nil { + log.Fatalf("%v", err) + } +} + +func (cfg *Config) validateReservedActionArgumentNames() error { + for _, action := range cfg.Actions { + if err := action.validateReservedArgumentNames(); err != nil { + return err + } + } + + return nil +} + +func (action *Action) validateReservedArgumentNames() error { + if action == nil { + return nil + } + + for _, arg := range action.Arguments { + if strings.HasPrefix(arg.Name, ReservedArgumentNamePrefix) { + return fmt.Errorf("action %q argument %q uses reserved prefix %q", action.Title, arg.Name, ReservedArgumentNamePrefix) + } + } + + return nil } func (cfg *Config) sanitizeDashboardsForInlineActions() { diff --git a/service/internal/config/sanitize_test.go b/service/internal/config/sanitize_test.go index adba106..036d710 100644 --- a/service/internal/config/sanitize_test.go +++ b/service/internal/config/sanitize_test.go @@ -92,6 +92,59 @@ func TestSanitizeConfigInlineDashboardActions(t *testing.T) { } } +func TestValidateReservedActionArgumentNames(t *testing.T) { + c := DefaultConfig() + c.Actions = append(c.Actions, &Action{ + Title: "Reserved arg", + Arguments: []ActionArgument{ + {Name: "ot_custom", Type: "ascii"}, + }, + }) + + err := c.validateReservedActionArgumentNames() + + require.Error(t, err) + assert.Contains(t, err.Error(), `action "Reserved arg" argument "ot_custom" uses reserved prefix "ot_"`) +} + +func TestValidateReservedActionArgumentNamesAllowsNonReserved(t *testing.T) { + c := DefaultConfig() + c.Actions = append(c.Actions, &Action{ + Title: "Allowed arg", + Arguments: []ActionArgument{ + {Name: "target", Type: "ascii"}, + }, + }) + + require.NoError(t, c.validateReservedActionArgumentNames()) +} + +func TestValidateReservedActionArgumentNamesChecksInlineActions(t *testing.T) { + c := DefaultConfig() + c.Dashboards = []*DashboardComponent{ + { + Title: "Dashboard", + Contents: []*DashboardComponent{ + { + Title: "Inline reserved arg", + InlineAction: &Action{ + Shell: "echo test", + Arguments: []ActionArgument{ + {Name: "ot_custom", Type: "ascii"}, + }, + }, + }, + }, + }, + } + + c.sanitizeDashboardsForInlineActions() + err := c.validateReservedActionArgumentNames() + + require.Error(t, err) + assert.Contains(t, err.Error(), `action "Inline reserved arg" argument "ot_custom" uses reserved prefix "ot_"`) +} + func TestValidateUniqueLocalUserAPIKeys(t *testing.T) { t.Parallel() diff --git a/service/internal/executor/arguments.go b/service/internal/executor/arguments.go index a73a0d7..e499134 100644 --- a/service/internal/executor/arguments.go +++ b/service/internal/executor/arguments.go @@ -21,6 +21,7 @@ var ( "unicode_identifier": `^[\w\-\.\_\d]+$`, "ascii": `^[a-zA-Z0-9]+$`, "ascii_identifier": `^[a-zA-Z0-9\-\._]+$`, + "shell_safe_identifier": `^[a-zA-Z0-9@\.\_\+\-]+$`, "ascii_sentence": `^[a-zA-Z0-9\-\._, ]+$`, } ) diff --git a/service/internal/executor/arguments_test.go b/service/internal/executor/arguments_test.go index 8cf03a9..7d409f1 100644 --- a/service/internal/executor/arguments_test.go +++ b/service/internal/executor/arguments_test.go @@ -576,6 +576,38 @@ func TestTypeSafetyCheckAsciiIdentifier(t *testing.T) { } } +func TestTypeSafetyCheckShellSafeIdentifier(t *testing.T) { + tests := []struct { + name string + value string + hasError bool + }{ + {"Simple username", "alice123", false}, + {"Email username", "alice@example.com", false}, + {"Plus addressing", "alice+test@example.com", false}, + {"Hyphen underscore dot", "alice-test_user.example", false}, + {"Invalid space", "alice example", true}, + {"Invalid shell substitution", "$(whoami)", true}, + {"Invalid backtick", "`whoami`", true}, + {"Invalid semicolon", "alice;id", true}, + {"Invalid ampersand", "alice&id", true}, + {"Invalid pipe", "alice|id", true}, + {"Invalid quote", "alice'example", true}, + {"Invalid slash", "alice/example", true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := TypeSafetyCheck("username", tt.value, "shell_safe_identifier") + if tt.hasError { + assert.NotNil(t, err, "Expected error for value '%s'", tt.value) + } else { + assert.Nil(t, err, "Expected no error for value '%s', but got: %v", tt.value, err) + } + }) + } +} + func TestTypeSafetyCheckAsciiSentence(t *testing.T) { tests := []struct { name string diff --git a/service/internal/executor/executor.go b/service/internal/executor/executor.go index 670aeea..fbcaa68 100644 --- a/service/internal/executor/executor.go +++ b/service/internal/executor/executor.go @@ -666,13 +666,15 @@ func stepACLCheck(req *ExecutionRequest) bool { func stepParseArgs(req *ExecutionRequest) bool { ensureArgumentMap(req) - injectSystemArgs(req) if !hasBindingAndAction(req) { return fail(req, fmt.Errorf("cannot parse arguments: Binding or Action is nil")) } filterToDefinedArgumentsOnly(req) + if err := injectSystemArgs(req); err != nil { + return fail(req, err) + } mangleInvalidArgumentValues(req) if hasExec(req) { @@ -735,7 +737,7 @@ func filterToDefinedArgumentsOnly(req *ExecutionRequest) { func keepArgument(name string, definedNames map[string]struct{}) bool { _, ok := definedNames[name] - return ok || strings.HasPrefix(name, "ot_") + return ok } func hasWebhookTag(req *ExecutionRequest) bool { @@ -747,9 +749,38 @@ func hasWebhookTag(req *ExecutionRequest) bool { return false } -func injectSystemArgs(req *ExecutionRequest) { - req.Arguments["ot_executionTrackingId"] = req.TrackingID - req.Arguments["ot_username"] = req.AuthenticatedUser.Username +var systemArgumentDefinitions = []config.ActionArgument{ + {Name: "ot_executionTrackingId", Type: "ascii_identifier", RejectNull: true}, + {Name: "ot_username", Type: "shell_safe_identifier", RejectNull: true}, +} + +func injectSystemArgs(req *ExecutionRequest) error { + args, err := validatedSystemArgs(req) + if err != nil { + return err + } + + for name, value := range args { + req.Arguments[name] = value + } + + return nil +} + +func validatedSystemArgs(req *ExecutionRequest) (map[string]string, error) { + values := map[string]string{ + "ot_executionTrackingId": req.TrackingID, + "ot_username": req.AuthenticatedUser.Username, + } + + for i := range systemArgumentDefinitions { + arg := &systemArgumentDefinitions[i] + if err := ValidateArgument(arg, values[arg.Name], req.Binding.Action); err != nil { + return nil, fmt.Errorf("system argument %q failed validation: %w", arg.Name, err) + } + } + + return values, nil } func hasBindingAndAction(req *ExecutionRequest) bool { @@ -939,36 +970,20 @@ func prepareCommand(cmd *exec.Cmd, streamer *OutputStreamer, req *ExecutionReque } func stepExecAfter(req *ExecutionRequest) bool { - if req.Binding.Action.ShellAfterCompleted == "" { - return true - } - ctx, cancel := newTimeoutContext(context.Background(), time.Duration(req.Binding.Action.Timeout)*time.Second, req.executor) defer cancel() var stdout bytes.Buffer var stderr bytes.Buffer - args := map[string]string{ - "output": req.logEntry.Output, - "exitCode": fmt.Sprintf("%v", req.logEntry.ExitCode), - "ot_executionTrackingId": req.TrackingID, - "ot_username": req.AuthenticatedUser.Username, - } - - finalParsedCommand, err := tpl.ParseTemplateWithActionContext(req.Binding.Action.ShellAfterCompleted, req.Binding.Entity, args) - + cmd, args, err := buildShellAfterCommand(ctx, req, &stdout, &stderr) if err != nil { - msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n" - req.logEntry.Output += msg - log.Warn(msg) + return fail(req, err) + } + if cmd == nil { return true } - cmd := wrapCommandInShell(ctx, finalParsedCommand) - cmd.Stdout = &stdout - cmd.Stderr = &stderr - cmd.Env = buildEnv(args) runerr := cmd.Start() @@ -998,6 +1013,43 @@ func stepExecAfter(req *ExecutionRequest) bool { return true } +func buildShellAfterCommand(ctx context.Context, req *ExecutionRequest, stdout, stderr *bytes.Buffer) (*exec.Cmd, map[string]string, error) { + if req.Binding.Action.ShellAfterCompleted == "" { + return nil, nil, nil + } + + args, err := buildShellAfterArgs(req) + if err != nil { + return nil, nil, err + } + + finalParsedCommand, err := tpl.ParseTemplateWithActionContext(req.Binding.Action.ShellAfterCompleted, req.Binding.Entity, args) + if err != nil { + msg := "Could not prepare shellAfterCompleted command: " + err.Error() + "\n" + req.logEntry.Output += msg + log.Warn(msg) + return nil, nil, nil + } + + cmd := wrapCommandInShell(ctx, finalParsedCommand) + cmd.Stdout = stdout + cmd.Stderr = stderr + + return cmd, args, nil +} + +func buildShellAfterArgs(req *ExecutionRequest) (map[string]string, error) { + args, err := validatedSystemArgs(req) + if err != nil { + return nil, err + } + + args["output"] = req.logEntry.Output + args["exitCode"] = fmt.Sprintf("%v", req.logEntry.ExitCode) + + return args, nil +} + //gocyclo:ignore func stepTrigger(req *ExecutionRequest) bool { if req.Binding.Action.Triggers == nil { diff --git a/service/internal/executor/executor_test.go b/service/internal/executor/executor_test.go index a36fc10..d79f014 100644 --- a/service/internal/executor/executor_test.go +++ b/service/internal/executor/executor_test.go @@ -1,6 +1,7 @@ package executor import ( + "strings" "testing" "time" @@ -37,7 +38,7 @@ func TestCreateExecutorAndExec(t *testing.T) { e, cfg := testingExecutor() req := ExecutionRequest{ - AuthenticatedUser: &authpublic.AuthenticatedUser{Username: "Mr Tickle"}, + AuthenticatedUser: &authpublic.AuthenticatedUser{Username: "MrTickle"}, Cfg: cfg, Arguments: map[string]string{ "person": "yourself", @@ -379,7 +380,7 @@ func TestFilterToDefinedArgumentsOnly(t *testing.T) { assert.Empty(t, req.Arguments["extra_undefined"]) } -func TestFilterToDefinedArgumentsPreservesSystemArgs(t *testing.T) { +func TestFilterToDefinedArgumentsDropsReservedPrefixArgs(t *testing.T) { req := newExecRequest() req.Binding.Action = &config.Action{ Title: "Filter test", @@ -393,8 +394,209 @@ func TestFilterToDefinedArgumentsPreservesSystemArgs(t *testing.T) { filterToDefinedArgumentsOnly(req) - assert.Equal(t, "track-123", req.Arguments["ot_executionTrackingId"]) - assert.Equal(t, "webhook", req.Arguments["ot_username"]) + assert.Empty(t, req.Arguments["ot_executionTrackingId"]) + assert.Empty(t, req.Arguments["ot_username"]) +} + +func TestStepParseArgsInjectsSystemArgsAfterFiltering(t *testing.T) { + req := newExecRequest() + req.TrackingID = "server-track-456" + req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice"} + req.Binding.Action = &config.Action{ + Title: "Filter then inject", + Shell: "echo test", + Arguments: []config.ActionArgument{ + {Name: "name", Type: "ascii"}, + }, + } + req.Arguments = map[string]string{ + "name": "Alice", + "ot_executionTrackingId": "attacker-track", + "ot_username": "mallory", + "ot_custom": "polluted", + } + + assert.True(t, stepParseArgs(req)) + assert.Equal(t, "Alice", req.Arguments["name"]) + assert.Equal(t, "server-track-456", req.Arguments["ot_executionTrackingId"]) + assert.Equal(t, "alice", req.Arguments["ot_username"]) + assert.Empty(t, req.Arguments["ot_custom"]) +} + +func TestStepParseArgsDropsReservedPrefixArgsFromEnvironment(t *testing.T) { + req := newExecRequest() + req.TrackingID = "server-track-456" + req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"} + req.Binding.Action = &config.Action{ + Title: "No reserved prefix pollution", + Shell: "echo test", + Arguments: []config.ActionArgument{}, + } + req.Arguments = map[string]string{ + "ot_custom": "polluted", + } + + assert.True(t, stepParseArgs(req)) + env := buildEnv(req.Arguments) + + assert.False(t, containsEnvPrefix(env, "OT_CUSTOM=")) + assert.True(t, containsEnvPrefix(env, "OT_USERNAME=alice@example.com")) + assert.True(t, containsEnvPrefix(env, "OT_EXECUTIONTRACKINGID=server-track-456")) +} + +func TestSystemArgumentDefinitionsAreReservedAndShellSafe(t *testing.T) { + unsafeTypes := map[string]struct{}{ + "email": {}, + "password": {}, + "raw_string_multiline": {}, + "url": {}, + "very_dangerous_raw_string": {}, + } + seen := map[string]struct{}{} + + for _, arg := range systemArgumentDefinitions { + assert.True(t, strings.HasPrefix(arg.Name, config.ReservedArgumentNamePrefix)) + assert.NotEmpty(t, arg.Type) + assert.True(t, arg.RejectNull) + + _, duplicate := seen[arg.Name] + assert.False(t, duplicate, "duplicate system argument definition %q", arg.Name) + seen[arg.Name] = struct{}{} + + _, unsafe := unsafeTypes[arg.Type] + assert.False(t, unsafe, "system argument %q uses unsafe type %q", arg.Name, arg.Type) + } +} + +func TestValidatedSystemArgsMatchesSystemArgumentDefinitions(t *testing.T) { + req := newExecRequest() + req.TrackingID = "server-track-456" + req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"} + + args, err := validatedSystemArgs(req) + + assert.Nil(t, err) + assert.Len(t, args, len(systemArgumentDefinitions)) + for _, arg := range systemArgumentDefinitions { + assert.Contains(t, args, arg.Name) + } +} + +func TestBuildShellAfterArgsOnlyAddsExpectedNonSystemArgs(t *testing.T) { + req := newExecRequest() + req.logEntry = &InternalLogEntry{ + Output: "hello", + ExitCode: 7, + } + req.TrackingID = "server-track-456" + req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"} + req.Binding.Action = &config.Action{ShellAfterCompleted: "echo test"} + + args, err := buildShellAfterArgs(req) + + assert.Nil(t, err) + assert.Len(t, args, len(systemArgumentDefinitions)+2) + assert.Contains(t, args, "output") + assert.Contains(t, args, "exitCode") + for _, arg := range systemArgumentDefinitions { + assert.Contains(t, args, arg.Name) + } +} + +func TestStepParseArgsAllowsEmailUsernameSystemArg(t *testing.T) { + req := newExecRequest() + req.logEntry = &InternalLogEntry{} + req.TrackingID = "server-track-456" + req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"} + req.Binding.Action = &config.Action{ + Title: "Email username", + Shell: "echo test", + Arguments: []config.ActionArgument{}, + } + + assert.True(t, stepParseArgs(req)) + assert.Equal(t, "alice@example.com", req.Arguments["ot_username"]) +} + +func TestStepParseArgsFailsWhenUsernameSystemArgIsInvalid(t *testing.T) { + req := newExecRequest() + req.logEntry = &InternalLogEntry{} + req.TrackingID = "server-track-456" + req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice;id"} + req.Binding.Action = &config.Action{ + Title: "Invalid system arg", + Shell: "echo test", + Arguments: []config.ActionArgument{}, + } + + assert.False(t, stepParseArgs(req)) + assert.Contains(t, req.logEntry.Output, `system argument "ot_username" failed validation`) + assert.Empty(t, req.Arguments["ot_username"]) +} + +func TestStepParseArgsFailsWhenTrackingIDSystemArgIsInvalid(t *testing.T) { + req := newExecRequest() + req.logEntry = &InternalLogEntry{} + req.TrackingID = "track/../../bad" + req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice"} + req.Binding.Action = &config.Action{ + Title: "Invalid tracking ID", + Shell: "echo test", + Arguments: []config.ActionArgument{}, + } + + assert.False(t, stepParseArgs(req)) + assert.Contains(t, req.logEntry.Output, `system argument "ot_executionTrackingId" failed validation`) + assert.Empty(t, req.Arguments["ot_executionTrackingId"]) +} + +func TestBuildShellAfterArgsUsesValidatedSystemArgs(t *testing.T) { + req := newExecRequest() + req.logEntry = &InternalLogEntry{ + Output: "hello", + ExitCode: 7, + } + req.TrackingID = "server-track-456" + req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice@example.com"} + req.Binding.Action = &config.Action{ + Title: "Shell after", + ShellAfterCompleted: "echo test", + } + + args, err := buildShellAfterArgs(req) + + assert.Nil(t, err) + assert.Equal(t, "alice@example.com", args["ot_username"]) + assert.Equal(t, "server-track-456", args["ot_executionTrackingId"]) + assert.Equal(t, "hello", args["output"]) + assert.Equal(t, "7", args["exitCode"]) +} + +func TestBuildShellAfterArgsFailsWhenSystemArgIsInvalid(t *testing.T) { + req := newExecRequest() + req.logEntry = &InternalLogEntry{} + req.TrackingID = "server-track-456" + req.AuthenticatedUser = &authpublic.AuthenticatedUser{Username: "alice;id"} + req.Binding.Action = &config.Action{ + Title: "Shell after invalid username", + ShellAfterCompleted: "echo test", + } + + args, err := buildShellAfterArgs(req) + + assert.Nil(t, args) + assert.NotNil(t, err) + assert.Contains(t, err.Error(), `system argument "ot_username" failed validation`) +} + +func containsEnvPrefix(env []string, prefix string) bool { + for _, item := range env { + if strings.HasPrefix(item, prefix) { + return true + } + } + + return false } func TestTriggerExecutesTriggeredAction(t *testing.T) { From 9ac6acefd063f3c88f4ac7dfa308a89f3d25d089 Mon Sep 17 00:00:00 2001 From: jamesread Date: Mon, 1 Jun 2026 00:29:48 +0100 Subject: [PATCH 3/3] fix: emoji names regression, and webhook execution helper info --- frontend/package.json | 2 +- .../gen/olivetin/api/v1/olivetin_pb.d.ts | 10 + .../gen/olivetin/api/v1/olivetin_pb.js | 2 +- .../vue/components/ActionIconGlyph.vue | 14 +- .../vue/components/actionIconGlyphHelpers.mjs | 38 ++++ .../actionIconGlyphHelpers.test.mjs | 20 ++ .../vue/views/ActionExecConditionsView.vue | 12 +- proto/olivetin/api/v1/olivetin.proto | 2 + service/gen/olivetin/api/v1/olivetin.pb.go | 215 ++++++++++-------- service/internal/api/apiActionExecTriggers.go | 6 +- service/internal/api/api_test.go | 18 ++ 11 files changed, 231 insertions(+), 108 deletions(-) create mode 100644 frontend/resources/vue/components/actionIconGlyphHelpers.mjs create mode 100644 frontend/resources/vue/components/actionIconGlyphHelpers.test.mjs diff --git a/frontend/package.json b/frontend/package.json index 42acda3..f3e6072 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -10,7 +10,7 @@ "stylelint-config-standard": "^40.0.0" }, "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" + "test": "node --test resources/vue/components/*.test.mjs" }, "author": "", "parcelIgnore": [ diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts index 09ffe91..752456e 100644 --- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts +++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.d.ts @@ -111,6 +111,16 @@ export declare type ActionWebhookExecHint = Message<"olivetin.api.v1.ActionWebho * @generated from field: string match_path = 2; */ matchPath: string; + + /** + * @generated from field: map match_headers = 3; + */ + matchHeaders: { [key: string]: string }; + + /** + * @generated from field: map match_query = 4; + */ + matchQuery: { [key: string]: string }; }; /** diff --git a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js index 9a41c2f..2ded9fe 100644 --- a/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js +++ b/frontend/resources/scripts/gen/olivetin/api/v1/olivetin_pb.js @@ -8,7 +8,7 @@ import { fileDesc, messageDesc, serviceDesc } from "@bufbuild/protobuf/codegenv2 * Describes the file olivetin/api/v1/olivetin.proto. */ export const file_olivetin_api_v1_olivetin = /*@__PURE__*/ - fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSK2AwoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludCI9ChVBY3Rpb25XZWJob29rRXhlY0hpbnQSEAoIdGVtcGxhdGUYASABKAkSEgoKbWF0Y2hfcGF0aBgCIAEoCSK7AgoOQWN0aW9uQXJndW1lbnQSDAoEbmFtZRgBIAEoCRINCgV0aXRsZRgCIAEoCRIMCgR0eXBlGAMgASgJEhUKDWRlZmF1bHRfdmFsdWUYBCABKAkSNgoHY2hvaWNlcxgFIAMoCzIlLm9saXZldGluLmFwaS52MS5BY3Rpb25Bcmd1bWVudENob2ljZRITCgtkZXNjcmlwdGlvbhgGIAEoCRJFCgtzdWdnZXN0aW9ucxgHIAMoCzIwLm9saXZldGluLmFwaS52MS5BY3Rpb25Bcmd1bWVudC5TdWdnZXN0aW9uc0VudHJ5Eh8KF3N1Z2dlc3Rpb25zX2Jyb3dzZXJfa2V5GAggASgJGjIKEFN1Z2dlc3Rpb25zRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI0ChRBY3Rpb25Bcmd1bWVudENob2ljZRINCgV2YWx1ZRgBIAEoCRINCgV0aXRsZRgCIAEoCSKyAQoGRW50aXR5Eg0KBXRpdGxlGAEgASgJEhIKCnVuaXF1ZV9rZXkYAiABKAkSDAoEdHlwZRgDIAEoCRITCgtkaXJlY3RvcmllcxgEIAMoCRIzCgZmaWVsZHMYBSADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5LkZpZWxkc0VudHJ5Gi0KC0ZpZWxkc0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiVAoUR2V0RGFzaGJvYXJkUmVzcG9uc2USDQoFdGl0bGUYASABKAkSLQoJZGFzaGJvYXJkGAQgASgLMhoub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZCJfCg9FZmZlY3RpdmVQb2xpY3kSGAoQc2hvd19kaWFnbm9zdGljcxgBIAEoCBIVCg1zaG93X2xvZ19saXN0GAIgASgIEhsKE3Nob3dfdmVyc2lvbl9udW1iZXIYAyABKAgiTQoTR2V0RGFzaGJvYXJkUmVxdWVzdBINCgV0aXRsZRgBIAEoCRITCgtlbnRpdHlfdHlwZRgCIAEoCRISCgplbnRpdHlfa2V5GAMgASgJIlEKCURhc2hib2FyZBINCgV0aXRsZRgBIAEoCRI1Cghjb250ZW50cxgCIAMoCzIjLm9saXZldGluLmFwaS52MS5EYXNoYm9hcmRDb21wb25lbnQi2wEKEkRhc2hib2FyZENvbXBvbmVudBINCgV0aXRsZRgBIAEoCRIMCgR0eXBlGAIgASgJEjUKCGNvbnRlbnRzGAMgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudBIMCgRpY29uGAQgASgJEhEKCWNzc19jbGFzcxgFIAEoCRInCgZhY3Rpb24YBiABKAsyFy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uEhMKC2VudGl0eV90eXBlGAcgASgJEhIKCmVudGl0eV9rZXkYCCABKAkifQoSU3RhcnRBY3Rpb25SZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkSNwoJYXJndW1lbnRzGAIgAygLMiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQXJndW1lbnQSGgoSdW5pcXVlX3RyYWNraW5nX2lkGAMgASgJIjIKE1N0YXJ0QWN0aW9uQXJndW1lbnQSDAoEbmFtZRgBIAEoCRINCgV2YWx1ZRgCIAEoCSI0ChNTdGFydEFjdGlvblJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgCIAEoCSJnChlTdGFydEFjdGlvbkFuZFdhaXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudCJKChpTdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiLAoXU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIjkKGFN0YXJ0QWN0aW9uQnlHZXRSZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYAiABKAkiMwoeU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCSJPCh9TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSJOCg5HZXRMb2dzUmVxdWVzdBIUCgxzdGFydF9vZmZzZXQYASABKAMSEwoLZGF0ZV9maWx0ZXIYAiABKAkSEQoJcGFnZV9zaXplGAMgASgDIpoDCghMb2dFbnRyeRIYChBkYXRldGltZV9zdGFydGVkGAEgASgJEhQKDGFjdGlvbl90aXRsZRgCIAEoCRIOCgZvdXRwdXQYAyABKAkSEQoJdGltZWRfb3V0GAUgASgIEhEKCWV4aXRfY29kZRgGIAEoBRIMCgR1c2VyGAcgASgJEhIKCnVzZXJfY2xhc3MYCCABKAkSEwoLYWN0aW9uX2ljb24YCSABKAkSDAoEdGFncxgKIAMoCRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYCyABKAkSGQoRZGF0ZXRpbWVfZmluaXNoZWQYDCABKAkSGQoRZXhlY3V0aW9uX3N0YXJ0ZWQYDiABKAgSGgoSZXhlY3V0aW9uX2ZpbmlzaGVkGA8gASgIEg8KB2Jsb2NrZWQYECABKAgSFgoOZGF0ZXRpbWVfaW5kZXgYESABKAMSEAoIY2FuX2tpbGwYEiABKAgSIwobZGF0ZXRpbWVfcmF0ZV9saW1pdF9leHBpcmVzGBMgASgJEhIKCmJpbmRpbmdfaWQYFCABKAkikQEKD0dldExvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDIj8KFEdldEFjdGlvbkxvZ3NSZXF1ZXN0EhEKCWFjdGlvbl9pZBgBIAEoCRIUCgxzdGFydF9vZmZzZXQYAiABKAMilwEKFUdldEFjdGlvbkxvZ3NSZXNwb25zZRInCgRsb2dzGAEgAygLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5EhcKD2NvdW50X3JlbWFpbmluZxgCIAEoAxIRCglwYWdlX3NpemUYAyABKAMSEwoLdG90YWxfY291bnQYBCABKAMSFAoMc3RhcnRfb2Zmc2V0GAUgASgDImUKG1ZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBINCgV2YWx1ZRgBIAEoCRIMCgR0eXBlGAIgASgJEhIKCmJpbmRpbmdfaWQYAyABKAkSFQoNYXJndW1lbnRfbmFtZRgEIAEoCSJCChxWYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlEg0KBXZhbGlkGAEgASgIEhMKC2Rlc2NyaXB0aW9uGAIgASgJIjYKFVdhdGNoRXhlY3V0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkiJgoUV2F0Y2hFeGVjdXRpb25VcGRhdGUSDgoGdXBkYXRlGAEgASgJIkoKFkV4ZWN1dGlvblN0YXR1c1JlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEhEKCWFjdGlvbl9pZBgCIAEoCSJHChdFeGVjdXRpb25TdGF0dXNSZXNwb25zZRIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiDwoNV2hvQW1JUmVxdWVzdCJsCg5XaG9BbUlSZXNwb25zZRIaChJhdXRoZW50aWNhdGVkX3VzZXIYASABKAkSEQoJdXNlcmdyb3VwGAIgASgJEhAKCHByb3ZpZGVyGAMgASgJEgwKBGFjbHMYBCADKAkSCwoDc2lkGAUgASgJIhIKEFNvc1JlcG9ydFJlcXVlc3QiIgoRU29zUmVwb3J0UmVzcG9uc2USDQoFYWxlcnQYASABKAkiEQoPRHVtcFZhcnNSZXF1ZXN0IpUBChBEdW1wVmFyc1Jlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEkEKCGNvbnRlbnRzGAIgAygLMi8ub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVzcG9uc2UuQ29udGVudHNFbnRyeRovCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRINCgV2YWx1ZRgCIAEoCToCOAEiOwoMRGVidWdCaW5kaW5nEhQKDGFjdGlvbl90aXRsZRgBIAEoCRIVCg1lbnRpdHlfcHJlZml4GAIgASgJIh4KHER1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QizgEKHUR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlEg0KBWFsZXJ0GAEgASgJEk4KCGNvbnRlbnRzGAIgAygLMjwub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlLkNvbnRlbnRzRW50cnkaTgoNQ29udGVudHNFbnRyeRILCgNrZXkYASABKAkSLAoFdmFsdWUYAiABKAsyHS5vbGl2ZXRpbi5hcGkudjEuRGVidWdCaW5kaW5nOgI4ASISChBHZXRSZWFkeXpSZXF1ZXN0IiMKEUdldFJlYWR5elJlc3BvbnNlEg4KBnN0YXR1cxgBIAEoCSIUChJFdmVudFN0cmVhbVJlcXVlc3Qi4wIKE0V2ZW50U3RyZWFtUmVzcG9uc2USPQoOZW50aXR5X2NoYW5nZWQYAiABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFbnRpdHlDaGFuZ2VkSAASPQoOY29uZmlnX2NoYW5nZWQYAyABKAsyIy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRDb25maWdDaGFuZ2VkSAASRQoSZXhlY3V0aW9uX2ZpbmlzaGVkGAQgASgLMicub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uRmluaXNoZWRIABJDChFleGVjdXRpb25fc3RhcnRlZBgFIAEoCzImLm9saXZldGluLmFwaS52MS5FdmVudEV4ZWN1dGlvblN0YXJ0ZWRIABI5CgxvdXRwdXRfY2h1bmsYBiABKAsyIS5vbGl2ZXRpbi5hcGkudjEuRXZlbnRPdXRwdXRDaHVua0gAQgcKBWV2ZW50IkEKEEV2ZW50T3V0cHV0Q2h1bmsSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJEg4KBm91dHB1dBgCIAEoCSIUChJFdmVudEVudGl0eUNoYW5nZWQiFAoSRXZlbnRDb25maWdDaGFuZ2VkIkYKFkV2ZW50RXhlY3V0aW9uRmluaXNoZWQSLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5IkUKFUV2ZW50RXhlY3V0aW9uU3RhcnRlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiMgoRS2lsbEFjdGlvblJlcXVlc3QSHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAEgASgJIm0KEktpbGxBY3Rpb25SZXNwb25zZRIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGa2lsbGVkGAIgASgIEhkKEWFscmVhZHlfY29tcGxldGVkGAMgASgIEg0KBWZvdW5kGAQgASgIIjsKFUxvY2FsVXNlckxvZ2luUmVxdWVzdBIQCgh1c2VybmFtZRgBIAEoCRIQCghwYXNzd29yZBgCIAEoCSIpChZMb2NhbFVzZXJMb2dpblJlc3BvbnNlEg8KB3N1Y2Nlc3MYASABKAgiJwoTUGFzc3dvcmRIYXNoUmVxdWVzdBIQCghwYXNzd29yZBgBIAEoCSIkChRQYXNzd29yZEhhc2hSZXNwb25zZRIMCgRoYXNoGAEgASgJIg8KDUxvZ291dFJlcXVlc3QiEAoOTG9nb3V0UmVzcG9uc2UiFwoVR2V0RGlhZ25vc3RpY3NSZXF1ZXN0IkUKFkdldERpYWdub3N0aWNzUmVzcG9uc2USEwoLU3NoRm91bmRLZXkYASABKAkSFgoOU3NoRm91bmRDb25maWcYAiABKAkiDQoLSW5pdFJlcXVlc3Qi6wUKDEluaXRSZXNwb25zZRISCgpzaG93Rm9vdGVyGAEgASgIEhYKDnNob3dOYXZpZ2F0aW9uGAIgASgIEhcKD3Nob3dOZXdWZXJzaW9ucxgDIAEoCBIYChBhdmFpbGFibGVWZXJzaW9uGAQgASgJEhYKDmN1cnJlbnRWZXJzaW9uGAUgASgJEhEKCXBhZ2VUaXRsZRgGIAEoCRIeChZzZWN0aW9uTmF2aWdhdGlvblN0eWxlGAcgASgJEhoKEmRlZmF1bHRJY29uRm9yQmFjaxgIIAEoCRIWCg5lbmFibGVDdXN0b21KcxgJIAEoCBIUCgxhdXRoTG9naW5VcmwYCiABKAkSFgoOYXV0aExvY2FsTG9naW4YCyABKAgSEQoJc3R5bGVNb2RzGAwgAygJEjgKD29BdXRoMlByb3ZpZGVycxgNIAMoCzIfLm9saXZldGluLmFwaS52MS5PQXV0aDJQcm92aWRlchI4Cg9hZGRpdGlvbmFsTGlua3MYDiADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWRkaXRpb25hbExpbmsSFgoOcm9vdERhc2hib2FyZHMYDyADKAkSGgoSYXV0aGVudGljYXRlZF91c2VyGBAgASgJEiMKG2F1dGhlbnRpY2F0ZWRfdXNlcl9wcm92aWRlchgRIAEoCRI6ChBlZmZlY3RpdmVfcG9saWN5GBIgASgLMiAub2xpdmV0aW4uYXBpLnYxLkVmZmVjdGl2ZVBvbGljeRIWCg5iYW5uZXJfbWVzc2FnZRgTIAEoCRISCgpiYW5uZXJfY3NzGBQgASgJEhgKEHNob3dfZGlhZ25vc3RpY3MYFSABKAgSFQoNc2hvd19sb2dfbGlzdBgWIAEoCBIWCg5sb2dpbl9yZXF1aXJlZBgXIAEoCBIYChBhdmFpbGFibGVfdGhlbWVzGBggAygJEiQKHHNob3dfbmF2aWdhdGVfb25fc3RhcnRfaWNvbnMYGSABKAgiLAoOQWRkaXRpb25hbExpbmsSDQoFdGl0bGUYASABKAkSCwoDdXJsGAIgASgJIjoKDk9BdXRoMlByb3ZpZGVyEg0KBXRpdGxlGAEgASgJEgwKBGljb24YAyABKAkSCwoDa2V5GAQgASgJIi0KF0dldEFjdGlvbkJpbmRpbmdSZXF1ZXN0EhIKCmJpbmRpbmdfaWQYASABKAkiQwoYR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlEicKBmFjdGlvbhgBIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24iFAoSR2V0RW50aXRpZXNSZXF1ZXN0IlQKE0dldEVudGl0aWVzUmVzcG9uc2USPQoSZW50aXR5X2RlZmluaXRpb25zGAEgAygLMiEub2xpdmV0aW4uYXBpLnYxLkVudGl0eURlZmluaXRpb24iaQoQRW50aXR5RGVmaW5pdGlvbhINCgV0aXRsZRgBIAEoCRIqCglpbnN0YW5jZXMYAiADKAsyFy5vbGl2ZXRpbi5hcGkudjEuRW50aXR5EhoKEnVzZWRfb25fZGFzaGJvYXJkcxgDIAMoCSI0ChBHZXRFbnRpdHlSZXF1ZXN0EhIKCnVuaXF1ZV9rZXkYASABKAkSDAoEdHlwZRgCIAEoCSI1ChRSZXN0YXJ0QWN0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAky6BIKEk9saXZlVGluQXBpU2VydmljZRJdCgxHZXREYXNoYm9hcmQSJC5vbGl2ZXRpbi5hcGkudjEuR2V0RGFzaGJvYXJkUmVxdWVzdBolLm9saXZldGluLmFwaS52MS5HZXREYXNoYm9hcmRSZXNwb25zZSIAEloKC1N0YXJ0QWN0aW9uEiMub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlc3BvbnNlIgASbwoSU3RhcnRBY3Rpb25BbmRXYWl0Eioub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QaKy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25BbmRXYWl0UmVzcG9uc2UiABJpChBTdGFydEFjdGlvbkJ5R2V0Eigub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRSZXF1ZXN0Gikub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRSZXNwb25zZSIAEn4KF1N0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0Ei8ub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVxdWVzdBowLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlc3BvbnNlIgASXgoNUmVzdGFydEFjdGlvbhIlLm9saXZldGluLmFwaS52MS5SZXN0YXJ0QWN0aW9uUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvblJlc3BvbnNlIgASVwoKS2lsbEFjdGlvbhIiLm9saXZldGluLmFwaS52MS5LaWxsQWN0aW9uUmVxdWVzdBojLm9saXZldGluLmFwaS52MS5LaWxsQWN0aW9uUmVzcG9uc2UiABJmCg9FeGVjdXRpb25TdGF0dXMSJy5vbGl2ZXRpbi5hcGkudjEuRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBooLm9saXZldGluLmFwaS52MS5FeGVjdXRpb25TdGF0dXNSZXNwb25zZSIAEk4KB0dldExvZ3MSHy5vbGl2ZXRpbi5hcGkudjEuR2V0TG9nc1JlcXVlc3QaIC5vbGl2ZXRpbi5hcGkudjEuR2V0TG9nc1Jlc3BvbnNlIgASYAoNR2V0QWN0aW9uTG9ncxIlLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25Mb2dzUmVxdWVzdBomLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25Mb2dzUmVzcG9uc2UiABJ1ChRWYWxpZGF0ZUFyZ3VtZW50VHlwZRIsLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlcXVlc3QaLS5vbGl2ZXRpbi5hcGkudjEuVmFsaWRhdGVBcmd1bWVudFR5cGVSZXNwb25zZSIAEksKBldob0FtSRIeLm9saXZldGluLmFwaS52MS5XaG9BbUlSZXF1ZXN0Gh8ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlc3BvbnNlIgASVAoJU29zUmVwb3J0EiEub2xpdmV0aW4uYXBpLnYxLlNvc1JlcG9ydFJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuU29zUmVwb3J0UmVzcG9uc2UiABJRCghEdW1wVmFycxIgLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1JlcXVlc3QaIS5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZSIAEngKFUR1bXBQdWJsaWNJZEFjdGlvbk1hcBItLm9saXZldGluLmFwaS52MS5EdW1wUHVibGljSWRBY3Rpb25NYXBSZXF1ZXN0Gi4ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlc3BvbnNlIgASVAoJR2V0UmVhZHl6EiEub2xpdmV0aW4uYXBpLnYxLkdldFJlYWR5elJlcXVlc3QaIi5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVzcG9uc2UiABJjCg5Mb2NhbFVzZXJMb2dpbhImLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuTG9jYWxVc2VyTG9naW5SZXNwb25zZSIAEl0KDFBhc3N3b3JkSGFzaBIkLm9saXZldGluLmFwaS52MS5QYXNzd29yZEhhc2hSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlc3BvbnNlIgASSwoGTG9nb3V0Eh4ub2xpdmV0aW4uYXBpLnYxLkxvZ291dFJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVzcG9uc2UiABJcCgtFdmVudFN0cmVhbRIjLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuRXZlbnRTdHJlYW1SZXNwb25zZSIAMAESYwoOR2V0RGlhZ25vc3RpY3MSJi5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXF1ZXN0Gicub2xpdmV0aW4uYXBpLnYxLkdldERpYWdub3N0aWNzUmVzcG9uc2UiABJFCgRJbml0Ehwub2xpdmV0aW4uYXBpLnYxLkluaXRSZXF1ZXN0Gh0ub2xpdmV0aW4uYXBpLnYxLkluaXRSZXNwb25zZSIAEmkKEEdldEFjdGlvbkJpbmRpbmcSKC5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuR2V0QWN0aW9uQmluZGluZ1Jlc3BvbnNlIgASWgoLR2V0RW50aXRpZXMSIy5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLkdldEVudGl0aWVzUmVzcG9uc2UiABJJCglHZXRFbnRpdHkSIS5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXR5UmVxdWVzdBoXLm9saXZldGluLmFwaS52MS5FbnRpdHkiAEI4WjZnaXRodWIuY29tL09saXZlVGluL09saXZlVGluL2dlbi9vbGl2ZXRpbi9hcGkvdjE7YXBpdjFiBnByb3RvMw"); + fileDesc("Ch5vbGl2ZXRpbi9hcGkvdjEvb2xpdmV0aW4ucHJvdG8SD29saXZldGluLmFwaS52MSK2AwoGQWN0aW9uEhIKCmJpbmRpbmdfaWQYASABKAkSDQoFdGl0bGUYAiABKAkSDAoEaWNvbhgDIAEoCRIQCghjYW5fZXhlYxgEIAEoCBIyCglhcmd1bWVudHMYBSADKAsyHy5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uQXJndW1lbnQSFgoOcG9wdXBfb25fc3RhcnQYBiABKAkSDQoFb3JkZXIYByABKAUSDwoHdGltZW91dBgIIAEoBRIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYCSABKAkSFwoPZXhlY19vbl9zdGFydHVwGAogASgIEhQKDGV4ZWNfb25fY3JvbhgLIAMoCRIjChtleGVjX29uX2ZpbGVfY3JlYXRlZF9pbl9kaXIYDCADKAkSIwobZXhlY19vbl9maWxlX2NoYW5nZWRfaW5fZGlyGA0gAygJEh0KFWV4ZWNfb25fY2FsZW5kYXJfZmlsZRgOIAEoCRJAChBleGVjX29uX3dlYmhvb2tzGA8gAygLMiYub2xpdmV0aW4uYXBpLnYxLkFjdGlvbldlYmhvb2tFeGVjSGludCLDAgoVQWN0aW9uV2ViaG9va0V4ZWNIaW50EhAKCHRlbXBsYXRlGAEgASgJEhIKCm1hdGNoX3BhdGgYAiABKAkSTwoNbWF0Y2hfaGVhZGVycxgDIAMoCzI4Lm9saXZldGluLmFwaS52MS5BY3Rpb25XZWJob29rRXhlY0hpbnQuTWF0Y2hIZWFkZXJzRW50cnkSSwoLbWF0Y2hfcXVlcnkYBCADKAsyNi5vbGl2ZXRpbi5hcGkudjEuQWN0aW9uV2ViaG9va0V4ZWNIaW50Lk1hdGNoUXVlcnlFbnRyeRozChFNYXRjaEhlYWRlcnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBGjEKD01hdGNoUXVlcnlFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIrsCCg5BY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXRpdGxlGAIgASgJEgwKBHR5cGUYAyABKAkSFQoNZGVmYXVsdF92YWx1ZRgEIAEoCRI2CgdjaG9pY2VzGAUgAygLMiUub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkFyZ3VtZW50Q2hvaWNlEhMKC2Rlc2NyaXB0aW9uGAYgASgJEkUKC3N1Z2dlc3Rpb25zGAcgAygLMjAub2xpdmV0aW4uYXBpLnYxLkFjdGlvbkFyZ3VtZW50LlN1Z2dlc3Rpb25zRW50cnkSHwoXc3VnZ2VzdGlvbnNfYnJvd3Nlcl9rZXkYCCABKAkaMgoQU3VnZ2VzdGlvbnNFbnRyeRILCgNrZXkYASABKAkSDQoFdmFsdWUYAiABKAk6AjgBIjQKFEFjdGlvbkFyZ3VtZW50Q2hvaWNlEg0KBXZhbHVlGAEgASgJEg0KBXRpdGxlGAIgASgJIrIBCgZFbnRpdHkSDQoFdGl0bGUYASABKAkSEgoKdW5pcXVlX2tleRgCIAEoCRIMCgR0eXBlGAMgASgJEhMKC2RpcmVjdG9yaWVzGAQgAygJEjMKBmZpZWxkcxgFIAMoCzIjLm9saXZldGluLmFwaS52MS5FbnRpdHkuRmllbGRzRW50cnkaLQoLRmllbGRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASJUChRHZXREYXNoYm9hcmRSZXNwb25zZRINCgV0aXRsZRgBIAEoCRItCglkYXNoYm9hcmQYBCABKAsyGi5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkIl8KD0VmZmVjdGl2ZVBvbGljeRIYChBzaG93X2RpYWdub3N0aWNzGAEgASgIEhUKDXNob3dfbG9nX2xpc3QYAiABKAgSGwoTc2hvd192ZXJzaW9uX251bWJlchgDIAEoCCJNChNHZXREYXNoYm9hcmRSZXF1ZXN0Eg0KBXRpdGxlGAEgASgJEhMKC2VudGl0eV90eXBlGAIgASgJEhIKCmVudGl0eV9rZXkYAyABKAkiUQoJRGFzaGJvYXJkEg0KBXRpdGxlGAEgASgJEjUKCGNvbnRlbnRzGAIgAygLMiMub2xpdmV0aW4uYXBpLnYxLkRhc2hib2FyZENvbXBvbmVudCLbAQoSRGFzaGJvYXJkQ29tcG9uZW50Eg0KBXRpdGxlGAEgASgJEgwKBHR5cGUYAiABKAkSNQoIY29udGVudHMYAyADKAsyIy5vbGl2ZXRpbi5hcGkudjEuRGFzaGJvYXJkQ29tcG9uZW50EgwKBGljb24YBCABKAkSEQoJY3NzX2NsYXNzGAUgASgJEicKBmFjdGlvbhgGIAEoCzIXLm9saXZldGluLmFwaS52MS5BY3Rpb24SEwoLZW50aXR5X3R5cGUYByABKAkSEgoKZW50aXR5X2tleRgIIAEoCSJ9ChJTdGFydEFjdGlvblJlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCRI3Cglhcmd1bWVudHMYAiADKAsyJC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25Bcmd1bWVudBIaChJ1bmlxdWVfdHJhY2tpbmdfaWQYAyABKAkiMgoTU3RhcnRBY3Rpb25Bcmd1bWVudBIMCgRuYW1lGAEgASgJEg0KBXZhbHVlGAIgASgJIjQKE1N0YXJ0QWN0aW9uUmVzcG9uc2USHQoVZXhlY3V0aW9uX3RyYWNraW5nX2lkGAIgASgJImcKGVN0YXJ0QWN0aW9uQW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEjcKCWFyZ3VtZW50cxgCIAMoCzIkLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFyZ3VtZW50IkoKGlN0YXJ0QWN0aW9uQW5kV2FpdFJlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIsChdTdGFydEFjdGlvbkJ5R2V0UmVxdWVzdBIRCglhY3Rpb25faWQYASABKAkiOQoYU3RhcnRBY3Rpb25CeUdldFJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgCIAEoCSIzCh5TdGFydEFjdGlvbkJ5R2V0QW5kV2FpdFJlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJIk8KH1N0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVzcG9uc2USLAoJbG9nX2VudHJ5GAEgASgLMhkub2xpdmV0aW4uYXBpLnYxLkxvZ0VudHJ5Ik4KDkdldExvZ3NSZXF1ZXN0EhQKDHN0YXJ0X29mZnNldBgBIAEoAxITCgtkYXRlX2ZpbHRlchgCIAEoCRIRCglwYWdlX3NpemUYAyABKAMimgMKCExvZ0VudHJ5EhgKEGRhdGV0aW1lX3N0YXJ0ZWQYASABKAkSFAoMYWN0aW9uX3RpdGxlGAIgASgJEg4KBm91dHB1dBgDIAEoCRIRCgl0aW1lZF9vdXQYBSABKAgSEQoJZXhpdF9jb2RlGAYgASgFEgwKBHVzZXIYByABKAkSEgoKdXNlcl9jbGFzcxgIIAEoCRITCgthY3Rpb25faWNvbhgJIAEoCRIMCgR0YWdzGAogAygJEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgLIAEoCRIZChFkYXRldGltZV9maW5pc2hlZBgMIAEoCRIZChFleGVjdXRpb25fc3RhcnRlZBgOIAEoCBIaChJleGVjdXRpb25fZmluaXNoZWQYDyABKAgSDwoHYmxvY2tlZBgQIAEoCBIWCg5kYXRldGltZV9pbmRleBgRIAEoAxIQCghjYW5fa2lsbBgSIAEoCBIjChtkYXRldGltZV9yYXRlX2xpbWl0X2V4cGlyZXMYEyABKAkSEgoKYmluZGluZ19pZBgUIAEoCSKRAQoPR2V0TG9nc1Jlc3BvbnNlEicKBGxvZ3MYASADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSFwoPY291bnRfcmVtYWluaW5nGAIgASgDEhEKCXBhZ2Vfc2l6ZRgDIAEoAxITCgt0b3RhbF9jb3VudBgEIAEoAxIUCgxzdGFydF9vZmZzZXQYBSABKAMiPwoUR2V0QWN0aW9uTG9nc1JlcXVlc3QSEQoJYWN0aW9uX2lkGAEgASgJEhQKDHN0YXJ0X29mZnNldBgCIAEoAyKXAQoVR2V0QWN0aW9uTG9nc1Jlc3BvbnNlEicKBGxvZ3MYASADKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkSFwoPY291bnRfcmVtYWluaW5nGAIgASgDEhEKCXBhZ2Vfc2l6ZRgDIAEoAxITCgt0b3RhbF9jb3VudBgEIAEoAxIUCgxzdGFydF9vZmZzZXQYBSABKAMiZQobVmFsaWRhdGVBcmd1bWVudFR5cGVSZXF1ZXN0Eg0KBXZhbHVlGAEgASgJEgwKBHR5cGUYAiABKAkSEgoKYmluZGluZ19pZBgDIAEoCRIVCg1hcmd1bWVudF9uYW1lGAQgASgJIkIKHFZhbGlkYXRlQXJndW1lbnRUeXBlUmVzcG9uc2USDQoFdmFsaWQYASABKAgSEwoLZGVzY3JpcHRpb24YAiABKAkiNgoVV2F0Y2hFeGVjdXRpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCSImChRXYXRjaEV4ZWN1dGlvblVwZGF0ZRIOCgZ1cGRhdGUYASABKAkiSgoWRXhlY3V0aW9uU3RhdHVzUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSEQoJYWN0aW9uX2lkGAIgASgJIkcKF0V4ZWN1dGlvblN0YXR1c1Jlc3BvbnNlEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIPCg1XaG9BbUlSZXF1ZXN0ImwKDldob0FtSVJlc3BvbnNlEhoKEmF1dGhlbnRpY2F0ZWRfdXNlchgBIAEoCRIRCgl1c2VyZ3JvdXAYAiABKAkSEAoIcHJvdmlkZXIYAyABKAkSDAoEYWNscxgEIAMoCRILCgNzaWQYBSABKAkiEgoQU29zUmVwb3J0UmVxdWVzdCIiChFTb3NSZXBvcnRSZXNwb25zZRINCgVhbGVydBgBIAEoCSIRCg9EdW1wVmFyc1JlcXVlc3QilQEKEER1bXBWYXJzUmVzcG9uc2USDQoFYWxlcnQYASABKAkSQQoIY29udGVudHMYAiADKAsyLy5vbGl2ZXRpbi5hcGkudjEuRHVtcFZhcnNSZXNwb25zZS5Db250ZW50c0VudHJ5Gi8KDUNvbnRlbnRzRW50cnkSCwoDa2V5GAEgASgJEg0KBXZhbHVlGAIgASgJOgI4ASI7CgxEZWJ1Z0JpbmRpbmcSFAoMYWN0aW9uX3RpdGxlGAEgASgJEhUKDWVudGl0eV9wcmVmaXgYAiABKAkiHgocRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVxdWVzdCLOAQodRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2USDQoFYWxlcnQYASABKAkSTgoIY29udGVudHMYAiADKAsyPC5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UuQ29udGVudHNFbnRyeRpOCg1Db250ZW50c0VudHJ5EgsKA2tleRgBIAEoCRIsCgV2YWx1ZRgCIAEoCzIdLm9saXZldGluLmFwaS52MS5EZWJ1Z0JpbmRpbmc6AjgBIhIKEEdldFJlYWR5elJlcXVlc3QiIwoRR2V0UmVhZHl6UmVzcG9uc2USDgoGc3RhdHVzGAEgASgJIhQKEkV2ZW50U3RyZWFtUmVxdWVzdCLjAgoTRXZlbnRTdHJlYW1SZXNwb25zZRI9Cg5lbnRpdHlfY2hhbmdlZBgCIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudEVudGl0eUNoYW5nZWRIABI9Cg5jb25maWdfY2hhbmdlZBgDIAEoCzIjLm9saXZldGluLmFwaS52MS5FdmVudENvbmZpZ0NoYW5nZWRIABJFChJleGVjdXRpb25fZmluaXNoZWQYBCABKAsyJy5vbGl2ZXRpbi5hcGkudjEuRXZlbnRFeGVjdXRpb25GaW5pc2hlZEgAEkMKEWV4ZWN1dGlvbl9zdGFydGVkGAUgASgLMiYub2xpdmV0aW4uYXBpLnYxLkV2ZW50RXhlY3V0aW9uU3RhcnRlZEgAEjkKDG91dHB1dF9jaHVuaxgGIAEoCzIhLm9saXZldGluLmFwaS52MS5FdmVudE91dHB1dENodW5rSABCBwoFZXZlbnQiQQoQRXZlbnRPdXRwdXRDaHVuaxIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkSDgoGb3V0cHV0GAIgASgJIhQKEkV2ZW50RW50aXR5Q2hhbmdlZCIUChJFdmVudENvbmZpZ0NoYW5nZWQiRgoWRXZlbnRFeGVjdXRpb25GaW5pc2hlZBIsCglsb2dfZW50cnkYASABKAsyGS5vbGl2ZXRpbi5hcGkudjEuTG9nRW50cnkiRQoVRXZlbnRFeGVjdXRpb25TdGFydGVkEiwKCWxvZ19lbnRyeRgBIAEoCzIZLm9saXZldGluLmFwaS52MS5Mb2dFbnRyeSIyChFLaWxsQWN0aW9uUmVxdWVzdBIdChVleGVjdXRpb25fdHJhY2tpbmdfaWQYASABKAkibQoSS2lsbEFjdGlvblJlc3BvbnNlEh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCRIOCgZraWxsZWQYAiABKAgSGQoRYWxyZWFkeV9jb21wbGV0ZWQYAyABKAgSDQoFZm91bmQYBCABKAgiOwoVTG9jYWxVc2VyTG9naW5SZXF1ZXN0EhAKCHVzZXJuYW1lGAEgASgJEhAKCHBhc3N3b3JkGAIgASgJIikKFkxvY2FsVXNlckxvZ2luUmVzcG9uc2USDwoHc3VjY2VzcxgBIAEoCCInChNQYXNzd29yZEhhc2hSZXF1ZXN0EhAKCHBhc3N3b3JkGAEgASgJIiQKFFBhc3N3b3JkSGFzaFJlc3BvbnNlEgwKBGhhc2gYASABKAkiDwoNTG9nb3V0UmVxdWVzdCIQCg5Mb2dvdXRSZXNwb25zZSIXChVHZXREaWFnbm9zdGljc1JlcXVlc3QiRQoWR2V0RGlhZ25vc3RpY3NSZXNwb25zZRITCgtTc2hGb3VuZEtleRgBIAEoCRIWCg5Tc2hGb3VuZENvbmZpZxgCIAEoCSINCgtJbml0UmVxdWVzdCLrBQoMSW5pdFJlc3BvbnNlEhIKCnNob3dGb290ZXIYASABKAgSFgoOc2hvd05hdmlnYXRpb24YAiABKAgSFwoPc2hvd05ld1ZlcnNpb25zGAMgASgIEhgKEGF2YWlsYWJsZVZlcnNpb24YBCABKAkSFgoOY3VycmVudFZlcnNpb24YBSABKAkSEQoJcGFnZVRpdGxlGAYgASgJEh4KFnNlY3Rpb25OYXZpZ2F0aW9uU3R5bGUYByABKAkSGgoSZGVmYXVsdEljb25Gb3JCYWNrGAggASgJEhYKDmVuYWJsZUN1c3RvbUpzGAkgASgIEhQKDGF1dGhMb2dpblVybBgKIAEoCRIWCg5hdXRoTG9jYWxMb2dpbhgLIAEoCBIRCglzdHlsZU1vZHMYDCADKAkSOAoPb0F1dGgyUHJvdmlkZXJzGA0gAygLMh8ub2xpdmV0aW4uYXBpLnYxLk9BdXRoMlByb3ZpZGVyEjgKD2FkZGl0aW9uYWxMaW5rcxgOIAMoCzIfLm9saXZldGluLmFwaS52MS5BZGRpdGlvbmFsTGluaxIWCg5yb290RGFzaGJvYXJkcxgPIAMoCRIaChJhdXRoZW50aWNhdGVkX3VzZXIYECABKAkSIwobYXV0aGVudGljYXRlZF91c2VyX3Byb3ZpZGVyGBEgASgJEjoKEGVmZmVjdGl2ZV9wb2xpY3kYEiABKAsyIC5vbGl2ZXRpbi5hcGkudjEuRWZmZWN0aXZlUG9saWN5EhYKDmJhbm5lcl9tZXNzYWdlGBMgASgJEhIKCmJhbm5lcl9jc3MYFCABKAkSGAoQc2hvd19kaWFnbm9zdGljcxgVIAEoCBIVCg1zaG93X2xvZ19saXN0GBYgASgIEhYKDmxvZ2luX3JlcXVpcmVkGBcgASgIEhgKEGF2YWlsYWJsZV90aGVtZXMYGCADKAkSJAocc2hvd19uYXZpZ2F0ZV9vbl9zdGFydF9pY29ucxgZIAEoCCIsCg5BZGRpdGlvbmFsTGluaxINCgV0aXRsZRgBIAEoCRILCgN1cmwYAiABKAkiOgoOT0F1dGgyUHJvdmlkZXISDQoFdGl0bGUYASABKAkSDAoEaWNvbhgDIAEoCRILCgNrZXkYBCABKAkiLQoXR2V0QWN0aW9uQmluZGluZ1JlcXVlc3QSEgoKYmluZGluZ19pZBgBIAEoCSJDChhHZXRBY3Rpb25CaW5kaW5nUmVzcG9uc2USJwoGYWN0aW9uGAEgASgLMhcub2xpdmV0aW4uYXBpLnYxLkFjdGlvbiIUChJHZXRFbnRpdGllc1JlcXVlc3QiVAoTR2V0RW50aXRpZXNSZXNwb25zZRI9ChJlbnRpdHlfZGVmaW5pdGlvbnMYASADKAsyIS5vbGl2ZXRpbi5hcGkudjEuRW50aXR5RGVmaW5pdGlvbiJpChBFbnRpdHlEZWZpbml0aW9uEg0KBXRpdGxlGAEgASgJEioKCWluc3RhbmNlcxgCIAMoCzIXLm9saXZldGluLmFwaS52MS5FbnRpdHkSGgoSdXNlZF9vbl9kYXNoYm9hcmRzGAMgAygJIjQKEEdldEVudGl0eVJlcXVlc3QSEgoKdW5pcXVlX2tleRgBIAEoCRIMCgR0eXBlGAIgASgJIjUKFFJlc3RhcnRBY3Rpb25SZXF1ZXN0Eh0KFWV4ZWN1dGlvbl90cmFja2luZ19pZBgBIAEoCTLoEgoST2xpdmVUaW5BcGlTZXJ2aWNlEl0KDEdldERhc2hib2FyZBIkLm9saXZldGluLmFwaS52MS5HZXREYXNoYm9hcmRSZXF1ZXN0GiUub2xpdmV0aW4uYXBpLnYxLkdldERhc2hib2FyZFJlc3BvbnNlIgASWgoLU3RhcnRBY3Rpb24SIy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVzcG9uc2UiABJvChJTdGFydEFjdGlvbkFuZFdhaXQSKi5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25BbmRXYWl0UmVxdWVzdBorLm9saXZldGluLmFwaS52MS5TdGFydEFjdGlvbkFuZFdhaXRSZXNwb25zZSIAEmkKEFN0YXJ0QWN0aW9uQnlHZXQSKC5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldFJlcXVlc3QaKS5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldFJlc3BvbnNlIgASfgoXU3RhcnRBY3Rpb25CeUdldEFuZFdhaXQSLy5vbGl2ZXRpbi5hcGkudjEuU3RhcnRBY3Rpb25CeUdldEFuZFdhaXRSZXF1ZXN0GjAub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uQnlHZXRBbmRXYWl0UmVzcG9uc2UiABJeCg1SZXN0YXJ0QWN0aW9uEiUub2xpdmV0aW4uYXBpLnYxLlJlc3RhcnRBY3Rpb25SZXF1ZXN0GiQub2xpdmV0aW4uYXBpLnYxLlN0YXJ0QWN0aW9uUmVzcG9uc2UiABJXCgpLaWxsQWN0aW9uEiIub2xpdmV0aW4uYXBpLnYxLktpbGxBY3Rpb25SZXF1ZXN0GiMub2xpdmV0aW4uYXBpLnYxLktpbGxBY3Rpb25SZXNwb25zZSIAEmYKD0V4ZWN1dGlvblN0YXR1cxInLm9saXZldGluLmFwaS52MS5FeGVjdXRpb25TdGF0dXNSZXF1ZXN0Gigub2xpdmV0aW4uYXBpLnYxLkV4ZWN1dGlvblN0YXR1c1Jlc3BvbnNlIgASTgoHR2V0TG9ncxIfLm9saXZldGluLmFwaS52MS5HZXRMb2dzUmVxdWVzdBogLm9saXZldGluLmFwaS52MS5HZXRMb2dzUmVzcG9uc2UiABJgCg1HZXRBY3Rpb25Mb2dzEiUub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkxvZ3NSZXF1ZXN0GiYub2xpdmV0aW4uYXBpLnYxLkdldEFjdGlvbkxvZ3NSZXNwb25zZSIAEnUKFFZhbGlkYXRlQXJndW1lbnRUeXBlEiwub2xpdmV0aW4uYXBpLnYxLlZhbGlkYXRlQXJndW1lbnRUeXBlUmVxdWVzdBotLm9saXZldGluLmFwaS52MS5WYWxpZGF0ZUFyZ3VtZW50VHlwZVJlc3BvbnNlIgASSwoGV2hvQW1JEh4ub2xpdmV0aW4uYXBpLnYxLldob0FtSVJlcXVlc3QaHy5vbGl2ZXRpbi5hcGkudjEuV2hvQW1JUmVzcG9uc2UiABJUCglTb3NSZXBvcnQSIS5vbGl2ZXRpbi5hcGkudjEuU29zUmVwb3J0UmVxdWVzdBoiLm9saXZldGluLmFwaS52MS5Tb3NSZXBvcnRSZXNwb25zZSIAElEKCER1bXBWYXJzEiAub2xpdmV0aW4uYXBpLnYxLkR1bXBWYXJzUmVxdWVzdBohLm9saXZldGluLmFwaS52MS5EdW1wVmFyc1Jlc3BvbnNlIgASeAoVRHVtcFB1YmxpY0lkQWN0aW9uTWFwEi0ub2xpdmV0aW4uYXBpLnYxLkR1bXBQdWJsaWNJZEFjdGlvbk1hcFJlcXVlc3QaLi5vbGl2ZXRpbi5hcGkudjEuRHVtcFB1YmxpY0lkQWN0aW9uTWFwUmVzcG9uc2UiABJUCglHZXRSZWFkeXoSIS5vbGl2ZXRpbi5hcGkudjEuR2V0UmVhZHl6UmVxdWVzdBoiLm9saXZldGluLmFwaS52MS5HZXRSZWFkeXpSZXNwb25zZSIAEmMKDkxvY2FsVXNlckxvZ2luEiYub2xpdmV0aW4uYXBpLnYxLkxvY2FsVXNlckxvZ2luUmVxdWVzdBonLm9saXZldGluLmFwaS52MS5Mb2NhbFVzZXJMb2dpblJlc3BvbnNlIgASXQoMUGFzc3dvcmRIYXNoEiQub2xpdmV0aW4uYXBpLnYxLlBhc3N3b3JkSGFzaFJlcXVlc3QaJS5vbGl2ZXRpbi5hcGkudjEuUGFzc3dvcmRIYXNoUmVzcG9uc2UiABJLCgZMb2dvdXQSHi5vbGl2ZXRpbi5hcGkudjEuTG9nb3V0UmVxdWVzdBofLm9saXZldGluLmFwaS52MS5Mb2dvdXRSZXNwb25zZSIAElwKC0V2ZW50U3RyZWFtEiMub2xpdmV0aW4uYXBpLnYxLkV2ZW50U3RyZWFtUmVxdWVzdBokLm9saXZldGluLmFwaS52MS5FdmVudFN0cmVhbVJlc3BvbnNlIgAwARJjCg5HZXREaWFnbm9zdGljcxImLm9saXZldGluLmFwaS52MS5HZXREaWFnbm9zdGljc1JlcXVlc3QaJy5vbGl2ZXRpbi5hcGkudjEuR2V0RGlhZ25vc3RpY3NSZXNwb25zZSIAEkUKBEluaXQSHC5vbGl2ZXRpbi5hcGkudjEuSW5pdFJlcXVlc3QaHS5vbGl2ZXRpbi5hcGkudjEuSW5pdFJlc3BvbnNlIgASaQoQR2V0QWN0aW9uQmluZGluZxIoLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25CaW5kaW5nUmVxdWVzdBopLm9saXZldGluLmFwaS52MS5HZXRBY3Rpb25CaW5kaW5nUmVzcG9uc2UiABJaCgtHZXRFbnRpdGllcxIjLm9saXZldGluLmFwaS52MS5HZXRFbnRpdGllc1JlcXVlc3QaJC5vbGl2ZXRpbi5hcGkudjEuR2V0RW50aXRpZXNSZXNwb25zZSIAEkkKCUdldEVudGl0eRIhLm9saXZldGluLmFwaS52MS5HZXRFbnRpdHlSZXF1ZXN0Ghcub2xpdmV0aW4uYXBpLnYxLkVudGl0eSIAQjhaNmdpdGh1Yi5jb20vT2xpdmVUaW4vT2xpdmVUaW4vZ2VuL29saXZldGluL2FwaS92MTthcGl2MWIGcHJvdG8z"); /** * Describes the message olivetin.api.v1.Action. diff --git a/frontend/resources/vue/components/ActionIconGlyph.vue b/frontend/resources/vue/components/ActionIconGlyph.vue index 4695ff5..99df9d0 100644 --- a/frontend/resources/vue/components/ActionIconGlyph.vue +++ b/frontend/resources/vue/components/ActionIconGlyph.vue @@ -7,6 +7,7 @@ height="1em" class="action-icon-glyph-svg" /> + @@ -15,6 +16,7 @@ import { computed } from 'vue' import { HugeiconsIcon } from '@hugeicons/vue' import { CommandLineIcon } from '@hugeicons/core-free-icons' +import { decodeHtmlEntities, glyphLooksLikeHtml } from './actionIconGlyphHelpers.mjs' const hugeiconsPrefix = 'hugeicons:' @@ -46,16 +48,6 @@ const hugeiconsModel = computed(() => { return iconModel ?? CommandLineIcon }) -function decodeHtmlEntities(text) { - return text.replace(/&#x([0-9a-fA-F]+);?/g, (_, hex) => { - const codePoint = Number.parseInt(hex, 16) - return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : '' - }).replace(/&#(\d+);?/g, (_, decimal) => { - const codePoint = Number.parseInt(decimal, 10) - return Number.isFinite(codePoint) ? String.fromCodePoint(codePoint) : '' - }) -} - const decodedTextGlyph = computed(() => { if (hugeiconsModel.value) { return '' @@ -63,6 +55,8 @@ const decodedTextGlyph = computed(() => { return decodeHtmlEntities(props.glyph) }) + +const decodedTextGlyphIsHtml = computed(() => glyphLooksLikeHtml(decodedTextGlyph.value))