diff --git a/docs/Releases.md b/docs/Releases.md index cd85345ea..f81853234 100644 --- a/docs/Releases.md +++ b/docs/Releases.md @@ -6,6 +6,8 @@ layout: "default" This page tracks major changes included in any update starting with version 4.0.0.3 #### Unreleased +- **New**: + - Support for strict CSP (dynamic inline styles removed) ([#634](https://github.com/MiniProfiler/dotnet/pull/634) - thanks [rwasef1830](https://github.com/rwasef1830)) - **Fixes/Changes**: - Upgraded MongoDB driver, allowing automatic index creation and profiler expiration ([#613](https://github.com/MiniProfiler/dotnet/pull/613) - thanks [IanKemp](https://github.com/IanKemp)) diff --git a/samples/Samples.AspNet/Helpers/NonceService.cs b/samples/Samples.AspNet/Helpers/NonceService.cs new file mode 100644 index 000000000..3c7ac3ba9 --- /dev/null +++ b/samples/Samples.AspNet/Helpers/NonceService.cs @@ -0,0 +1,20 @@ +using System; +using System.Security.Cryptography; +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; + +namespace Samples.AspNetCore +{ + /// + /// Nonce service (custom implementation) for sharing a random nonce for the lifetime of a request. + /// + public class NonceService + { + public string RequestNonce { get; } = Convert.ToBase64String(RandomNumberGenerator.GetBytes(64)); + } + + public static class NonceExtensions + { + public static string? GetNonce(this HttpContext context) => context.RequestServices.GetService()?.RequestNonce; + } +} diff --git a/samples/Samples.AspNet/Pages/RazorPagesSample.cshtml b/samples/Samples.AspNet/Pages/RazorPagesSample.cshtml index 1cd9cf6d3..9c315856c 100644 --- a/samples/Samples.AspNet/Pages/RazorPagesSample.cshtml +++ b/samples/Samples.AspNet/Pages/RazorPagesSample.cshtml @@ -13,7 +13,7 @@ @section scripts { - "); - sb.Append(Includes(profiler, path: path, isAuthorized: true)); + sb.Append(Includes(profiler, path: path, isAuthorized: true, nonce: nonce)); sb.Append(@"
"); return sb.ToString(); } @@ -254,17 +279,20 @@ public static string SingleResultHtml(MiniProfiler profiler, string path) /// Renders a full HTML page for the share link in MiniProfiler. /// /// The options to render for. + /// The current request service provider. /// The root path that MiniProfiler is being served from. /// A full HTML page for this MiniProfiler. - public static string ResultListHtml(MiniProfilerBaseOptions options, string path) + public static string ResultListHtml(MiniProfilerBaseOptions options, IServiceProvider serviceProvider, string path) { var version = options.VersionHash; + var nonce = options.NonceProvider?.Invoke(serviceProvider) ?? string.Empty; + var nonceAttribute = !string.IsNullOrWhiteSpace(nonce) ? " nonce=\"" + HttpUtility.HtmlAttributeEncode(nonce) + "\"" : null; return $@" List of profiling sessions - + - + MiniProfiler.listInit({{path: '{path}', version: '{version}', colorScheme: '{options.ColorScheme}'}}); diff --git a/src/MiniProfiler.Shared/MiniProfiler.Shared.csproj b/src/MiniProfiler.Shared/MiniProfiler.Shared.csproj index 760e8d15f..7a7623193 100644 --- a/src/MiniProfiler.Shared/MiniProfiler.Shared.csproj +++ b/src/MiniProfiler.Shared/MiniProfiler.Shared.csproj @@ -14,7 +14,7 @@ - + diff --git a/src/MiniProfiler.Shared/ui/includes.css b/src/MiniProfiler.Shared/ui/includes.css index 90936eac7..f6c48f69f 100644 --- a/src/MiniProfiler.Shared/ui/includes.css +++ b/src/MiniProfiler.Shared/ui/includes.css @@ -179,6 +179,9 @@ .mp-result table.mp-client-timings { margin-top: 10px; } +.mp-result table.mp-client-timings th:first-child { + text-align: left; +} .mp-result table.mp-client-timings td:nth-child(2) { width: 100%; padding: 0; diff --git a/src/MiniProfiler.Shared/ui/includes.less b/src/MiniProfiler.Shared/ui/includes.less index 150dfebc8..c5820e432 100644 --- a/src/MiniProfiler.Shared/ui/includes.less +++ b/src/MiniProfiler.Shared/ui/includes.less @@ -51,7 +51,7 @@ } .mp-scheme-light { - color-scheme: light; + color-scheme: light; } .mp-scheme-dark { @@ -72,7 +72,6 @@ --mp-highlight-keyword-color: #36a1ef; /* Borders */ --mp-result-border: solid 0.5px #575757; - color-scheme: dark; body { @@ -154,6 +153,10 @@ table.mp-client-timings { margin-top: 10px; + th:first-child { + text-align: left; + } + td:nth-child(2) { width: 100%; padding: 0; diff --git a/src/MiniProfiler.Shared/ui/lib/MiniProfiler.ts b/src/MiniProfiler.Shared/ui/lib/MiniProfiler.ts index f4ee8521f..0e06984ff 100644 --- a/src/MiniProfiler.Shared/ui/lib/MiniProfiler.ts +++ b/src/MiniProfiler.Shared/ui/lib/MiniProfiler.ts @@ -319,11 +319,11 @@ namespace StackExchange.Profiling { // fetch and render results mp.fetchResults(mp.options.ids); - + let lsDisplayValue; try { lsDisplayValue = window.localStorage.getItem('MiniProfiler-Display'); - } catch(e) { } + } catch (e) { } if (lsDisplayValue) { mp.container.style.display = lsDisplayValue; @@ -355,7 +355,7 @@ namespace StackExchange.Profiling { // profiler will be defined in the full page's head window.profiler.Started = new Date('' + window.profiler.Started); // Ugh, JavaScript const profilerHtml = mp.renderProfiler(window.profiler, false); - mp.container.insertAdjacentHTML('beforeend', profilerHtml); + mp.setStylesAndDisplay(profilerHtml); // highight mp.container.querySelectorAll('pre code').forEach(block => mp.highlight(block as HTMLElement)); @@ -728,7 +728,7 @@ namespace StackExchange.Profiling { let str = ` -
${renderDebugInfo(timing)} 0 ? ` style="padding-left:${timing.Depth * 11}px;"` : ''}> + 0 ? ` data-padding-left="${timing.Depth * 11}px"` : ''}> ${encode(timing.Name)} @@ -830,7 +830,7 @@ namespace StackExchange.Profiling { - + @@ -840,7 +840,7 @@ namespace StackExchange.Profiling { ${list.map((t) => ` - + @@ -957,12 +957,7 @@ namespace StackExchange.Profiling { } const profilerHtml = this.renderProfiler(json, true); - - if (this.controls) { - this.controls.insertAdjacentHTML('beforebegin', profilerHtml); - } else { - this.container.insertAdjacentHTML('beforeend', profilerHtml); - } + this.setStylesAndDisplay(profilerHtml); // limit count to maxTracesToShow, remove those before it const results = this.container.querySelectorAll('.mp-result'); @@ -972,6 +967,28 @@ namespace StackExchange.Profiling { } } + private setStylesAndDisplay(profilerHtml: string) { + const nonVisibleElement = document.createElement("div"); + nonVisibleElement.innerHTML = profilerHtml; + + // dynamic padding, margin and width + nonVisibleElement.querySelectorAll("[data-padding-left]").forEach(function (element) { + element.style.paddingLeft = element.dataset.paddingLeft; + }); + nonVisibleElement.querySelectorAll("[data-margin-left]").forEach(function (element) { + element.style.marginLeft = element.dataset.marginLeft; + }); + nonVisibleElement.querySelectorAll("[data-width]").forEach(function (element) { + element.style.width = element.dataset.width; + }); + + if (this.controls) { + this.controls.insertAdjacentElement('beforebegin', nonVisibleElement.firstElementChild); + } else { + this.container.insertAdjacentElement('beforeend', nonVisibleElement.firstElementChild); + } + } + private scrollToQuery = (link: HTMLElement, queries: HTMLElement) => { const id = link.closest('tr').dataset['timingId']; const rows = queries.querySelectorAll('tr[data-timing-id="' + id + '"]'); @@ -1221,7 +1238,7 @@ namespace StackExchange.Profiling { results.style.display = newValue; try { window.localStorage.setItem('MiniProfiler-Display', newValue); - } catch(e) { } + } catch (e) { } } }, false); } diff --git a/src/MiniProfiler/MiniProfilerHandler.cs b/src/MiniProfiler/MiniProfilerHandler.cs index 639266879..ed6ef36cb 100644 --- a/src/MiniProfiler/MiniProfilerHandler.cs +++ b/src/MiniProfiler/MiniProfilerHandler.cs @@ -143,7 +143,7 @@ public void ProcessRequest(HttpContext context) context.Response.ContentType = "text/html; charset=utf-8"; var path = VirtualPathUtility.ToAbsolute(Options.RouteBasePath).EnsureTrailingSlash(); - return Render.ResultListHtml(Options, path); + return Render.ResultListHtml(Options, context, path); } /// @@ -279,7 +279,7 @@ private static string ResultsJson(HttpContext context, MiniProfiler profiler) private string ResultsFullPage(HttpContext context, MiniProfiler profiler) { context.Response.ContentType = "text/html; charset=utf-8"; - return Render.SingleResultHtml(profiler, VirtualPathUtility.ToAbsolute(Options.RouteBasePath).EnsureTrailingSlash()); + return Render.SingleResultHtml(profiler, context, VirtualPathUtility.ToAbsolute(Options.RouteBasePath).EnsureTrailingSlash()); } private bool TryGetResource(string filename, out string resource) diff --git a/tests/MiniProfiler.Tests/RenderTests.cs b/tests/MiniProfiler.Tests/RenderTests.cs index 7aa8bcd77..a88eba0c2 100644 --- a/tests/MiniProfiler.Tests/RenderTests.cs +++ b/tests/MiniProfiler.Tests/RenderTests.cs @@ -15,7 +15,7 @@ public void DefaultRender() { var profiler = GetBasicProfiler(); var renderOptions = new RenderOptions(); - var result = Render.Includes(profiler, "/", true, renderOptions, new List() { profiler.Id }); + var result = Render.Includes(profiler, "/", true, renderOptions, null, new List() { profiler.Id }); Output.WriteLine("Result: " + result); Assert.NotNull(result); @@ -43,7 +43,7 @@ public void OptionsSet() TrivialDurationThresholdMilliseconds = 23, DecimalPlaces = 1, }; - var result = Render.Includes(profiler, "/", true, renderOptions, new List() { profiler.Id }); + var result = Render.Includes(profiler, "/", true, renderOptions, null, new List() { profiler.Id }); Output.WriteLine("Result: " + result); Assert.NotNull(result);
client eventclient event duration (ms) from start (ms)
${encode(t.name)}
${(t.duration >= 0 ? `${duration(t.duration, 0)}` : '')}