Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix form actions for apps deployed behind reverse proxy #51403

Merged
merged 3 commits into from
Oct 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -351,12 +351,29 @@ void EmitFormActionIfNotExplicit(TextWriter output, bool isForm, bool hasExplici
output.Write("action");
output.Write('=');
output.Write('\"');
_htmlEncoder.Encode(output, _navigationManager.Uri);
_htmlEncoder.Encode(output, GetRootRelativeUrlForFormAction(_navigationManager));
output.Write('\"');
}
}
}

private static string GetRootRelativeUrlForFormAction(NavigationManager navigationManager)
{
// We want a root-relative URL because:
// - if we used a base-relative one, then if currentUrl==baseHref, that would result
// in an empty string, but forms have special handling for action="" (it means "submit
// to the current URL, but that would be wrong if there's an uncommitted navigation in
// flight, e.g., after the user clicking 'back' - it would go to whatever's now in the
// address bar, ignoring where the form was rendered)
// - if we used an absolute URL, then it creates a significant extra pit of failure for
// apps hosted behind a reverse proxy (e.g., container apps), because the server's view
// of the absolute URL isn't usable outside the container
// - of course, sites hosted behind URL rewriting that modifies the path will still be
// wrong, but developers won't do that often as it makes things like <a href> really
// difficult to get right. In that case, developers must emit an action attribute manually.
return new Uri(navigationManager.Uri, UriKind.Absolute).PathAndQuery;
DamianEdwards marked this conversation as resolved.
Show resolved Hide resolved
}

private int RenderChildren(int componentId, TextWriter output, ArrayRange<RenderTreeFrame> frames, int position, int maxElements)
{
if (maxElements == 0)
Expand Down
37 changes: 31 additions & 6 deletions src/Components/Web/test/HtmlRendering/HtmlRendererTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using System.Globalization;
using System.Text;
using System.Web;
using Microsoft.AspNetCore.Components.Forms;
using Microsoft.AspNetCore.Components.Forms.Mapping;
using Microsoft.AspNetCore.Components.Rendering;
Expand Down Expand Up @@ -1106,15 +1107,25 @@ await htmlRenderer.Dispatcher.InvokeAsync(async () =>
});
}

[Fact]
public async Task RenderComponentAsync_AddsActionAttributeWithCurrentUrlToFormWithoutAttributes_WhenNoActionSpecified()
[Theory]
[InlineData("https://example.com/", "https://example.com", "/")]
[InlineData("https://example.com/", "https://example.com/", "/")]
[InlineData("https://example.com/", "https://example.com/page", "/page")]
[InlineData("https://example.com/", "https://example.com/a/b/c", "/a/b/c")]
[InlineData("https://example.com/", "https://example.com/a/b/c?q=1&p=hello%20there", "/a/b/c?q=1&p=hello%20there")]
[InlineData("https://example.com/subdir/", "https://example.com/subdir", "/subdir")]
[InlineData("https://example.com/subdir/", "https://example.com/subdir/", "/subdir/")]
[InlineData("https://example.com/a/b/", "https://example.com/a/b/c?q=1&p=2", "/a/b/c?q=1&p=2")]
[InlineData("http://user:pass@xyz.example.com:1234/a/b/", "http://user:pass@xyz.example.com:1234/a/b/c&q=1&p=2", "/a/b/c&q=1&p=2")]
public async Task RenderComponentAsync_AddsActionAttributeWithCurrentUrlToFormWithoutAttributes_WhenNoActionSpecified(
string baseUrl, string currentUrl, string expectedAction)
{
// Arrange
var serviceProvider = GetServiceProvider(collection => collection.AddSingleton(new RenderFragment(rtb =>
{
rtb.OpenElement(0, "form");
rtb.CloseElement();
})).AddScoped<NavigationManager, TestNavigationManager>());
})).AddScoped<NavigationManager>(_ => new TestNavigationManager(baseUrl, currentUrl)));

var htmlRenderer = GetHtmlRenderer(serviceProvider);
await htmlRenderer.Dispatcher.InvokeAsync(async () =>
Expand All @@ -1123,7 +1134,7 @@ await htmlRenderer.Dispatcher.InvokeAsync(async () =>
var result = await htmlRenderer.RenderComponentAsync<TestComponent>();

// Assert
Assert.Equal("<form action=\"https://www.example.com/page\"></form>", result.ToHtmlString());
Assert.Equal($"<form action=\"{HttpUtility.HtmlAttributeEncode(expectedAction)}\"></form>", result.ToHtmlString());
});
}

Expand All @@ -1145,7 +1156,7 @@ await htmlRenderer.Dispatcher.InvokeAsync(async () =>
var result = await htmlRenderer.RenderComponentAsync<TestComponent>();

// Assert
Assert.Equal("<form method=\"post\" action=\"https://www.example.com/page\"></form>", result.ToHtmlString());
Assert.Equal("<form method=\"post\" action=\"/page\"></form>", result.ToHtmlString());
});
}

Expand Down Expand Up @@ -1382,7 +1393,21 @@ public void Map(FormValueMappingContext context)

private class TestNavigationManager : NavigationManager
{
protected override void EnsureInitialized() => Initialize("https://www.example.com/", "https://www.example.com/page");
private string _baseUrl;
private string _currentUrl;

public TestNavigationManager()
: this("https://www.example.com/", "https://www.example.com/page")
{
}

public TestNavigationManager(string baseUrl, string currentUrl)
{
_baseUrl = baseUrl;
_currentUrl = currentUrl;
}

protected override void EnsureInitialized() => Initialize(_baseUrl, _currentUrl);
}

private IServiceProvider GetServiceProvider(Action<IServiceCollection> configure = null)
Expand Down
Loading
Loading