From 99b0dec9a08eeb92692342b0d83a17001d112a55 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 28 Nov 2025 10:19:44 +0100 Subject: [PATCH 1/9] Fix preview showing published version when Save and Preview is clicked multiple times MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes #20981 When clicking "Save and Preview" multiple times, the preview tab would show the published version instead of the latest saved version. This occurred because: 1. Each "Save and Preview" creates a new preview session with a new token 2. The preview window is reused (via named window target) 3. Without a URL change, the browser doesn't reload and misses the new session token 4. The stale page gets redirected to the published URL Solution: Add a cache-busting parameter (?rnd=timestamp) to the preview URL, forcing the browser to reload and pick up the new preview session token. This aligns with how SignalR refreshes work. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../documents/workspace/document-workspace.context.ts | 5 ++++- .../preview/preview-apps/preview-environments.element.ts | 5 ++++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index 6dadb4ae7c95..d4b8ac1da8dd 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -353,7 +353,10 @@ export class UmbDocumentWorkspaceContext ); if (previewUrlData.url) { - const previewWindow = window.open(previewUrlData.url, `umbpreview-${unique}`); + // Add cache-busting parameter to ensure the preview tab reloads with the new preview session + const previewUrl = new URL(previewUrlData.url, window.location.origin); + previewUrl.searchParams.set('rnd', Date.now().toString()); + const previewWindow = window.open(previewUrl.toString(), `umbpreview-${unique}`); previewWindow?.focus(); return; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts index 8b0bf0452adc..fae83d9a340c 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts @@ -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.location.origin); + previewUrl.searchParams.set('rnd', Date.now().toString()); + const previewWindow = window.open(previewUrl.toString(), `umbpreview-${this._unique}`); previewWindow?.focus(); return; } From debcd8565e14d03f02438c0e1034f079efc50808 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 28 Nov 2025 10:31:03 +0100 Subject: [PATCH 2/9] Improve Save and Preview to avoid full page reloads when preview is already open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When clicking "Save and Preview" multiple times with a preview tab already open, the entire preview tab would reload. This enhancement makes it behave like the "Save" button - only the iframe reloads, not the entire preview wrapper. Changes: - Store reference to preview window when opened - Check if preview window is still open before creating new session - If open, just focus it and let SignalR handle the iframe refresh - If closed, create new preview session and open new window This provides a smoother UX where subsequent saves don't cause the preview frame and controls to reload, only the content iframe refreshes via SignalR. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../workspace/document-workspace.context.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index d4b8ac1da8dd..5c9572a82249 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -72,6 +72,7 @@ export class UmbDocumentWorkspaceContext #documentSegmentRepository = new UmbDocumentSegmentRepository(this); #actionEventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE; #localize = new UmbLocalizationController(this); + #previewWindow: WindowProxy | null = null; constructor(host: UmbControllerHost) { super(host, { @@ -343,7 +344,13 @@ export class UmbDocumentWorkspaceContext await this.performCreateOrUpdate(variantIds, saveData); } - // Get the preview URL from the server. + // Check if preview window is still open - if so, just focus it and let SignalR handle the refresh + if (this.#previewWindow && !this.#previewWindow.closed) { + this.#previewWindow.focus(); + return; + } + + // Preview not open, create new preview session and open window const previewRepository = new UmbPreviewRepository(this); const previewUrlData = await previewRepository.getPreviewUrl( unique, @@ -356,8 +363,8 @@ export class UmbDocumentWorkspaceContext // Add cache-busting parameter to ensure the preview tab reloads with the new preview session const previewUrl = new URL(previewUrlData.url, window.location.origin); previewUrl.searchParams.set('rnd', Date.now().toString()); - const previewWindow = window.open(previewUrl.toString(), `umbpreview-${unique}`); - previewWindow?.focus(); + this.#previewWindow = window.open(previewUrl.toString(), `umbpreview-${unique}`); + this.#previewWindow?.focus(); return; } From cbd616077a9ffe78125165b5bdd9ac7a3d5b6004 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 28 Nov 2025 11:23:21 +0100 Subject: [PATCH 3/9] Close preview window when ending preview session MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Changes the "End Preview" behavior to close the preview tab instead of navigating to the published URL. This provides a cleaner UX and ensures subsequent "Save and Preview" actions will always create a fresh preview session. Benefits: - Eliminates edge case where preview window remains open but is no longer in preview mode - Simpler behavior - preview session ends and window closes - Users can use "Preview website" button if they want to view published page Also removes unnecessary await on SignalR connection.stop() to prevent blocking if the connection cleanup hangs. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/packages/preview/context/preview.context.ts | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/preview/context/preview.context.ts b/src/Umbraco.Web.UI.Client/src/packages/preview/context/preview.context.ts index de4b0267c76d..7e315c3a31e6 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/preview/context/preview.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/preview/context/preview.context.ts @@ -200,18 +200,15 @@ export class UmbPreviewContext extends UmbContextBase { 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) { From 9d5b765c0d497b43f367eb1f92d46161920f2929 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 28 Nov 2025 12:01:32 +0100 Subject: [PATCH 4/9] Fix preview cookie expiration and add proper error handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit addresses cookie management issues in the preview system: 1. **Cookie Expiration API Enhancement** - Added `ExpireCookie` overload with security parameters (httpOnly, secure, sameSiteMode) - Added `SetCookieValue` overload with optional expires parameter - Marked old methods as obsolete for removal in Umbraco 19 - Ensures cookies are expired with matching security attributes 2. **PreviewService Cookie Handling** - Changed to use new `ExpireCookie` method with explicit security attributes - Maintains `Secure=true` and `SameSite=None` for cross-site scenarios - Uses new `SetCookieValue` overload with explicit expires parameter - Properly expires preview cookies when ending preview session 3. **Frontend Error Handling** - Added try-catch around preview window reference checks - Handles stale window references gracefully - Prevents potential errors from accessing closed window properties These changes ensure preview cookies are properly managed throughout their lifecycle and support both same-site and cross-site scenarios (e.g., when the backoffice is on a different domain/port during development). Fixes #20981 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- src/Umbraco.Core/Services/PreviewService.cs | 13 +++++-- src/Umbraco.Core/Web/ICookieManager.cs | 34 +++++++++++++++++++ .../AspNetCore/AspNetCoreCookieManager.cs | 32 ++++++++++++----- .../workspace/document-workspace.context.ts | 11 ++++-- 4 files changed, 76 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Services/PreviewService.cs b/src/Umbraco.Core/Services/PreviewService.cs index 9810d8f07e19..2a8a1b18c9ac 100644 --- a/src/Umbraco.Core/Services/PreviewService.cs +++ b/src/Umbraco.Core/Services/PreviewService.cs @@ -34,7 +34,10 @@ public async Task 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", expires: null); } return attempt.Success; @@ -42,7 +45,13 @@ public async Task TryEnterPreviewAsync(IUser user) public Task EndPreviewAsync() { - _cookieManager.ExpireCookie(Constants.Web.PreviewCookieName); + // Expire the cookie with the same attributes used when creating it + // This ensures the browser properly removes the cookie + _cookieManager.ExpireCookie( + Constants.Web.PreviewCookieName, + httpOnly: true, + secure: true, + sameSiteMode: "None"); return Task.CompletedTask; } diff --git a/src/Umbraco.Core/Web/ICookieManager.cs b/src/Umbraco.Core/Web/ICookieManager.cs index 6eaf2aafd9f1..b3dae67e5cef 100644 --- a/src/Umbraco.Core/Web/ICookieManager.cs +++ b/src/Umbraco.Core/Web/ICookieManager.cs @@ -9,8 +9,24 @@ public interface ICookieManager /// Expires the cookie with the specified name. /// /// The cookie name. + [Obsolete("Please use the overload that accepts httpOnly, secure and sameSiteMode parameters. This will be removed in Umbraco 19.")] void ExpireCookie(string cookieName); + /// + /// Expires the cookie with the specified name and security attributes. + /// + /// The cookie name. + /// Indicates whether the cookie should be marked as HTTP only. + /// Indicates whether the cookie should be marked as secure. + /// Indicates the cookie's same site status. + /// + /// The value provided by should match the enum values available from + /// Microsoft.AspNetCore.Http.SameSiteMode. + /// This hasn't been used as the parameter directly to avoid a dependency on Microsoft.AspNetCore.Http in + /// the core project. + /// + void ExpireCookie(string cookieName, bool httpOnly, bool secure, string sameSiteMode); + /// /// Gets the value of the cookie with the specified name. /// @@ -31,8 +47,26 @@ public interface ICookieManager /// This hasn't been used as the parameter directly to avoid a dependency on Microsoft.AspNetCore.Http in /// the core project. /// + [Obsolete("Please use the overload that accepts an expires parameter. This will be removed in Umbraco 19.")] void SetCookieValue(string cookieName, string value, bool httpOnly, bool secure, string sameSiteMode); + /// + /// Sets the value of a cookie with the specified name and expiration. + /// + /// The cookie name. + /// The cookie value. + /// Indicates whether the created cookie should be marked as HTTP only. + /// Indicates whether the created cookie should be marked as secure. + /// Indicates the created cookie's same site status. + /// Optional expiration date for the cookie. If null, the cookie is a session cookie. + /// + /// The value provided by should match the enum values available from + /// Microsoft.AspNetCore.Http.SameSiteMode. + /// This hasn't been used as the parameter directly to avoid a dependency on Microsoft.AspNetCore.Http in + /// the core project. + /// + void SetCookieValue(string cookieName, string value, bool httpOnly, bool secure, string sameSiteMode, DateTimeOffset? expires); + /// /// Determines whether a cookie with the specified name exists. /// diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs index 854699cd29d1..f0b36adc9f2b 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs @@ -18,6 +18,7 @@ public AspNetCoreCookieManager(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; /// + [Obsolete("Please use the overload that accepts httpOnly, secure and sameSiteMode parameters. This will be removed in Umbraco 19.")] public void ExpireCookie(string cookieName) { HttpContext? httpContext = _httpContextAccessor.HttpContext; @@ -38,22 +39,35 @@ public void ExpireCookie(string cookieName) }); } + /// + public void ExpireCookie(string cookieName, bool httpOnly, bool secure, string sameSiteMode) + => SetCookieValue(cookieName, string.Empty, httpOnly, secure, sameSiteMode, DateTimeOffset.Now.AddYears(-1)); + /// public string? GetCookieValue(string cookieName) => _httpContextAccessor.HttpContext?.Request.Cookies[cookieName]; /// + [Obsolete("Please use the overload that accepts an expires parameter. This will be removed in Umbraco 19.")] public void SetCookieValue(string cookieName, string value, bool httpOnly, bool secure, string sameSiteMode) + => SetCookieValue(cookieName, value, httpOnly, secure, sameSiteMode, null); + + /// + public void SetCookieValue(string cookieName, string value, bool httpOnly, bool secure, string sameSiteMode, DateTimeOffset? expires) { 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, + }; + + if (expires.HasValue) + { + options.Expires = expires.Value; + } + + _httpContextAccessor.HttpContext?.Response.Cookies.Append(cookieName, value, options); } private static SameSiteMode ParseSameSiteMode(string sameSiteMode) => diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index 5c9572a82249..271a4dadf573 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -345,9 +345,14 @@ export class UmbDocumentWorkspaceContext } // Check if preview window is still open - if so, just focus it and let SignalR handle the refresh - if (this.#previewWindow && !this.#previewWindow.closed) { - this.#previewWindow.focus(); - return; + try { + if (this.#previewWindow && !this.#previewWindow.closed) { + this.#previewWindow.focus(); + return; + } + } catch { + // Window reference is stale, continue to create new preview session + this.#previewWindow = null; } // Preview not open, create new preview session and open window From 84f1fd5344881ae2d570ff44edd0c84a8e249695 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Fri, 28 Nov 2025 12:08:06 +0100 Subject: [PATCH 5/9] Track document ID for preview window to prevent reusing window across different documents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When navigating from one document to another in the backoffice, the preview window reference was being reused even though it was showing a different document. This meant clicking "Save and Preview" would just focus the existing window without updating it to show the new document. Now we track which document the preview window is showing and only reuse the window if: 1. The window is still open 2. The window is showing the same document This ensures each document gets its own preview session while still avoiding unnecessary full page reloads when repeatedly previewing the same document. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../documents/workspace/document-workspace.context.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index 271a4dadf573..a20a3491aebc 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -72,7 +72,8 @@ export class UmbDocumentWorkspaceContext #documentSegmentRepository = new UmbDocumentSegmentRepository(this); #actionEventContext?: typeof UMB_ACTION_EVENT_CONTEXT.TYPE; #localize = new UmbLocalizationController(this); - #previewWindow: WindowProxy | null = null; + #previewWindow?: WindowProxy | null = null; + #previewWindowDocumentId?: string | null = null; constructor(host: UmbControllerHost) { super(host, { @@ -344,15 +345,17 @@ export class UmbDocumentWorkspaceContext await this.performCreateOrUpdate(variantIds, saveData); } - // Check if preview window is still open - if so, just focus it and let SignalR handle the refresh + // 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) { + if (this.#previewWindow && !this.#previewWindow.closed && this.#previewWindowDocumentId === unique) { 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 @@ -369,6 +372,7 @@ export class UmbDocumentWorkspaceContext const previewUrl = new URL(previewUrlData.url, window.location.origin); previewUrl.searchParams.set('rnd', Date.now().toString()); this.#previewWindow = window.open(previewUrl.toString(), `umbpreview-${unique}`); + this.#previewWindowDocumentId = unique; this.#previewWindow?.focus(); return; } From 9dc19e58ed2be599a02dc3bfda1a04277bf9ec58 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 28 Nov 2025 15:12:50 +0100 Subject: [PATCH 6/9] Remove updates to ICookieManager and use Cookies.Delete to remove cookie. --- src/Umbraco.Core/Services/PreviewService.cs | 8 +------- src/Umbraco.Core/Web/ICookieManager.cs | 16 ---------------- .../AspNetCore/AspNetCoreCookieManager.cs | 15 +-------------- 3 files changed, 2 insertions(+), 37 deletions(-) diff --git a/src/Umbraco.Core/Services/PreviewService.cs b/src/Umbraco.Core/Services/PreviewService.cs index 2a8a1b18c9ac..b774b741a288 100644 --- a/src/Umbraco.Core/Services/PreviewService.cs +++ b/src/Umbraco.Core/Services/PreviewService.cs @@ -45,13 +45,7 @@ public async Task TryEnterPreviewAsync(IUser user) public Task EndPreviewAsync() { - // Expire the cookie with the same attributes used when creating it - // This ensures the browser properly removes the cookie - _cookieManager.ExpireCookie( - Constants.Web.PreviewCookieName, - httpOnly: true, - secure: true, - sameSiteMode: "None"); + _cookieManager.ExpireCookie(Constants.Web.PreviewCookieName); return Task.CompletedTask; } diff --git a/src/Umbraco.Core/Web/ICookieManager.cs b/src/Umbraco.Core/Web/ICookieManager.cs index b3dae67e5cef..51d782e946ad 100644 --- a/src/Umbraco.Core/Web/ICookieManager.cs +++ b/src/Umbraco.Core/Web/ICookieManager.cs @@ -9,24 +9,8 @@ public interface ICookieManager /// Expires the cookie with the specified name. /// /// The cookie name. - [Obsolete("Please use the overload that accepts httpOnly, secure and sameSiteMode parameters. This will be removed in Umbraco 19.")] void ExpireCookie(string cookieName); - /// - /// Expires the cookie with the specified name and security attributes. - /// - /// The cookie name. - /// Indicates whether the cookie should be marked as HTTP only. - /// Indicates whether the cookie should be marked as secure. - /// Indicates the cookie's same site status. - /// - /// The value provided by should match the enum values available from - /// Microsoft.AspNetCore.Http.SameSiteMode. - /// This hasn't been used as the parameter directly to avoid a dependency on Microsoft.AspNetCore.Http in - /// the core project. - /// - void ExpireCookie(string cookieName, bool httpOnly, bool secure, string sameSiteMode); - /// /// Gets the value of the cookie with the specified name. /// diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs index f0b36adc9f2b..5ccae53c7516 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs @@ -18,7 +18,6 @@ public AspNetCoreCookieManager(IHttpContextAccessor httpContextAccessor) => _httpContextAccessor = httpContextAccessor; /// - [Obsolete("Please use the overload that accepts httpOnly, secure and sameSiteMode parameters. This will be removed in Umbraco 19.")] public void ExpireCookie(string cookieName) { HttpContext? httpContext = _httpContextAccessor.HttpContext; @@ -28,21 +27,9 @@ 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); } - /// - public void ExpireCookie(string cookieName, bool httpOnly, bool secure, string sameSiteMode) - => SetCookieValue(cookieName, string.Empty, httpOnly, secure, sameSiteMode, DateTimeOffset.Now.AddYears(-1)); - /// public string? GetCookieValue(string cookieName) => _httpContextAccessor.HttpContext?.Request.Cookies[cookieName]; From 44a1ffbf2c8737ba28c3161b3d4a2de5ea96067d Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 28 Nov 2025 15:54:07 +0100 Subject: [PATCH 7/9] Fix file not found on click to save and preview. --- .../documents/documents/workspace/document-workspace.context.ts | 2 +- .../preview/preview-apps/preview-environments.element.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index a20a3491aebc..e29677615d1a 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -369,7 +369,7 @@ export class UmbDocumentWorkspaceContext if (previewUrlData.url) { // Add cache-busting parameter to ensure the preview tab reloads with the new preview session - const previewUrl = new URL(previewUrlData.url, window.location.origin); + 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; diff --git a/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts index fae83d9a340c..5cb43b2371a7 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/preview/preview-apps/preview-environments.element.ts @@ -79,7 +79,7 @@ export class UmbPreviewEnvironmentsElement extends UmbLitElement { if (previewUrlData.url) { // Add cache-busting parameter to ensure the preview tab reloads with the new preview session - const previewUrl = new URL(previewUrlData.url, window.location.origin); + 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(); From 350fb64c1116bd6cd632edf60603281cb213d7ca Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 28 Nov 2025 15:59:44 +0100 Subject: [PATCH 8/9] Removed further currently unnecessary updates to the cookie manager interface and implementation. --- src/Umbraco.Core/Services/PreviewService.cs | 2 +- src/Umbraco.Core/Web/ICookieManager.cs | 18 ------------------ .../AspNetCore/AspNetCoreCookieManager.cs | 10 ---------- 3 files changed, 1 insertion(+), 29 deletions(-) diff --git a/src/Umbraco.Core/Services/PreviewService.cs b/src/Umbraco.Core/Services/PreviewService.cs index b774b741a288..43e24f0e07f7 100644 --- a/src/Umbraco.Core/Services/PreviewService.cs +++ b/src/Umbraco.Core/Services/PreviewService.cs @@ -37,7 +37,7 @@ public async Task TryEnterPreviewAsync(IUser user) // 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", expires: null); + _cookieManager.SetCookieValue(Constants.Web.PreviewCookieName, attempt.Result!, httpOnly: true, secure: true, sameSiteMode: "None"); } return attempt.Success; diff --git a/src/Umbraco.Core/Web/ICookieManager.cs b/src/Umbraco.Core/Web/ICookieManager.cs index 51d782e946ad..6eaf2aafd9f1 100644 --- a/src/Umbraco.Core/Web/ICookieManager.cs +++ b/src/Umbraco.Core/Web/ICookieManager.cs @@ -31,26 +31,8 @@ public interface ICookieManager /// This hasn't been used as the parameter directly to avoid a dependency on Microsoft.AspNetCore.Http in /// the core project. /// - [Obsolete("Please use the overload that accepts an expires parameter. This will be removed in Umbraco 19.")] void SetCookieValue(string cookieName, string value, bool httpOnly, bool secure, string sameSiteMode); - /// - /// Sets the value of a cookie with the specified name and expiration. - /// - /// The cookie name. - /// The cookie value. - /// Indicates whether the created cookie should be marked as HTTP only. - /// Indicates whether the created cookie should be marked as secure. - /// Indicates the created cookie's same site status. - /// Optional expiration date for the cookie. If null, the cookie is a session cookie. - /// - /// The value provided by should match the enum values available from - /// Microsoft.AspNetCore.Http.SameSiteMode. - /// This hasn't been used as the parameter directly to avoid a dependency on Microsoft.AspNetCore.Http in - /// the core project. - /// - void SetCookieValue(string cookieName, string value, bool httpOnly, bool secure, string sameSiteMode, DateTimeOffset? expires); - /// /// Determines whether a cookie with the specified name exists. /// diff --git a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs index 5ccae53c7516..0d7aad9d4978 100644 --- a/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs +++ b/src/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManager.cs @@ -34,12 +34,7 @@ public void ExpireCookie(string cookieName) public string? GetCookieValue(string cookieName) => _httpContextAccessor.HttpContext?.Request.Cookies[cookieName]; /// - [Obsolete("Please use the overload that accepts an expires parameter. This will be removed in Umbraco 19.")] public void SetCookieValue(string cookieName, string value, bool httpOnly, bool secure, string sameSiteMode) - => SetCookieValue(cookieName, value, httpOnly, secure, sameSiteMode, null); - - /// - public void SetCookieValue(string cookieName, string value, bool httpOnly, bool secure, string sameSiteMode, DateTimeOffset? expires) { SameSiteMode sameSiteModeValue = ParseSameSiteMode(sameSiteMode); var options = new CookieOptions @@ -49,11 +44,6 @@ public void SetCookieValue(string cookieName, string value, bool httpOnly, bool Secure = secure, }; - if (expires.HasValue) - { - options.Expires = expires.Value; - } - _httpContextAccessor.HttpContext?.Response.Cookies.Append(cookieName, value, options); } From 65ac7a158997278e1abe5b40b9bdd8787067a2c7 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 28 Nov 2025 16:43:52 +0100 Subject: [PATCH 9/9] Fixed failing unit test. --- .../AspNetCore/AspNetCoreCookieManagerTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManagerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManagerTests.cs index e367870a50df..58789979c5eb 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManagerTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/AspNetCore/AspNetCoreCookieManagerTests.cs @@ -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=")); }