Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
25 changes: 21 additions & 4 deletions src/Umbraco.Web.Common/Repositories/WebProfilerRepository.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Web;
using Umbraco.Extensions;

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

private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ICookieManager _cookieManager;
private readonly GlobalSettings _globalSettings;

public WebProfilerRepository(IHttpContextAccessor httpContextAccessor)
public WebProfilerRepository(IHttpContextAccessor httpContextAccessor, ICookieManager cookieManager, IOptions<GlobalSettings> globalSettings)
{
_httpContextAccessor = httpContextAccessor;
_cookieManager = cookieManager;
_globalSettings = globalSettings.Value;
}

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

Expand All @@ -43,6 +60,6 @@ public bool GetStatus(int userId)
return xUmbDebug;
}

return request.Cookies.ContainsKey(CookieName);
return _cookieManager.HasCookie(CookieName);
}
}
8 changes: 7 additions & 1 deletion src/Umbraco.Web.UI.Client/src/assets/lang/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2550,13 +2550,19 @@ export default {
profiling: {
performanceProfiling: 'Performance profiling',
performanceProfilingDescription:
"<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>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>",
activateByDefault: 'Activate the profiler by default',
reminder: 'Friendly reminder',
reminderDescription:
'<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>',
profilerEnabledDescription:
"<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>",
errorEnablingProfilerTitle: 'Error enabling profiler',
errorEnablingProfilerDescription:
'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.',
errorDisablingProfilerTitle: 'Error disabling profiler',
errorDisablingProfilerDescription:
'It was not possible to disable the profiler. Try again, and if the problem persists, please check the log for more details.',
},
settingsDashboard: {
documentationHeader: 'Documentation',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
import { css, html, customElement, state, query, unsafeHTML } from '@umbraco-cms/backoffice/external/lit';
import { css, html, customElement, state, query, when } from '@umbraco-cms/backoffice/external/lit';
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
import { ProfilingService } from '@umbraco-cms/backoffice/external/backend-api';
import { tryExecute } from '@umbraco-cms/backoffice/resources';
import { consumeContext } from '@umbraco-cms/backoffice/context-api';
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';

@customElement('umb-dashboard-performance-profiling')
export class UmbDashboardPerformanceProfilingElement extends UmbLitElement {
Expand All @@ -13,55 +15,107 @@
@state()
private _isDebugMode = true;

@state()
private _isLoading = true;

@query('#toggle')
private _toggle!: HTMLInputElement;

@consumeContext({ context: UMB_NOTIFICATION_CONTEXT })
private _notificationContext: typeof UMB_NOTIFICATION_CONTEXT.TYPE | undefined;

#setToggle(value: boolean) {
this._toggle.checked = value;
this._profilingStatus = value;
this._isLoading = false;
}

override firstUpdated() {
this._getProfilingStatus();
override async firstUpdated() {
const status = await this.#getProfilingStatus();
this.#setToggle(status);
}

private async _getProfilingStatus() {
async #getProfilingStatus() {
const { data } = await tryExecute(this, ProfilingService.getProfilingStatus());

if (!data) return;
this._profilingStatus = data.enabled ?? false;
return data?.enabled ?? false;
}

private async _changeProfilingStatus() {
const { error } = await tryExecute(
this,
ProfilingService.putProfilingStatus({ body: { enabled: !this._profilingStatus } }),
);
async #disableProfilingStatus() {
this._isLoading = true;
const { error } = await tryExecute(this, ProfilingService.putProfilingStatus({ body: { enabled: false } }));

if (error) {
this.#setToggle(this._profilingStatus);
} else {
this.#setToggle(!this._profilingStatus);
this.#setToggle(true);
return;
}

// Test that it was actually disabled
const status = await this.#getProfilingStatus();

if (status) {
this.#setToggle(true);
this._notificationContext?.peek('warning', {
data: {
headline: this.localize.term('profiling_errorDisablingProfilerTitle'),
message: this.localize.term('profiling_errorDisablingProfilerDescription'),
},
});
return;
}

this.#setToggle(false);
}

Check warning on line 68 in src/Umbraco.Web.UI.Client/src/packages/performance-profiling/dashboard-performance-profiling.element.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Code Duplication

The module contains 2 functions with similar structure: UmbDashboardPerformanceProfilingElement.disableProfilingStatus,UmbDashboardPerformanceProfilingElement.enableProfilingStatus. Avoid duplicated, aka copy-pasted, code inside the module. More duplication lowers the code health.

async #enableProfilingStatus() {
this._isLoading = true;
const { error } = await tryExecute(this, ProfilingService.putProfilingStatus({ body: { enabled: true } }));

if (error) {
this.#setToggle(false);
return;
}

// Test that it was actually enabled
const status = await this.#getProfilingStatus();

if (!status) {
this.#setToggle(false);
this._notificationContext?.peek('warning', {
data: {
headline: this.localize.term('profiling_errorEnablingProfilerTitle'),
message: this.localize.term('profiling_errorEnablingProfilerDescription'),
},
});
return;
}

this.#setToggle(true);
}

#renderProfilingStatus() {
return this._isDebugMode
? html`
${unsafeHTML(this.localize.term('profiling_performanceProfilingDescription'))}
<umb-localize key="profiling_performanceProfilingDescription"></umb-localize>

<uui-toggle
id="toggle"
label=${this.localize.term('profiling_activateByDefault')}
label-position="left"
?checked="${this._profilingStatus}"
@change="${this._changeProfilingStatus}"></uui-toggle>
?disabled="${this._isLoading}"
@change="${() =>
this._profilingStatus ? this.#disableProfilingStatus() : this.#enableProfilingStatus()}"></uui-toggle>

${when(this._isLoading, () => html`<uui-loader-circle></uui-loader-circle>`)}

<h4>${this.localize.term('profiling_reminder')}</h4>
<h4>
<umb-localize key="profiling_reminder"></umb-localize>
</h4>

${unsafeHTML(this.localize.term('profiling_reminderDescription'))}
<umb-localize key="profiling_reminderDescription"></umb-localize>
`
: html` ${unsafeHTML(this.localize.term('profiling_profilerEnabledDescription'))} `;
: html`<umb-localize key="profiling_profilerEnabledDescription"></umb-localize>`;
}

override render() {
Expand Down
Loading