Skip to content

Commit f2775b0

Browse files
committed
feat: general improvements and minor bugs
1 parent 6c4554f commit f2775b0

2 files changed

Lines changed: 152 additions & 9 deletions

File tree

src/WebExpress.WebCore/WebMessage/ResponseSender.cs

Lines changed: 68 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
11
using Microsoft.AspNetCore.Http.Features;
2+
using Microsoft.Extensions.Primitives;
23
using System;
4+
using System.Collections.Generic;
5+
using System.Globalization;
6+
using System.Linq;
7+
using System.Net;
38
using System.Threading.Tasks;
49
using WebExpress.WebCore.WebHtml;
510

@@ -59,7 +64,20 @@ public async Task SendAsync(IHttpContext context, IResponse response, bool keepA
5964

6065
if (response.Header.Cookies.Count != 0)
6166
{
62-
responseFeature.Headers.SetCookie = string.Join(" ", response.Header.Cookies);
67+
// Cookie.ToString() only emits "name=value" and discards
68+
// Path / Expires / Domain / SameSite - which makes the
69+
// browser default-path the cookie to the request URI's
70+
// directory. Build a proper Set-Cookie header per cookie
71+
// so attributes survive the round trip.
72+
var headerValues = response.Header.Cookies
73+
.Cast<Cookie>()
74+
.Select(SerializeSetCookie)
75+
.Where(s => !string.IsNullOrEmpty(s))
76+
.ToArray();
77+
if (headerValues.Length > 0)
78+
{
79+
responseFeature.Headers.SetCookie = new StringValues(headerValues);
80+
}
6381
}
6482

6583
if (!string.IsNullOrWhiteSpace(response.Header.Upgrade))
@@ -112,5 +130,54 @@ public async Task SendAsync(IHttpContext context, IResponse response, bool keepA
112130
log.Error(context.RemoteEndPoint + ": " + ex.Message);
113131
}
114132
}
133+
134+
/// <summary>
135+
/// Serialises a <see cref="Cookie"/> as a single Set-Cookie header
136+
/// value preserving Path, Domain, Expires and the HttpOnly / Secure
137+
/// flags. SameSite defaults to <c>Lax</c> because System.Net.Cookie
138+
/// does not model the attribute directly.
139+
/// </summary>
140+
/// <param name="cookie">The cookie to serialise.</param>
141+
/// <returns>A Set-Cookie header value, or null when the cookie is empty.</returns>
142+
private static string SerializeSetCookie(Cookie cookie)
143+
{
144+
if (cookie is null || string.IsNullOrEmpty(cookie.Name))
145+
{
146+
return null;
147+
}
148+
149+
var parts = new List<string>
150+
{
151+
$"{cookie.Name}={cookie.Value ?? string.Empty}"
152+
};
153+
154+
if (!string.IsNullOrEmpty(cookie.Path))
155+
{
156+
parts.Add($"Path={cookie.Path}");
157+
}
158+
if (!string.IsNullOrEmpty(cookie.Domain))
159+
{
160+
parts.Add($"Domain={cookie.Domain}");
161+
}
162+
if (cookie.Expires != DateTime.MinValue)
163+
{
164+
// RFC 7231 IMF-fixdate: "Wed, 21 Oct 2015 07:28:00 GMT".
165+
parts.Add($"Expires={cookie.Expires.ToUniversalTime().ToString("r", CultureInfo.InvariantCulture)}");
166+
}
167+
if (cookie.HttpOnly)
168+
{
169+
parts.Add("HttpOnly");
170+
}
171+
if (cookie.Secure)
172+
{
173+
parts.Add("Secure");
174+
}
175+
176+
// System.Net.Cookie has no SameSite property; default to Lax for
177+
// first-party fit-for-purpose behaviour without cross-site leaks.
178+
parts.Add("SameSite=Lax");
179+
180+
return string.Join("; ", parts);
181+
}
115182
}
116183
}

src/WebExpress.WebCore/WebTheme/RenderContextThemeExtensions.cs

Lines changed: 84 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,31 @@ namespace WebExpress.WebCore.WebTheme
99
/// application carried in the render context.
1010
/// </summary>
1111
/// <remarks>
12-
/// Themes were moved out of <c>IApplicationContext</c> with the icon-theme
13-
/// migration: the active theme is now the first theme registered for an
14-
/// application via the <c>ThemeManager</c>. These helpers keep call sites
15-
/// short for code that has a render context but no access to the visual
16-
/// tree (e.g. <c>Icon</c> Funcs on form buttons).
12+
/// The resolution order mirrors <c>VisualTreeControl</c> so server-side
13+
/// icon factories and the visual tree end up with the same theme:
14+
/// <list type="number">
15+
/// <item><description>
16+
/// The application's declared default theme (<c>[Theme&lt;T&gt;]</c> →
17+
/// <c>IApplicationContext.DefaultTheme</c>).
18+
/// </description></item>
19+
/// <item><description>
20+
/// The first theme registered for the application (legacy fallback).
21+
/// </description></item>
22+
/// <item><description>
23+
/// <see langword="null"/>; downstream <see cref="TypeIconTheme"/>
24+
/// callers fall back to <see cref="TypeIconTheme.Default"/>.
25+
/// </description></item>
26+
/// </list>
27+
/// Per-user overrides are wired by application code: the page's
28+
/// <c>Process</c> hook calls <c>visualTree.UseTheme&lt;TTheme&gt;()</c>
29+
/// based on whatever store the application keeps; the framework itself
30+
/// does not consult cookies, sessions, or identities.
1731
/// </remarks>
1832
public static class RenderContextThemeExtensions
1933
{
2034
/// <summary>
21-
/// Returns the first theme registered for the render context's
22-
/// application, or <see langword="null"/> when no theme has been
23-
/// registered.
35+
/// Returns the active theme for the render context using the
36+
/// resolution order documented on the class.
2437
/// </summary>
2538
/// <param name="renderContext">The current render context.</param>
2639
/// <returns>The active theme context or <see langword="null"/>.</returns>
@@ -32,6 +45,13 @@ public static IThemeContext GetActiveTheme(this IRenderContext renderContext)
3245
return null;
3346
}
3447

48+
// 1. application's declared default
49+
if (applicationContext.DefaultTheme is { } declared)
50+
{
51+
return declared;
52+
}
53+
54+
// 2. first registered theme for the application
3555
return WebEx.ComponentHub?.ThemeManager?.Themes
3656
?.FirstOrDefault(t => t.ApplicationContext == applicationContext);
3757
}
@@ -47,5 +67,61 @@ public static TypeIconTheme GetIconTheme(this IRenderContext renderContext)
4767
{
4868
return renderContext.GetActiveTheme()?.IconTheme ?? TypeIconTheme.Default;
4969
}
70+
71+
/// <summary>
72+
/// Re-themes an existing <see cref="WebIcon.IIcon"/> for the active
73+
/// icon theme of <paramref name="renderContext"/>. Convenience over
74+
/// <see cref="ApplyIconTheme(WebIcon.IIcon, TypeIconTheme)"/> for
75+
/// callers that only have a render context in hand.
76+
/// </summary>
77+
/// <param name="icon">The icon to re-theme; may be <see langword="null"/>.</param>
78+
/// <param name="renderContext">The current render context.</param>
79+
/// <returns>The re-themed icon or the original instance.</returns>
80+
public static WebIcon.IIcon ApplyIconTheme(this WebIcon.IIcon icon, IRenderContext renderContext)
81+
{
82+
return icon.ApplyIconTheme(renderContext.GetIconTheme());
83+
}
84+
85+
/// <summary>
86+
/// Re-themes an existing <see cref="WebIcon.IIcon"/> for the given
87+
/// <paramref name="theme"/>. Icons created at registration time
88+
/// (e.g. <c>PageContext.PageIcon</c>) carry the theme that was
89+
/// active when the page was discovered; this helper rebuilds them
90+
/// so the breadcrumb, sidebars, etc. swap glyphs at runtime when
91+
/// the application code activates a different theme via
92+
/// <c>visualTree.UseTheme&lt;TTheme&gt;()</c>. Controls that have
93+
/// a visual tree in hand should pass
94+
/// <c>visualTree.IconTheme</c> here.
95+
/// <para>
96+
/// Falls back to <paramref name="icon"/> when its concrete type does
97+
/// not expose a <c>(TypeIconTheme)</c> constructor.
98+
/// </para>
99+
/// </summary>
100+
/// <param name="icon">The icon to re-theme; may be <see langword="null"/>.</param>
101+
/// <param name="theme">The icon theme to apply.</param>
102+
/// <returns>The re-themed icon or the original instance.</returns>
103+
public static WebIcon.IIcon ApplyIconTheme(this WebIcon.IIcon icon, TypeIconTheme theme)
104+
{
105+
if (icon is null)
106+
{
107+
return null;
108+
}
109+
110+
var iconType = icon.GetType();
111+
var ctor = iconType.GetConstructor(new[] { typeof(TypeIconTheme) });
112+
if (ctor is null)
113+
{
114+
return icon;
115+
}
116+
117+
try
118+
{
119+
return ctor.Invoke(new object[] { theme }) as WebIcon.IIcon ?? icon;
120+
}
121+
catch
122+
{
123+
return icon;
124+
}
125+
}
50126
}
51127
}

0 commit comments

Comments
 (0)