Skip to content
Merged
5 changes: 4 additions & 1 deletion src/Umbraco.Core/Services/PreviewService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@ public async Task<bool> TryEnterPreviewAsync(IUser user)

if (attempt.Success)
{
_cookieManager.SetCookieValue(Constants.Web.PreviewCookieName, attempt.Result!, true, true, "None");
// Preview cookies must use SameSite=None and Secure=true to support cross-site scenarios
// (e.g., when the backoffice is on a different domain/port than the frontend during development).
// SameSite=None requires Secure=true per browser specifications.
_cookieManager.SetCookieValue(Constants.Web.PreviewCookieName, attempt.Result!, httpOnly: true, secure: true, sameSiteMode: "None");
}

return attempt.Success;
Expand Down
27 changes: 9 additions & 18 deletions src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,7 @@ public void ExpireCookie(string cookieName)
return;
}

var cookieValue = httpContext.Request.Cookies[cookieName];

httpContext.Response.Cookies.Append(
cookieName,
cookieValue ?? string.Empty,
new CookieOptions
{
Expires = DateTime.Now.AddYears(-1),
});
httpContext.Response.Cookies.Delete(cookieName);
}

/// <inheritdoc/>
Expand All @@ -45,15 +37,14 @@ public void ExpireCookie(string cookieName)
public void SetCookieValue(string cookieName, string value, bool httpOnly, bool secure, string sameSiteMode)
{
SameSiteMode sameSiteModeValue = ParseSameSiteMode(sameSiteMode);
_httpContextAccessor.HttpContext?.Response.Cookies.Append(
cookieName,
value,
new CookieOptions
{
HttpOnly = httpOnly,
SameSite = sameSiteModeValue,
Secure = secure,
});
var options = new CookieOptions
{
HttpOnly = httpOnly,
SameSite = sameSiteModeValue,
Secure = secure,
};

_httpContextAccessor.HttpContext?.Response.Cookies.Append(cookieName, value, options);
}

private static SameSiteMode ParseSameSiteMode(string sameSiteMode) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,8 @@
#documentSegmentRepository = new UmbDocumentSegmentRepository(this);
#actionEventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE;
#localize = new UmbLocalizationController(this);
#previewWindow?: WindowProxy | null = null;
#previewWindowDocumentId?: string | null = null;

constructor(host: UmbControllerHost) {
super(host, {
Expand Down Expand Up @@ -343,7 +345,20 @@
await this.performCreateOrUpdate(variantIds, saveData);
}

// Get the preview URL from the server.
// Check if preview window is still open and showing the same document
// If so, just focus it and let SignalR handle the refresh
try {
if (this.#previewWindow && !this.#previewWindow.closed && this.#previewWindowDocumentId === unique) {

Check warning on line 351 in src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

❌ New issue: Complex Conditional

UmbDocumentWorkspaceContext.handleSaveAndPreview has 1 complex conditionals with 2 branches, threshold = 2. A complex conditional is an expression inside a branch (e.g. if, for, while) which consists of multiple, logical operators such as AND/OR. The more logical operators in an expression, the more severe the code smell.
this.#previewWindow.focus();
return;
}
} catch {
// Window reference is stale, continue to create new preview session
this.#previewWindow = null;
this.#previewWindowDocumentId = null;
}

// Preview not open, create new preview session and open window
const previewRepository = new UmbPreviewRepository(this);
const previewUrlData = await previewRepository.getPreviewUrl(
unique,
Expand All @@ -353,8 +368,12 @@
);

if (previewUrlData.url) {
const previewWindow = window.open(previewUrlData.url, `umbpreview-${unique}`);
previewWindow?.focus();
// Add cache-busting parameter to ensure the preview tab reloads with the new preview session
const previewUrl = new URL(previewUrlData.url, window.document.baseURI);
previewUrl.searchParams.set('rnd', Date.now().toString());
this.#previewWindow = window.open(previewUrl.toString(), `umbpreview-${unique}`);
this.#previewWindowDocumentId = unique;
this.#previewWindow?.focus();
return;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UmbPreviewRepository } from '../repository/index.js';

Check notice on line 1 in src/Umbraco.Web.UI.Client/src/packages/preview/context/preview.context.ts

View check run for this annotation

CodeScene Delta Analysis / CodeScene Code Health Review (main)

✅ Getting better: Overall Code Complexity

The mean cyclomatic complexity decreases from 5.08 to 5.00, threshold = 4. This file has many conditional statements (e.g. if, for, while) across its implementation, leading to lower code health. Avoid adding more conditionals.
import { UMB_PREVIEW_CONTEXT } from './preview.context-token.js';
import { HubConnectionBuilder } from '@umbraco-cms/backoffice/external/signalr';
import { UmbBooleanState, UmbStringState } from '@umbraco-cms/backoffice/observable-api';
Expand Down Expand Up @@ -200,18 +200,15 @@
async exitPreview() {
await this.#previewRepository.exit();

// Stop SignalR connection without waiting - window will close anyway
if (this.#connection) {
await this.#connection.stop();
this.#connection.stop();
this.#connection = undefined;
}

let url = await this.#getPublishedUrl();

if (!url) {
url = this.#previewUrl.getValue() as string;
}

window.location.replace(url);
// Close the preview window
// This ensures that subsequent "Save and Preview" actions will create a new preview session
window.close();
}

iframeLoaded(iframe: HTMLIFrameElement) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,10 @@ export class UmbPreviewEnvironmentsElement extends UmbLitElement {
);

if (previewUrlData.url) {
const previewWindow = window.open(previewUrlData.url, `umbpreview-${this._unique}`);
// Add cache-busting parameter to ensure the preview tab reloads with the new preview session
const previewUrl = new URL(previewUrlData.url, window.document.baseURI);
previewUrl.searchParams.set('rnd', Date.now().toString());
const previewWindow = window.open(previewUrl.toString(), `umbpreview-${this._unique}`);
previewWindow?.focus();
return;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ public void Can_Expire_Cookie()
cookieManager.ExpireCookie(CookieName);

var setCookieHeader = httpContext.Response.Headers.SetCookie.ToString();
Assert.IsTrue(setCookieHeader.StartsWith(GetExpectedCookie()));
Assert.IsTrue(setCookieHeader.StartsWith("testCookie="));
Assert.IsTrue(setCookieHeader.Contains($"expires="));
}

Expand Down
Loading