Compare commits
13
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dcbbed4160 | ||
|
|
e94e165593 | ||
|
|
34709be6cc | ||
|
|
a62fa93c77 | ||
|
|
ab31fbb0aa | ||
|
|
a486d5df33 | ||
|
|
0e0aca55af | ||
|
|
3e9ff6b5cb | ||
|
|
fdca086a47 | ||
|
|
42a81beeac | ||
|
|
9284b9e0b1 | ||
|
|
5570583f70 | ||
|
|
eb979625d1 |
@@ -35,7 +35,7 @@
|
||||
<EnableStrictModeForCompatibleTfms>true</EnableStrictModeForCompatibleTfms>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Calculate version only once for the whole repository -->
|
||||
<!-- Calculate version only once for the whole repository -->
|
||||
<PropertyGroup>
|
||||
<GitVersionBaseDirectory>$(MSBuildThisFileDirectory)</GitVersionBaseDirectory>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -338,7 +338,9 @@ stages:
|
||||
# Integration Tests (SQL Server)
|
||||
- job:
|
||||
timeoutInMinutes: 120
|
||||
condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerIntegrationTests}})
|
||||
# We are currently encountering issues when running SQL Server Linux tests Microsoft.Data.SqlClient.SqlException (0x80131904)
|
||||
# condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerIntegrationTests}})
|
||||
condition: eq(${{parameters.sqlServerIntegrationTests}}, True)
|
||||
displayName: Integration Tests (SQL Server)
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -745,6 +747,8 @@ stages:
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}}))
|
||||
jobs:
|
||||
- job:
|
||||
pool:
|
||||
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
|
||||
displayName: Push to pre-release feed
|
||||
steps:
|
||||
- checkout: none
|
||||
@@ -771,6 +775,8 @@ stages:
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}}))
|
||||
jobs:
|
||||
- job:
|
||||
pool:
|
||||
vmImage: "windows-latest" # NuGetCommand@2 is no longer supported on Ubuntu 24.04 so we'll use windows until an alternative is available.
|
||||
displayName: Push to NuGet
|
||||
steps:
|
||||
- checkout: none
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
@@ -11,5 +11,5 @@ internal sealed class RequestPreviewService : RequestHeaderHandler, IRequestPrev
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool IsPreview() => GetHeaderValue("Preview") == "true";
|
||||
public bool IsPreview() => string.Equals(GetHeaderValue("Preview"), "true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Umbraco.Cms.Core.Configuration.Models;
|
||||
|
||||
@@ -27,6 +28,8 @@ public class SecuritySettings
|
||||
|
||||
internal const int StaticMemberDefaultLockoutTimeInMinutes = 30 * 24 * 60;
|
||||
internal const int StaticUserDefaultLockoutTimeInMinutes = 30 * 24 * 60;
|
||||
private const long StaticUserDefaultFailedLoginDurationInMilliseconds = 1000;
|
||||
private const long StaticUserMinimumFailedLoginDurationInMilliseconds = 250;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether to keep the user logged in.
|
||||
@@ -125,4 +128,26 @@ public class SecuritySettings
|
||||
/// </summary>
|
||||
[DefaultValue(StaticAllowConcurrentLogins)]
|
||||
public bool AllowConcurrentLogins { get; set; } = StaticAllowConcurrentLogins;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default duration (in milliseconds) of failed login attempts.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The default duration (in milliseconds) of failed login attempts.
|
||||
/// </value>
|
||||
/// <remarks>
|
||||
/// The user login endpoint ensures that failed login attempts take at least as long as the average successful login.
|
||||
/// However, if no successful logins have occurred, this value is used as the default duration.
|
||||
/// </remarks>
|
||||
[DefaultValue(StaticUserDefaultFailedLoginDurationInMilliseconds)]
|
||||
public long UserDefaultFailedLoginDurationInMilliseconds { get; set; } = StaticUserDefaultFailedLoginDurationInMilliseconds;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the minimum duration (in milliseconds) of failed login attempts.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The minimum duration (in milliseconds) of failed login attempts.
|
||||
/// </value>
|
||||
[DefaultValue(StaticUserMinimumFailedLoginDurationInMilliseconds)]
|
||||
public long UserMinimumFailedLoginDurationInMilliseconds { get; set; } = StaticUserMinimumFailedLoginDurationInMilliseconds;
|
||||
}
|
||||
|
||||
@@ -358,7 +358,7 @@ namespace Umbraco.Cms.Core.IO
|
||||
|
||||
// nothing prevents us to reach the file, security-wise, yet it is outside
|
||||
// this filesystem's root - throw
|
||||
throw new UnauthorizedAccessException($"File original: [{originalPath}] full: [{path}] is outside this filesystem's root.");
|
||||
throw new UnauthorizedAccessException($"Requested path {originalPath} is outside this filesystem's root.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
namespace Umbraco.Cms.Core;
|
||||
|
||||
/// <summary>
|
||||
/// Makes a code block timed (take at least a certain amount of time). This class cannot be inherited.
|
||||
/// </summary>
|
||||
public sealed class TimedScope : IDisposable, IAsyncDisposable
|
||||
{
|
||||
private readonly TimeSpan _duration;
|
||||
private readonly TimeProvider _timeProvider;
|
||||
private readonly CancellationTokenSource _cancellationTokenSource;
|
||||
private readonly long _startingTimestamp;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the elapsed time.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The elapsed time.
|
||||
/// </value>
|
||||
public TimeSpan Elapsed
|
||||
=> _timeProvider.GetElapsedTime(_startingTimestamp);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the remaining time.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The remaining time.
|
||||
/// </value>
|
||||
public TimeSpan Remaining
|
||||
=> TryGetRemaining(out TimeSpan remaining) ? remaining : TimeSpan.Zero;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
|
||||
public TimedScope(long millisecondsDuration)
|
||||
: this(TimeSpan.FromMilliseconds(millisecondsDuration))
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public TimedScope(long millisecondsDuration, CancellationToken cancellationToken)
|
||||
: this(TimeSpan.FromMilliseconds(millisecondsDuration), cancellationToken)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
|
||||
/// <param name="timeProvider">The time provider.</param>
|
||||
public TimedScope(long millisecondsDuration, TimeProvider timeProvider)
|
||||
: this(TimeSpan.FromMilliseconds(millisecondsDuration), timeProvider)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="millisecondsDuration">The number of milliseconds the scope should at least take.</param>
|
||||
/// <param name="timeProvider">The time provider.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public TimedScope(long millisecondsDuration, TimeProvider timeProvider, CancellationToken cancellationToken)
|
||||
: this(TimeSpan.FromMilliseconds(millisecondsDuration), timeProvider, cancellationToken)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope"/> class.
|
||||
/// </summary>
|
||||
/// <param name="duration">The duration the scope should at least take.</param>
|
||||
public TimedScope(TimeSpan duration)
|
||||
: this(duration, TimeProvider.System)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="duration">The duration the scope should at least take.</param>
|
||||
/// <param name="timeProvider">The time provider.</param>
|
||||
public TimedScope(TimeSpan duration, TimeProvider timeProvider)
|
||||
: this(duration, timeProvider, new CancellationTokenSource())
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="duration">The duration the scope should at least take.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public TimedScope(TimeSpan duration, CancellationToken cancellationToken)
|
||||
: this(duration, TimeProvider.System, cancellationToken)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="TimedScope" /> class.
|
||||
/// </summary>
|
||||
/// <param name="duration">The duration the scope should at least take.</param>
|
||||
/// <param name="timeProvider">The time provider.</param>
|
||||
/// <param name="cancellationToken">The cancellation token.</param>
|
||||
public TimedScope(TimeSpan duration, TimeProvider timeProvider, CancellationToken cancellationToken)
|
||||
: this(duration, timeProvider, CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{ }
|
||||
|
||||
private TimedScope(TimeSpan duration, TimeProvider timeProvider, CancellationTokenSource cancellationTokenSource)
|
||||
{
|
||||
_duration = duration;
|
||||
_timeProvider = timeProvider;
|
||||
_cancellationTokenSource = cancellationTokenSource;
|
||||
_startingTimestamp = timeProvider.GetTimestamp();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the timed scope.
|
||||
/// </summary>
|
||||
public void Cancel()
|
||||
=> _cancellationTokenSource.Cancel();
|
||||
|
||||
/// <summary>
|
||||
/// Cancels the timed scope asynchronously.
|
||||
/// </summary>
|
||||
public async Task CancelAsync()
|
||||
=> await _cancellationTokenSource.CancelAsync().ConfigureAwait(false);
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This will block using <see cref="Thread.Sleep(TimeSpan)" /> until the remaining time has elapsed, if not cancelled.
|
||||
/// </remarks>
|
||||
public void Dispose()
|
||||
{
|
||||
if (_cancellationTokenSource.IsCancellationRequested is false &&
|
||||
TryGetRemaining(out TimeSpan remaining))
|
||||
{
|
||||
Thread.Sleep(remaining);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources asynchronously.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// A task that represents the asynchronous dispose operation.
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This will delay using <see cref="Task.Delay(TimeSpan, TimeProvider, CancellationToken)" /> until the remaining time has elapsed, if not cancelled.
|
||||
/// </remarks>
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_cancellationTokenSource.IsCancellationRequested is false &&
|
||||
TryGetRemaining(out TimeSpan remaining))
|
||||
{
|
||||
await Task.Delay(remaining, _timeProvider, _cancellationTokenSource.Token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryGetRemaining(out TimeSpan remaining)
|
||||
{
|
||||
remaining = _duration.Subtract(Elapsed);
|
||||
|
||||
return remaining > TimeSpan.Zero;
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,9 @@ public class AuthenticationController : UmbracoApiControllerBase
|
||||
private readonly IUserService _userService;
|
||||
private readonly WebRoutingSettings _webRoutingSettings;
|
||||
|
||||
private const int FailedLoginDurationRandomOffsetInMilliseconds = 100;
|
||||
private static long? _loginDurationAverage;
|
||||
|
||||
// TODO: We need to review all _userManager.Raise calls since many/most should be on the usermanager or signinmanager, very few should be here
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public AuthenticationController(
|
||||
@@ -415,42 +418,78 @@ public class AuthenticationController : UmbracoApiControllerBase
|
||||
[Authorize(Policy = AuthorizationPolicies.DenyLocalLoginIfConfigured)]
|
||||
public async Task<ActionResult<UserDetail?>> PostLogin(LoginModel loginModel)
|
||||
{
|
||||
// Start a timed scope to ensure failed responses return is a consistent time
|
||||
await using var timedScope = new TimedScope(GetLoginDuration(), CancellationToken.None);
|
||||
|
||||
// Sign the user in with username/password, this also gives a chance for developers to
|
||||
// custom verify the credentials and auto-link user accounts with a custom IBackOfficePasswordChecker
|
||||
SignInResult result = await _signInManager.PasswordSignInAsync(
|
||||
loginModel.Username, loginModel.Password, true, true);
|
||||
|
||||
if (result.Succeeded)
|
||||
if (result.Succeeded is false)
|
||||
{
|
||||
// return the user detail
|
||||
return GetUserDetail(_userService.GetByUsername(loginModel.Username));
|
||||
}
|
||||
BackOfficeIdentityUser? user = await _userManager.FindByNameAsync(loginModel.Username.Trim());
|
||||
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
var twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(loginModel.Username);
|
||||
if (user is not null &&
|
||||
await _userManager.CheckPasswordAsync(user, loginModel.Password))
|
||||
{
|
||||
// The credentials were correct, so cancel timed scope and provide a more detailed failure response
|
||||
await timedScope.CancelAsync();
|
||||
|
||||
IUser? attemptedUser = _userService.GetByUsername(loginModel.Username);
|
||||
|
||||
// create a with information to display a custom two factor send code view
|
||||
var verifyResponse =
|
||||
new ObjectResult(new { twoFactorView = twofactorView, userId = attemptedUser?.Id })
|
||||
if (result.RequiresTwoFactor)
|
||||
{
|
||||
StatusCode = StatusCodes.Status402PaymentRequired
|
||||
};
|
||||
var twofactorView = _backOfficeTwoFactorOptions.GetTwoFactorView(loginModel.Username);
|
||||
|
||||
return verifyResponse;
|
||||
IUser? attemptedUser = _userService.GetByUsername(loginModel.Username);
|
||||
|
||||
// create a with information to display a custom two factor send code view
|
||||
var verifyResponse =
|
||||
new ObjectResult(new { twoFactorView = twofactorView, userId = attemptedUser?.Id })
|
||||
{
|
||||
StatusCode = StatusCodes.Status402PaymentRequired
|
||||
};
|
||||
|
||||
return verifyResponse;
|
||||
}
|
||||
|
||||
// TODO: We can check for these and respond differently if we think it's important
|
||||
// result.IsLockedOut
|
||||
// result.IsNotAllowed
|
||||
}
|
||||
|
||||
// Return BadRequest (400), we don't want to return a 401 because that get's intercepted
|
||||
// by our angular helper because it thinks that we need to re-perform the request once we are
|
||||
// authorized and we don't want to return a 403 because angular will show a warning message indicating
|
||||
// that the user doesn't have access to perform this function, we just want to return a normal invalid message.
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
// TODO: We can check for these and respond differently if we think it's important
|
||||
// result.IsLockedOut
|
||||
// result.IsNotAllowed
|
||||
// Set initial or update average (successful) login duration
|
||||
_loginDurationAverage = _loginDurationAverage is long average
|
||||
? (average + (long)timedScope.Elapsed.TotalMilliseconds) / 2
|
||||
: (long)timedScope.Elapsed.TotalMilliseconds;
|
||||
|
||||
// return BadRequest (400), we don't want to return a 401 because that get's intercepted
|
||||
// by our angular helper because it thinks that we need to re-perform the request once we are
|
||||
// authorized and we don't want to return a 403 because angular will show a warning message indicating
|
||||
// that the user doesn't have access to perform this function, we just want to return a normal invalid message.
|
||||
return BadRequest();
|
||||
// Cancel the timed scope (we don't want to unnecessarily wait on a successful response)
|
||||
await timedScope.CancelAsync();
|
||||
|
||||
// Return the user detail
|
||||
return GetUserDetail(_userService.GetByUsername(loginModel.Username));
|
||||
}
|
||||
|
||||
private long GetLoginDuration()
|
||||
{
|
||||
var loginDuration = Math.Max(_loginDurationAverage ?? _securitySettings.UserDefaultFailedLoginDurationInMilliseconds, _securitySettings.UserMinimumFailedLoginDurationInMilliseconds);
|
||||
var random = new Random();
|
||||
var randomDelay = random.Next(-FailedLoginDurationRandomOffsetInMilliseconds, FailedLoginDurationRandomOffsetInMilliseconds);
|
||||
loginDuration += randomDelay;
|
||||
|
||||
// Just be sure we don't get a negative number - possible if someone has configured a very low UserMinimumFailedLoginDurationInMilliseconds value.
|
||||
if (loginDuration < 0)
|
||||
{
|
||||
loginDuration = 0;
|
||||
}
|
||||
|
||||
return loginDuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -87,7 +87,7 @@ public class MacroRenderingController : UmbracoAuthorizedJsonController
|
||||
[HttpGet]
|
||||
public async Task<IActionResult> GetMacroResultAsHtmlForEditor(string macroAlias, int pageId,
|
||||
[FromQuery] IDictionary<string, object> macroParams) =>
|
||||
await GetMacroResultAsHtml(macroAlias, pageId, macroParams);
|
||||
await GetMacroResultAsHtml(macroAlias, pageId.ToString(), macroParams);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a rendered macro as HTML for rendering in the rich text editor.
|
||||
@@ -98,11 +98,24 @@ public class MacroRenderingController : UmbracoAuthorizedJsonController
|
||||
/// <param name="model"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
[NonAction]
|
||||
[Obsolete("This endpoint is no longer used.")]
|
||||
public async Task<IActionResult> GetMacroResultAsHtmlForEditor(MacroParameterModel model) =>
|
||||
await GetMacroResultAsHtml(model.MacroAlias, model.PageId.ToString(), model.MacroParams);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a rendered macro as HTML for rendering in the rich text editor.
|
||||
/// Using HTTP POST instead of GET allows for more parameters to be passed as it's not dependent on URL-length
|
||||
/// limitations like GET.
|
||||
/// The method using GET is kept to maintain backwards compatibility
|
||||
/// </summary>
|
||||
/// <param name="model"></param>
|
||||
/// <returns></returns>
|
||||
[HttpPost]
|
||||
public async Task<IActionResult> GetMacroResultAsHtmlForEditor(MacroParameterModel2 model) =>
|
||||
await GetMacroResultAsHtml(model.MacroAlias, model.PageId, model.MacroParams);
|
||||
|
||||
private async Task<IActionResult> GetMacroResultAsHtml(string? macroAlias, int pageId,
|
||||
IDictionary<string, object>? macroParams)
|
||||
private async Task<IActionResult> GetMacroResultAsHtml(string? macroAlias, string pageId, IDictionary<string, object>? macroParams)
|
||||
{
|
||||
IMacro? m = macroAlias is null ? null : _macroService.GetByAlias(macroAlias);
|
||||
if (m == null)
|
||||
@@ -111,11 +124,11 @@ public class MacroRenderingController : UmbracoAuthorizedJsonController
|
||||
}
|
||||
|
||||
IUmbracoContext umbracoContext = _umbracoContextAccessor.GetRequiredUmbracoContext();
|
||||
IPublishedContent? publishedContent = umbracoContext.Content?.GetById(true, pageId);
|
||||
IPublishedContent? publishedContent = GetPagePublishedContent(pageId, umbracoContext);
|
||||
|
||||
//if it isn't supposed to be rendered in the editor then return an empty string
|
||||
//currently we cannot render a macro if the page doesn't yet exist
|
||||
if (pageId == -1 || publishedContent == null || m.DontRender)
|
||||
if (publishedContent == null || m.DontRender)
|
||||
{
|
||||
//need to create a specific content result formatted as HTML since this controller has been configured
|
||||
//with only json formatters.
|
||||
@@ -149,6 +162,21 @@ public class MacroRenderingController : UmbracoAuthorizedJsonController
|
||||
}
|
||||
}
|
||||
|
||||
private static IPublishedContent? GetPagePublishedContent(string pageId, IUmbracoContext umbracoContext)
|
||||
{
|
||||
if (int.TryParse(pageId, NumberStyles.Integer, CultureInfo.InvariantCulture, out int pageIdAsInt))
|
||||
{
|
||||
return umbracoContext.Content?.GetById(true, pageIdAsInt);
|
||||
}
|
||||
|
||||
if (Guid.TryParse(pageId, out Guid pageIdAsGuid))
|
||||
{
|
||||
return umbracoContext.Content?.GetById(true, pageIdAsGuid);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
[HttpPost]
|
||||
public IActionResult CreatePartialViewMacroWithFile(CreatePartialViewMacroWithFileModel model)
|
||||
{
|
||||
@@ -180,6 +208,7 @@ public class MacroRenderingController : UmbracoAuthorizedJsonController
|
||||
return Ok();
|
||||
}
|
||||
|
||||
[Obsolete("This model is no longer used and has been replaced with MacroParameterModel2 that changes the type of the PageId property.")]
|
||||
public class MacroParameterModel
|
||||
{
|
||||
public string? MacroAlias { get; set; }
|
||||
@@ -187,6 +216,13 @@ public class MacroRenderingController : UmbracoAuthorizedJsonController
|
||||
public IDictionary<string, object>? MacroParams { get; set; }
|
||||
}
|
||||
|
||||
public class MacroParameterModel2
|
||||
{
|
||||
public string? MacroAlias { get; set; }
|
||||
public string PageId { get; set; } = string.Empty;
|
||||
public IDictionary<string, object>? MacroParams { get; set; }
|
||||
}
|
||||
|
||||
public class CreatePartialViewMacroWithFileModel
|
||||
{
|
||||
public string? Filename { get; set; }
|
||||
|
||||
@@ -217,10 +217,8 @@ public partial class PreviewController : Controller
|
||||
|
||||
// are we attempting a redirect to the default route (by ID with optional culture)?
|
||||
Match match = DefaultPreviewRedirectRegex().Match(redir ?? string.Empty);
|
||||
if (match.Success)
|
||||
if (match.Success && int.TryParse(match.Groups["id"].Value, out int id))
|
||||
{
|
||||
var id = int.Parse(match.Groups["id"].Value);
|
||||
|
||||
// first try to resolve the published URL
|
||||
if (_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext? umbracoContext) &&
|
||||
umbracoContext.Content is not null)
|
||||
|
||||
@@ -98,16 +98,6 @@ public class StaticFilesTreeController : TreeController
|
||||
|
||||
private void AddPhysicalFiles(string path, FormCollection queryStrings, TreeNodeCollection nodes)
|
||||
{
|
||||
IEnumerable<string> files = _fileSystem.GetFiles(path)
|
||||
.Where(x => x.StartsWith(AppPlugins) || x.StartsWith(Webroot));
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var name = Path.GetFileName(file);
|
||||
TreeNode node = CreateTreeNode(WebUtility.UrlEncode(file), path, queryStrings, name, Constants.Icons.DefaultIcon, false);
|
||||
nodes.Add(node);
|
||||
}
|
||||
|
||||
IEnumerable<string> directories = _fileSystem.GetDirectories(path);
|
||||
|
||||
foreach (var directory in directories)
|
||||
@@ -117,6 +107,16 @@ public class StaticFilesTreeController : TreeController
|
||||
TreeNode node = CreateTreeNode(WebUtility.UrlEncode(directory), path, queryStrings, name, Constants.Icons.Folder, hasChildren);
|
||||
nodes.Add(node);
|
||||
}
|
||||
|
||||
IEnumerable<string> files = _fileSystem.GetFiles(path)
|
||||
.Where(x => x.StartsWith(AppPlugins) || x.StartsWith(Webroot));
|
||||
|
||||
foreach (var file in files)
|
||||
{
|
||||
var name = Path.GetFileName(file);
|
||||
TreeNode node = CreateTreeNode(WebUtility.UrlEncode(file), path, queryStrings, name, Constants.Icons.DefaultIcon, false);
|
||||
nodes.Add(node);
|
||||
}
|
||||
}
|
||||
|
||||
private void AddWebRootFiles(string path, FormCollection queryStrings, TreeNodeCollection nodes)
|
||||
|
||||
+487
-476
File diff suppressed because it is too large
Load Diff
@@ -19,8 +19,8 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@microsoft/signalr": "8.0.7",
|
||||
"@umbraco-ui/uui": "1.12.2",
|
||||
"@umbraco-ui/uui-css": "1.12.1",
|
||||
"@umbraco-ui/uui": "1.13.0",
|
||||
"@umbraco-ui/uui-css": "1.13.0",
|
||||
"ace-builds": "1.31.1",
|
||||
"angular": "1.8.3",
|
||||
"angular-animate": "1.8.3",
|
||||
|
||||
+2377
-2365
File diff suppressed because it is too large
Load Diff
@@ -1,29 +1,29 @@
|
||||
{
|
||||
"name": "login",
|
||||
"private": true,
|
||||
"name": "login",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"watch": "tsc && vite build --watch",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=20.8",
|
||||
"npm": ">=10.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"lit": "^3.1.2",
|
||||
"msw": "^2.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc && vite build",
|
||||
"watch": "tsc && vite build --watch",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@umbraco-ui/uui": "1.12.2",
|
||||
"@umbraco-ui/uui-css": "1.12.1",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.7"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
}
|
||||
"engines": {
|
||||
"node": ">=20.8",
|
||||
"npm": ">=10.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"lit": "^3.1.2",
|
||||
"msw": "^2.2.0",
|
||||
"rxjs": "^7.8.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@umbraco-ui/uui": "1.13.0",
|
||||
"@umbraco-ui/uui-css": "1.13.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vite": "^5.1.7"
|
||||
},
|
||||
"msw": {
|
||||
"workerDirectory": "public"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Reflection;
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
@@ -126,8 +126,20 @@ internal class EagerMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
|
||||
return;
|
||||
}
|
||||
|
||||
// If it's an attribute routed IVirtualPageController with a Host attribute we should ignore if the host doesn't match the current request.
|
||||
// Maybe we would expect that it wouldn't be in the provided CandidateSet, but it will be included just based on the Route.
|
||||
// See: https://github.com/umbraco/Umbraco-CMS/issues/16816
|
||||
if (controllerTypeInfo is not null && controllerTypeInfo.IsType<IVirtualPageController>())
|
||||
{
|
||||
HostAttribute? hostAttribute = controllerTypeInfo.GetCustomAttribute<HostAttribute>();
|
||||
if (hostAttribute is not null && hostAttribute.Hosts.InvariantContains(httpContext.Request.Host.Value) is false)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If it's an UmbracoPageController we need to do some domain routing.
|
||||
// We need to do this in oder to handle cultures for our Dictionary.
|
||||
// We need to do this in order to handle cultures for our Dictionary.
|
||||
// This is because UmbracoPublishedContentCultureProvider is ued to set the Thread.CurrentThread.CurrentUICulture
|
||||
// The CultureProvider is run before the actual routing, this means that our UmbracoVirtualPageFilterAttribute is hit AFTER the culture is set.
|
||||
// Meaning we have to route the domain part already now, this is not pretty, but it beats having to look for content we know doesn't exist.
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
[TestFixture]
|
||||
public class RequestPreviewServiceTests
|
||||
{
|
||||
[TestCase(null, false)]
|
||||
[TestCase("", false)]
|
||||
[TestCase("false", false)]
|
||||
[TestCase("true", true)]
|
||||
[TestCase("True", true)]
|
||||
public void IsPreview_Returns_Expected_Result(string? headerValue, bool expected)
|
||||
{
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Headers["Preview"] = headerValue;
|
||||
|
||||
var httpContextAccessorMock = new Mock<IHttpContextAccessor>();
|
||||
httpContextAccessorMock
|
||||
.Setup(x => x.HttpContext)
|
||||
.Returns(httpContext);
|
||||
var sut = new RequestPreviewService(httpContextAccessorMock.Object);
|
||||
|
||||
var result = sut.IsPreview();
|
||||
|
||||
Assert.AreEqual(expected, result);
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json",
|
||||
"version": "13.8.0-rc",
|
||||
"version": "13.8.1",
|
||||
"assemblyVersion": {
|
||||
"precision": "build"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user