diff --git a/app/Livewire/Project/Shared/EnvironmentVariable/All.php b/app/Livewire/Project/Shared/EnvironmentVariable/All.php index 22f98dbe95..4f4e67a388 100644 --- a/app/Livewire/Project/Shared/EnvironmentVariable/All.php +++ b/app/Livewire/Project/Shared/EnvironmentVariable/All.php @@ -1001,8 +1001,12 @@ class All extends Component if ($item->is_multiline) { return "$item->key=(Multiline environment variable, edit in normal view)"; } + $value = formatEnvValueForDevView($item->value); + if ($value === null) { + return "$item->key=(Value cannot be shown in this view, edit in normal view)"; + } - return "$item->key=$item->value"; + return "$item->key=$value"; })->join("\n"); } @@ -1224,7 +1228,7 @@ class All extends Component $found = $this->resource->$method()->where('key', $key)->first(); if ($found) { - if (! $found->is_shown_once && ! $found->is_multiline) { + if (! $found->is_shown_once && ! $found->is_multiline && formatEnvValueForDevView($found->value) !== null) { $changed = false; // Update value if it changed diff --git a/app/Livewire/SharedVariables/Environment/Show.php b/app/Livewire/SharedVariables/Environment/Show.php index 73c023f321..f42820ba66 100644 --- a/app/Livewire/SharedVariables/Environment/Show.php +++ b/app/Livewire/SharedVariables/Environment/Show.php @@ -93,8 +93,12 @@ class Show extends Component if ($item->is_multiline) { return "$item->key=(Multiline environment variable, edit in normal view)"; } + $value = formatEnvValueForDevView($item->value); + if ($value === null) { + return "$item->key=(Value cannot be shown in this view, edit in normal view)"; + } - return "$item->key=$item->value"; + return "$item->key=$value"; })->join("\n"); } @@ -158,7 +162,7 @@ class Show extends Component $found = $this->environment->environment_variables()->where('key', $key)->first(); if ($found) { - if (! $found->is_shown_once && ! $found->is_multiline) { + if (! $found->is_shown_once && ! $found->is_multiline && formatEnvValueForDevView($found->value) !== null) { if ($found->value !== $value) { $found->value = $value; $found->save(); diff --git a/app/Livewire/SharedVariables/Project/Show.php b/app/Livewire/SharedVariables/Project/Show.php index 73590db941..a4c93cb557 100644 --- a/app/Livewire/SharedVariables/Project/Show.php +++ b/app/Livewire/SharedVariables/Project/Show.php @@ -91,8 +91,12 @@ class Show extends Component if ($item->is_multiline) { return "$item->key=(Multiline environment variable, edit in normal view)"; } + $value = formatEnvValueForDevView($item->value); + if ($value === null) { + return "$item->key=(Value cannot be shown in this view, edit in normal view)"; + } - return "$item->key=$item->value"; + return "$item->key=$value"; })->join("\n"); } @@ -150,7 +154,7 @@ class Show extends Component $found = $this->project->environment_variables()->where('key', $key)->first(); if ($found) { - if (! $found->is_shown_once && ! $found->is_multiline) { + if (! $found->is_shown_once && ! $found->is_multiline && formatEnvValueForDevView($found->value) !== null) { if ($found->value !== $value) { $found->value = $value; $found->save(); diff --git a/app/Livewire/SharedVariables/Server/Show.php b/app/Livewire/SharedVariables/Server/Show.php index e775c50257..062ab60123 100644 --- a/app/Livewire/SharedVariables/Server/Show.php +++ b/app/Livewire/SharedVariables/Server/Show.php @@ -92,8 +92,12 @@ class Show extends Component if ($item->is_multiline) { return "$item->key=(Multiline environment variable, edit in normal view)"; } + $value = formatEnvValueForDevView($item->value); + if ($value === null) { + return "$item->key=(Value cannot be shown in this view, edit in normal view)"; + } - return "$item->key=$item->value"; + return "$item->key=$value"; })->join("\n"); } @@ -162,7 +166,7 @@ class Show extends Component $found = $this->server->environment_variables()->where('key', $key)->first(); if ($found) { - if (! $found->is_shown_once && ! $found->is_multiline) { + if (! $found->is_shown_once && ! $found->is_multiline && formatEnvValueForDevView($found->value) !== null) { if ($found->value !== $value || $found->comment !== $comment) { $found->value = $value; $found->comment = $comment; diff --git a/app/Livewire/SharedVariables/Team/Index.php b/app/Livewire/SharedVariables/Team/Index.php index 034e71c223..adabb569a3 100644 --- a/app/Livewire/SharedVariables/Team/Index.php +++ b/app/Livewire/SharedVariables/Team/Index.php @@ -85,8 +85,12 @@ class Index extends Component if ($item->is_multiline) { return "$item->key=(Multiline environment variable, edit in normal view)"; } + $value = formatEnvValueForDevView($item->value); + if ($value === null) { + return "$item->key=(Value cannot be shown in this view, edit in normal view)"; + } - return "$item->key=$item->value"; + return "$item->key=$value"; })->join("\n"); } @@ -149,7 +153,7 @@ class Index extends Component $found = $this->team->environment_variables()->where('key', $key)->first(); if ($found) { - if (! $found->is_shown_once && ! $found->is_multiline) { + if (! $found->is_shown_once && ! $found->is_multiline && formatEnvValueForDevView($found->value) !== null) { if ($found->value !== $value) { $found->value = $value; $found->save(); diff --git a/bootstrap/helpers/shared.php b/bootstrap/helpers/shared.php index d4e1a03390..b9d482a1f7 100644 --- a/bootstrap/helpers/shared.php +++ b/bootstrap/helpers/shared.php @@ -1258,6 +1258,32 @@ function parseEnvFormatToArray($env_file_contents) return $env_array; } +/** + * Format a value for a KEY=VALUE line of the developer view, so that parseEnvFormatToArray() + * reads it back unchanged. Returns null when the format cannot hold the value: it has a line + * break, or it needs quotes and has both quote types. + */ +function formatEnvValueForDevView(?string $value): ?string +{ + $value ??= ''; + if (str_contains($value, "\n")) { + return null; + } + + $needsQuotes = str_starts_with($value, '"') || str_starts_with($value, "'") || preg_match('/\s#/', $value) === 1; + if (! $needsQuotes) { + return $value; + } + if (! str_contains($value, "'")) { + return "'{$value}'"; + } + if (! str_contains($value, '"')) { + return "\"{$value}\""; + } + + return null; +} + /** * Extract inline comments from environment variables in raw docker-compose YAML. * diff --git a/tests/Feature/EnvironmentVariableDevViewRoundTripTest.php b/tests/Feature/EnvironmentVariableDevViewRoundTripTest.php new file mode 100644 index 0000000000..0d4afd2b2d --- /dev/null +++ b/tests/Feature/EnvironmentVariableDevViewRoundTripTest.php @@ -0,0 +1,79 @@ + 0]); + + $this->user = User::factory()->create(); + $this->team = Team::factory()->create(); + $this->team->members()->attach($this->user, ['role' => 'owner']); + $this->project = Project::factory()->create(['team_id' => $this->team->id]); + $this->environment = Environment::factory()->create(['project_id' => $this->project->id]); + $this->application = Application::factory()->create(['environment_id' => $this->environment->id]); + + $this->actingAs($this->user); + session(['currentTeam' => $this->team]); +}); + +$values = [ + 'QUOTED' => '"abc"', + 'SINGLE_QUOTED' => "'\$FOO'", + 'HASH' => 'value #not-a-comment', + 'BOTH_QUOTES' => '\'"Company :: App" \'', +]; + +it('keeps values unchanged when the developer view is saved without edits', function () use ($values) { + foreach ($values as $key => $value) { + EnvironmentVariable::create([ + 'key' => $key, + 'value' => $value, + 'is_literal' => true, + 'resourceable_type' => Application::class, + 'resourceable_id' => $this->application->id, + ]); + } + + $component = Livewire::test(All::class, ['resource' => $this->application]) + ->call('loadEnvironmentVariables') + ->call('switch'); + + $component->call('submit')->assertHasNoErrors(); + $component->call('submit')->assertHasNoErrors(); + + expect($this->application->environment_variables()->whereIn('key', array_keys($values))->get()->pluck('value', 'key')->all())->toBe($values) + ->and($this->application->environment_variables()->whereIn('key', array_keys($values))->whereNotNull('comment')->count())->toBe(0) + ->and($component->get('variables'))->toContain("QUOTED='\"abc\"'") + ->and($component->get('variables'))->toContain('BOTH_QUOTES=(Value cannot be shown in this view, edit in normal view)'); +}); + +it('keeps shared variable values unchanged when the developer view is saved without edits', function () use ($values) { + foreach ($values as $key => $value) { + SharedEnvironmentVariable::create([ + 'key' => $key, + 'value' => $value, + 'type' => 'team', + 'team_id' => $this->team->id, + ]); + } + + $component = Livewire::test(Index::class)->call('switch'); + + $component->call('submit')->assertHasNoErrors(); + $component->call('submit')->assertHasNoErrors(); + + expect($this->team->environment_variables()->get()->pluck('value', 'key')->all())->toBe($values); +}); diff --git a/tests/Unit/FormatEnvValueForDevViewTest.php b/tests/Unit/FormatEnvValueForDevViewTest.php new file mode 100644 index 0000000000..63507a890c --- /dev/null +++ b/tests/Unit/FormatEnvValueForDevViewTest.php @@ -0,0 +1,25 @@ +toBe(['KEY' => ['value' => $value, 'comment' => null]]); +})->with([ + 'plain' => ['plain-value'], + 'empty' => [''], + 'double quoted' => ['"abc"'], + 'single quoted' => ["'\$FOO'"], + 'leading double quote only' => ['"abc'], + 'quotes inside' => ['He said "hi" and \'bye\''], + 'hash after whitespace' => ['value #not-a-comment'], + 'hash without whitespace' => ['value#hash'], + 'double quoted with suffix' => ['"Company :: App" '], +]); + +it('returns null for values that the env format cannot hold', function (string $value) { + expect(formatEnvValueForDevView($value))->toBeNull(); +})->with([ + 'both quote types and a leading quote' => ['\'"Company :: App" \''], + 'line break' => ["first\nsecond"], +]);