Skip to content

Commit f1ab605

Browse files
iOvergaardclaudeCopilot
authored andcommitted
Debug mode: Marks UMB-DEBUG cookie as HttpOnly and Secure (#21032)
* fix: sets profiling cookie to httpOnly and strict in order to run non-secure * fix: adds extra message to explain when you can set a cookie * fix: simplify cookie explanation comment in WebProfilerRepository 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com> * fix: checks that the profiler is actually enabled and/or disabled and warns the user if that is not the case * Update src/Umbraco.Web.UI.Client/src/assets/lang/en.ts Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent f4771d1 commit f1ab605

File tree

3 files changed

+107
-24
lines changed

3 files changed

+107
-24
lines changed

src/Umbraco.Web.Common/Repositories/WebProfilerRepository.cs

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
using Microsoft.AspNetCore.Http;
2+
using Microsoft.Extensions.Options;
3+
using Umbraco.Cms.Core.Configuration.Models;
24
using Umbraco.Cms.Core.Persistence.Repositories;
5+
using Umbraco.Cms.Core.Web;
36
using Umbraco.Extensions;
47

58
namespace Umbraco.Cms.Web.Common.Repositories;
@@ -11,21 +14,35 @@ internal sealed class WebProfilerRepository : IWebProfilerRepository
1114
private const string QueryName = "umbDebug";
1215

1316
private readonly IHttpContextAccessor _httpContextAccessor;
17+
private readonly ICookieManager _cookieManager;
18+
private readonly GlobalSettings _globalSettings;
1419

15-
public WebProfilerRepository(IHttpContextAccessor httpContextAccessor)
20+
public WebProfilerRepository(IHttpContextAccessor httpContextAccessor, ICookieManager cookieManager, IOptions<GlobalSettings> globalSettings)
1621
{
1722
_httpContextAccessor = httpContextAccessor;
23+
_cookieManager = cookieManager;
24+
_globalSettings = globalSettings.Value;
1825
}
1926

2027
public void SetStatus(int userId, bool status)
2128
{
2229
if (status)
2330
{
24-
_httpContextAccessor.GetRequiredHttpContext().Response.Cookies.Append(CookieName, "1", new CookieOptions { Expires = DateTime.Now.AddYears(1) });
31+
// This cookie enables debug profiling on the front-end without needing query strings or headers.
32+
// It uses SameSite=Strict, so it only works when the BackOffice and front-end share the same domain.
33+
// It's marked httpOnly to prevent JavaScript access (the server reads it, not client-side code).
34+
// No expiration is set, so it's a session cookie and will be deleted when the browser closes.
35+
// For cross-site setups, use the query string (?umbDebug=true) or header (X-UMB-DEBUG) instead.
36+
_cookieManager.SetCookieValue(
37+
CookieName,
38+
"1",
39+
httpOnly: true,
40+
secure: _globalSettings.UseHttps,
41+
sameSiteMode: "Strict");
2542
}
2643
else
2744
{
28-
_httpContextAccessor.GetRequiredHttpContext().Response.Cookies.Delete(CookieName);
45+
_cookieManager.ExpireCookie(CookieName);
2946
}
3047
}
3148

@@ -43,6 +60,6 @@ public bool GetStatus(int userId)
4360
return xUmbDebug;
4461
}
4562

46-
return request.Cookies.ContainsKey(CookieName);
63+
return _cookieManager.HasCookie(CookieName);
4764
}
4865
}

src/Umbraco.Web.UI.Client/src/assets/lang/en.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2525,13 +2525,19 @@ export default {
25252525
profiling: {
25262526
performanceProfiling: 'Performance profiling',
25272527
performanceProfilingDescription:
2528-
"<p>Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages.</p><p>If you want to activate the profiler for a specific page rendering, simply add <strong>umbDebug=true</strong> to the querystring when requesting the page.</p><p>If you want the profiler to be activated by default for all page renderings, you can use the toggle below. It will set a cookie in your browser, which then activates the profiler automatically. In other words, the profiler will only be active by default in <em>your</em> browser - not everyone else's.</p>",
2528+
"<p>Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages.</p><p>If you want to activate the profiler for a specific page rendering, simply add <strong>umbDebug=true</strong> to the querystring when requesting the page.</p><p>If you want the profiler to be activated by default for all page renderings, you can use the toggle below. It will set a cookie in your browser, which then activates the profiler automatically. In other words, the profiler will only be active by default in <em>your</em> browser - not everyone else's.</p><p><strong>Note:</strong> This will only work if the Backoffice is currently located on the same URL as the front-end website.</p>",
25292529
activateByDefault: 'Activate the profiler by default',
25302530
reminder: 'Friendly reminder',
25312531
reminderDescription:
25322532
'<p>You should never let a production site run in debug mode. Debug mode is turned off by setting <strong>Umbraco:CMS:Hosting:Debug</strong> to <strong>false</strong> in appsettings.json, appsettings.{Environment}.json or via an environment variable.</p>',
25332533
profilerEnabledDescription:
25342534
"<p>Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site.</p><p>Debug mode is turned on by setting <strong>Umbraco:CMS:Hosting:Debug</strong> to <strong>true</strong> in appsettings.json, appsettings.{Environment}.json or via an environment variable.</p>",
2535+
errorEnablingProfilerTitle: 'Error enabling profiler',
2536+
errorEnablingProfilerDescription:
2537+
'It was not possible to enable the profiler. Check that you are accessing the Backoffice on the same URL as the front-end website, and try again. If the problem persists, please check the log for more details.',
2538+
errorDisablingProfilerTitle: 'Error disabling profiler',
2539+
errorDisablingProfilerDescription:
2540+
'It was not possible to disable the profiler. Try again, and if the problem persists, please check the log for more details.',
25352541
},
25362542
settingsDashboardVideos: {
25372543
trainingHeadline: 'Hours of Umbraco training videos are only a click away',

src/Umbraco.Web.UI.Client/src/packages/performance-profiling/dashboard-performance-profiling.element.ts

Lines changed: 79 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
2-
import { css, html, customElement, state, query, unsafeHTML } from '@umbraco-cms/backoffice/external/lit';
2+
import { css, html, customElement, state, query, when } from '@umbraco-cms/backoffice/external/lit';
33
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
44
import { ProfilingService } from '@umbraco-cms/backoffice/external/backend-api';
55
import { tryExecute } from '@umbraco-cms/backoffice/resources';
6+
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
67

78
@customElement('umb-dashboard-performance-profiling')
89
export class UmbDashboardPerformanceProfilingElement extends UmbLitElement {
@@ -13,55 +14,114 @@ export class UmbDashboardPerformanceProfilingElement extends UmbLitElement {
1314
@state()
1415
private _isDebugMode = true;
1516

17+
@state()
18+
private _isLoading = true;
19+
1620
@query('#toggle')
1721
private _toggle!: HTMLInputElement;
1822

23+
private _notificationContext: typeof UMB_NOTIFICATION_CONTEXT.TYPE | undefined;
24+
25+
constructor() {
26+
super();
27+
28+
this.consumeContext(UMB_NOTIFICATION_CONTEXT, (notificationContext) => {
29+
this._notificationContext = notificationContext;
30+
});
31+
}
32+
1933
#setToggle(value: boolean) {
2034
this._toggle.checked = value;
2135
this._profilingStatus = value;
36+
this._isLoading = false;
2237
}
2338

24-
override firstUpdated() {
25-
this._getProfilingStatus();
39+
override async firstUpdated() {
40+
const status = await this.#getProfilingStatus();
41+
this.#setToggle(status);
2642
}
2743

28-
private async _getProfilingStatus() {
44+
async #getProfilingStatus() {
2945
const { data } = await tryExecute(this, ProfilingService.getProfilingStatus());
3046

31-
if (!data) return;
32-
this._profilingStatus = data.enabled ?? false;
47+
return data?.enabled ?? false;
48+
}
49+
50+
async #disableProfilingStatus() {
51+
this._isLoading = true;
52+
const { error } = await tryExecute(this, ProfilingService.putProfilingStatus({ body: { enabled: false } }));
53+
54+
if (error) {
55+
this.#setToggle(true);
56+
return;
57+
}
58+
59+
// Test that it was actually disabled
60+
const status = await this.#getProfilingStatus();
61+
62+
if (status) {
63+
this.#setToggle(true);
64+
this._notificationContext?.peek('warning', {
65+
data: {
66+
headline: this.localize.term('profiling_errorDisablingProfilerTitle'),
67+
message: this.localize.term('profiling_errorDisablingProfilerDescription'),
68+
},
69+
});
70+
return;
71+
}
72+
73+
this.#setToggle(false);
3374
}
3475

35-
private async _changeProfilingStatus() {
36-
const { error } = await tryExecute(
37-
this,
38-
ProfilingService.putProfilingStatus({ body: { enabled: !this._profilingStatus } }),
39-
);
76+
async #enableProfilingStatus() {
77+
this._isLoading = true;
78+
const { error } = await tryExecute(this, ProfilingService.putProfilingStatus({ body: { enabled: true } }));
4079

4180
if (error) {
42-
this.#setToggle(this._profilingStatus);
43-
} else {
44-
this.#setToggle(!this._profilingStatus);
81+
this.#setToggle(false);
82+
return;
4583
}
84+
85+
// Test that it was actually enabled
86+
const status = await this.#getProfilingStatus();
87+
88+
if (!status) {
89+
this.#setToggle(false);
90+
this._notificationContext?.peek('warning', {
91+
data: {
92+
headline: this.localize.term('profiling_errorEnablingProfilerTitle'),
93+
message: this.localize.term('profiling_errorEnablingProfilerDescription'),
94+
},
95+
});
96+
return;
97+
}
98+
99+
this.#setToggle(true);
46100
}
47101

48102
#renderProfilingStatus() {
49103
return this._isDebugMode
50104
? html`
51-
${unsafeHTML(this.localize.term('profiling_performanceProfilingDescription'))}
105+
<umb-localize key="profiling_performanceProfilingDescription"></umb-localize>
52106
53107
<uui-toggle
54108
id="toggle"
55109
label=${this.localize.term('profiling_activateByDefault')}
56110
label-position="left"
57111
?checked="${this._profilingStatus}"
58-
@change="${this._changeProfilingStatus}"></uui-toggle>
112+
?disabled="${this._isLoading}"
113+
@change="${() =>
114+
this._profilingStatus ? this.#disableProfilingStatus() : this.#enableProfilingStatus()}"></uui-toggle>
115+
116+
${when(this._isLoading, () => html`<uui-loader-circle></uui-loader-circle>`)}
59117
60-
<h4>${this.localize.term('profiling_reminder')}</h4>
118+
<h4>
119+
<umb-localize key="profiling_reminder"></umb-localize>
120+
</h4>
61121
62-
${unsafeHTML(this.localize.term('profiling_reminderDescription'))}
122+
<umb-localize key="profiling_reminderDescription"></umb-localize>
63123
`
64-
: html` ${unsafeHTML(this.localize.term('profiling_profilerEnabledDescription'))} `;
124+
: html`<umb-localize key="profiling_profilerEnabledDescription"></umb-localize>`;
65125
}
66126

67127
override render() {

0 commit comments

Comments
 (0)