Skip to content

Commit 30f5074

Browse files
[Fix] Site .env: stop escaping double quotes in values that never needed quoting (#1228)
* fix: allow quoted values in .env variables * fix: tighten CR coverage and fix coderabbit bug
1 parent dc75758 commit 30f5074

3 files changed

Lines changed: 118 additions & 10 deletions

File tree

app/Helpers/EnvParser.php

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ public static function parse(string $raw): array
6868
if ($wasDoubleQuoted) {
6969
$value = preg_replace_callback('/\\\\(.)/s', fn (array $matches): string => match ($matches[1]) {
7070
'n' => "\n",
71+
'r' => "\r",
7172
'"' => '"',
7273
'\\' => '\\',
7374
default => $matches[0],
@@ -169,18 +170,31 @@ public static function stringify(array $variables): string
169170
continue;
170171
}
171172

172-
$needsQuotes = str_contains($value, "\n") ||
173-
str_contains($value, ' ') ||
174-
str_contains($value, '"') ||
175-
str_contains($value, "'") ||
176-
str_contains($value, '#');
173+
$needsQuotes = preg_match('/\s/', $value) === 1
174+
|| str_contains($value, '#')
175+
|| str_starts_with($value, '"')
176+
|| str_starts_with($value, "'");
177177

178-
if ($needsQuotes) {
179-
$escapedValue = str_replace(['\\', "\n", '"'], ['\\\\', '\\n', '\\"'], $value);
180-
$lines[] = "{$key}=\"{$escapedValue}\"";
181-
} else {
178+
if (! $needsQuotes) {
182179
$lines[] = "{$key}={$value}";
180+
181+
continue;
183182
}
183+
184+
if (
185+
str_contains($value, '"')
186+
&& ! str_contains($value, "'")
187+
&& ! str_contains($value, '\\')
188+
&& ! str_contains($value, "\n")
189+
&& ! str_contains($value, "\r")
190+
) {
191+
$lines[] = "{$key}='{$value}'";
192+
193+
continue;
194+
}
195+
196+
$escapedValue = str_replace(['\\', "\n", "\r", '"'], ['\\\\', '\\n', '\\r', '\\"'], $value);
197+
$lines[] = "{$key}=\"{$escapedValue}\"";
184198
}
185199

186200
return implode("\n", $lines);

tests/Feature/ApplicationTest.php

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ function vitoPestFeatureApplicationTestAssertBroadcastsDeploymentCreated(callabl
546546
});
547547

548548
test('update env file with variables', function () {
549-
SSH::fake();
549+
$ssh = SSH::fake();
550550

551551
$this->actingAs($this->user);
552552

@@ -558,10 +558,16 @@ function vitoPestFeatureApplicationTestAssertBroadcastsDeploymentCreated(callabl
558558
['key' => 'APP_ENV', 'value' => 'production'],
559559
['key' => 'APP_DEBUG', 'value' => 'false'],
560560
['key' => 'DB_PASSWORD', 'value' => 'secret123'],
561+
['key' => 'VITE_PAYMENT_METHODS_MOLLIE', 'value' => '["ideal","paybybank","bancontact"]'],
561562
],
562563
])
563564
->assertSessionDoesntHaveErrors();
564565

566+
$this->assertStringContainsString(
567+
'VITE_PAYMENT_METHODS_MOLLIE=["ideal","paybybank","bancontact"]',
568+
$ssh->getUploadedContent()
569+
);
570+
565571
$this->site->refresh();
566572

567573
expect(data_get($this->site->type_data, 'env_path'))->toEqual($this->site->path.'/.env');

tests/Unit/EnvParserTest.php

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,87 @@
116116
expect($result)->toEqual('MULTILINE="line1\nline2"');
117117
});
118118

119+
test('stringify leaves a value containing double quotes unquoted when nothing forces quoting', function () {
120+
$variables = [
121+
['key' => 'VITE_PAYMENT_METHODS_MOLLIE', 'value' => '["ideal","paybybank","bancontact"]'],
122+
];
123+
$result = EnvParser::stringify($variables);
124+
125+
expect($result)->toEqual('VITE_PAYMENT_METHODS_MOLLIE=["ideal","paybybank","bancontact"]');
126+
});
127+
128+
/**
129+
* @return array<string, array<int, string>>
130+
*/
131+
dataset('jsonArrayOnDiskProvider', function () {
132+
return [
133+
'bare' => ['VITE_PAYMENT_METHODS_MOLLIE=["ideal","paybybank","bancontact"]'],
134+
'single quoted' => ["VITE_PAYMENT_METHODS_MOLLIE='[\"ideal\",\"paybybank\",\"bancontact\"]'"],
135+
'already escaped' => ['VITE_PAYMENT_METHODS_MOLLIE="[\\"ideal\\",\\"paybybank\\",\\"bancontact\\"]"'],
136+
];
137+
});
138+
139+
test('saving rewrites every on disk form of a json array value as valid json', function (string $raw) {
140+
$parsed = EnvParser::parse($raw);
141+
142+
expect($parsed)->toHaveCount(1);
143+
expect($parsed[0]['value'])->toEqual('["ideal","paybybank","bancontact"]');
144+
expect(EnvParser::stringify($parsed))->toEqual('VITE_PAYMENT_METHODS_MOLLIE=["ideal","paybybank","bancontact"]');
145+
})->with('jsonArrayOnDiskProvider');
146+
147+
test('stringify prefers single quotes over escaping double quotes', function () {
148+
$variables = [
149+
['key' => 'K', 'value' => 'he said "hi"'],
150+
];
151+
$result = EnvParser::stringify($variables);
152+
153+
expect($result)->toEqual('K=\'he said "hi"\'');
154+
});
155+
156+
test('stringify keeps a mid string double quote unquoted', function () {
157+
$result = EnvParser::stringify([['key' => 'K', 'value' => 'x"y']]);
158+
159+
expect($result)->toEqual('K=x"y');
160+
});
161+
162+
test('stringify quotes values containing a hash', function () {
163+
$result = EnvParser::stringify([['key' => 'K', 'value' => '#fff']]);
164+
165+
expect($result)->toEqual('K="#fff"');
166+
});
167+
168+
test('stringify quotes a value that starts with a double quote', function () {
169+
$result = EnvParser::stringify([['key' => 'K', 'value' => '"hello"']]);
170+
171+
expect($result)->toEqual('K=\'"hello"\'');
172+
});
173+
174+
test('stringify quotes a value that starts with a single quote', function () {
175+
$result = EnvParser::stringify([['key' => 'K', 'value' => "'hello'"]]);
176+
177+
expect($result)->toEqual('K="\'hello\'"');
178+
});
179+
180+
test('stringify escapes carriage returns rather than writing them raw', function () {
181+
$result = EnvParser::stringify([['key' => 'K', 'value' => "a\rb"]]);
182+
183+
expect($result)->toEqual('K="a\rb"');
184+
expect(EnvParser::parse($result)[0]['value'])->toEqual("a\rb");
185+
});
186+
187+
test('stringify does not single quote a carriage return value', function () {
188+
$result = EnvParser::stringify([['key' => 'K', 'value' => "a\rb\"c"]]);
189+
190+
expect($result)->toEqual('K="a\rb\"c"');
191+
expect(EnvParser::parse($result)[0]['value'])->toEqual("a\rb\"c");
192+
});
193+
194+
test('stringify escapes when both quote styles are present', function () {
195+
$result = EnvParser::stringify([['key' => 'K', 'value' => 'it\'s "quoted"']]);
196+
197+
expect($result)->toEqual('K="it\'s \\"quoted\\""');
198+
});
199+
119200
test('stringify skips empty keys', function () {
120201
$variables = [
121202
['key' => '', 'value' => 'empty'],
@@ -155,6 +236,13 @@
155236
'empty' => [''],
156237
'base64 padding' => ['base64:abc=123='],
157238
'backslash and quote' => ['C:\dir "x"'],
239+
'json array' => ['["ideal","paybybank","bancontact"]'],
240+
'leading double quote' => ['"hello"'],
241+
'leading single quote' => ["'hello'"],
242+
'tab' => ["a\tb"],
243+
'trailing tab' => ["a\t"],
244+
'carriage return' => ["a\rb"],
245+
'carriage return with double quote' => ["a\rb\"c"],
158246
];
159247
});
160248

0 commit comments

Comments
 (0)