mirror of
https://github.com/jlengrand/helidon.git
synced 2026-03-10 08:21:17 +00:00
Form params with ampersand and no value fix (#2227)
* Form params with ampersand and no value fix Signed-off-by: David Kral <david.k.kral@oracle.com>
This commit is contained in:
@@ -20,6 +20,7 @@ import java.util.Arrays;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* Provides access to any form parameters present in the request entity.
|
||||
@@ -68,6 +69,7 @@ public interface FormParams extends Parameters {
|
||||
|
||||
@Override
|
||||
public Builder add(String name, String... values) {
|
||||
Objects.requireNonNull(name);
|
||||
params.computeIfAbsent(name, k -> new ArrayList<>()).addAll(Arrays.asList(values));
|
||||
return this;
|
||||
}
|
||||
|
||||
@@ -15,10 +15,14 @@
|
||||
*/
|
||||
package io.helidon.common.http;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -48,14 +52,28 @@ class FormParamsImpl extends ReadOnlyParameters implements FormParams {
|
||||
}
|
||||
|
||||
static FormParams create(String paramAssignments, MediaType mediaType) {
|
||||
Charset charset = mediaType.charset().map(Charset::forName).orElse(StandardCharsets.UTF_8);
|
||||
Function<String, String> decoder = decoder(mediaType, charset);
|
||||
final Map<String, List<String>> params = new HashMap<>();
|
||||
Matcher m = PATTERNS.get(mediaType).matcher(paramAssignments);
|
||||
while (m.find()) {
|
||||
final String key = m.group(1);
|
||||
final String key = decoder.apply(m.group(1));
|
||||
final String value = m.group(2);
|
||||
params.computeIfAbsent(key, k -> new ArrayList<>()).add(value);
|
||||
if (value == null) {
|
||||
params.computeIfAbsent(key, k -> new ArrayList<>());
|
||||
} else {
|
||||
params.computeIfAbsent(key, k -> new ArrayList<>()).add(decoder.apply(value));
|
||||
}
|
||||
}
|
||||
return new FormParamsImpl(params);
|
||||
}
|
||||
|
||||
private static Function<String, String> decoder(MediaType mediaType, Charset charset) {
|
||||
if (mediaType == MediaType.TEXT_PLAIN) {
|
||||
return (s) -> s;
|
||||
} else {
|
||||
return (s) -> URLDecoder.decode(s, charset);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,10 +15,12 @@
|
||||
*/
|
||||
package io.helidon.media.common;
|
||||
|
||||
import java.net.URLDecoder;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.Flow;
|
||||
import java.util.function.Function;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@@ -47,7 +49,7 @@ class FormParamsBodyReader implements MessageBodyReader<FormParams> {
|
||||
}
|
||||
|
||||
private static Pattern preparePattern(String assignmentSeparator) {
|
||||
return Pattern.compile(String.format("([^=]+)=([^%1$s]+)%1$s?", assignmentSeparator));
|
||||
return Pattern.compile(String.format("([^=%1$s]+)=?([^%1$s]+)?%1$s?", assignmentSeparator));
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -66,23 +68,33 @@ class FormParamsBodyReader implements MessageBodyReader<FormParams> {
|
||||
MessageBodyReaderContext context) {
|
||||
MediaType mediaType = context.contentType().orElseThrow();
|
||||
Charset charset = mediaType.charset().map(Charset::forName).orElse(StandardCharsets.UTF_8);
|
||||
Function<String, String> decoder = decoder(mediaType, charset);
|
||||
|
||||
Single<String> result = mediaType.equals(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
? ContentReaders.readURLEncodedString(publisher, charset)
|
||||
: ContentReaders.readString(publisher, charset);
|
||||
|
||||
return (Single<U>) result.map(formStr -> create(formStr, mediaType));
|
||||
return (Single<U>) ContentReaders.readString(publisher, charset)
|
||||
.map(formStr -> create(formStr, mediaType, decoder));
|
||||
}
|
||||
|
||||
private FormParams create(String paramAssignments, MediaType mediaType) {
|
||||
private FormParams create(String paramAssignments, MediaType mediaType, Function<String, String> decoder) {
|
||||
FormParams.Builder builder = FormParams.builder();
|
||||
Matcher m = PATTERNS.get(mediaType).matcher(paramAssignments);
|
||||
while (m.find()) {
|
||||
final String key = m.group(1);
|
||||
final String value = m.group(2);
|
||||
builder.add(key, value);
|
||||
if (value == null) {
|
||||
builder.add(decoder.apply(key));
|
||||
} else {
|
||||
builder.add(decoder.apply(key), decoder.apply(value));
|
||||
}
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
private Function<String, String> decoder(MediaType mediaType, Charset charset) {
|
||||
if (mediaType == MediaType.TEXT_PLAIN) {
|
||||
return (s) -> s;
|
||||
} else {
|
||||
return (s) -> URLDecoder.decode(s, charset);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -90,13 +90,21 @@ class FormParamsBodyWriter implements MessageBodyWriter<FormParams> {
|
||||
Function<String, String> encoder = encoder();
|
||||
StringBuilder result = new StringBuilder();
|
||||
for (Map.Entry<String, List<String>> entry : formParams.toMap().entrySet()) {
|
||||
for (String value : entry.getValue()) {
|
||||
List<String> values = entry.getValue();
|
||||
if (values.size() == 0) {
|
||||
if (result.length() > 0) {
|
||||
result.append(separator);
|
||||
}
|
||||
result.append(encoder.apply(entry.getKey()));
|
||||
result.append("=");
|
||||
result.append(encoder.apply(value));
|
||||
} else {
|
||||
for (String value : values) {
|
||||
if (result.length() > 0) {
|
||||
result.append(separator);
|
||||
}
|
||||
result.append(encoder.apply(entry.getKey()));
|
||||
result.append("=");
|
||||
result.append(encoder.apply(value));
|
||||
}
|
||||
}
|
||||
}
|
||||
return result.toString();
|
||||
|
||||
@@ -80,6 +80,7 @@ public class GreetService implements Service {
|
||||
.get("/redirectPath", this::redirectPath)
|
||||
.get("/redirect/infinite", this::redirectInfinite)
|
||||
.post("/form", this::form)
|
||||
.post("/form/content", this::formContent)
|
||||
.get("/secure/basic", this::basicAuth)
|
||||
.get("/secure/basic/outbound", this::basicAuthOutbound)
|
||||
.put("/greeting", this::updateGreetingHandler);
|
||||
@@ -139,7 +140,7 @@ public class GreetService implements Service {
|
||||
}
|
||||
|
||||
private void redirectPath(ServerRequest request,
|
||||
ServerResponse response) {
|
||||
ServerResponse response) {
|
||||
response.headers().add(Http.Header.LOCATION, "/greet");
|
||||
response.status(Http.Status.MOVED_PERMANENTLY_301).send();
|
||||
}
|
||||
@@ -155,6 +156,11 @@ public class GreetService implements Service {
|
||||
.thenAccept(res::send);
|
||||
}
|
||||
|
||||
private void formContent(ServerRequest req, ServerResponse res) {
|
||||
req.content().as(FormParams.class)
|
||||
.thenAccept(res::send);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the greeting to use in future messages.
|
||||
*
|
||||
|
||||
@@ -33,6 +33,17 @@ public class FormTest extends TestParent {
|
||||
.add("name", "David Tester")
|
||||
.build();
|
||||
|
||||
private static final String SPECIAL = "special";
|
||||
private static final String MULTIPLE = "multiple";
|
||||
private static final String NO_VALUE = "noValue";
|
||||
private static final String SPECIAL_VALUE = "some &@#/ special value";
|
||||
|
||||
private static final FormParams ADVANCED_TEST_FORM = FormParams.builder()
|
||||
.add(SPECIAL, SPECIAL_VALUE)
|
||||
.add(MULTIPLE, "value1", "value2")
|
||||
.add(NO_VALUE)
|
||||
.build();
|
||||
|
||||
@Test
|
||||
public void testHelloWorld() {
|
||||
webClient.post()
|
||||
@@ -62,4 +73,16 @@ public class FormTest extends TestParent {
|
||||
assertThat(ex.getCause().getMessage(),
|
||||
is("No writer found for type: class io.helidon.common.http.FormParamsImpl"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFormContent() {
|
||||
FormParams received = webClient.post()
|
||||
.path("/form/content")
|
||||
.submit(ADVANCED_TEST_FORM, FormParams.class)
|
||||
.await();
|
||||
|
||||
assertThat(received.all(SPECIAL), is(ADVANCED_TEST_FORM.all(SPECIAL)));
|
||||
assertThat(received.all(MULTIPLE), is(ADVANCED_TEST_FORM.all(MULTIPLE)));
|
||||
assertThat(received.all(NO_VALUE).size(), is(0));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,12 +17,9 @@ package io.helidon.webserver;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.concurrent.Flow.Publisher;
|
||||
|
||||
import io.helidon.common.http.DataChunk;
|
||||
import io.helidon.common.http.FormParams;
|
||||
import io.helidon.common.http.MediaType;
|
||||
import io.helidon.common.reactive.Single;
|
||||
import io.helidon.media.common.ContentReaders;
|
||||
import io.helidon.media.common.DefaultMediaSupport;
|
||||
|
||||
@@ -61,7 +58,7 @@ public class FormParamsSupport implements Service, Handler {
|
||||
Charset charset = reqMediaType.charset().map(Charset::forName).orElse(StandardCharsets.UTF_8);
|
||||
|
||||
req.content().registerReader(FormParams.class,
|
||||
(chunks, type) -> readContent(reqMediaType, charset, chunks)
|
||||
(chunks, type) -> ContentReaders.readString(chunks, charset)
|
||||
.map(s -> FormParams.create(s, reqMediaType)).toStage());
|
||||
|
||||
req.next();
|
||||
@@ -75,10 +72,4 @@ public class FormParamsSupport implements Service, Handler {
|
||||
return INSTANCE;
|
||||
}
|
||||
|
||||
private static Single<String> readContent(MediaType mediaType, Charset charset,
|
||||
Publisher<DataChunk> chunks) {
|
||||
return mediaType.equals(MediaType.APPLICATION_FORM_URLENCODED)
|
||||
? ContentReaders.readURLEncodedString(chunks, charset)
|
||||
: ContentReaders.readString(chunks, charset);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user