Merge remote-tracking branch 'origin/v12/dev' into v13/dev

# Conflicts:
#	src/Umbraco.Cms.Api.Management/Controllers/ManagementApiControllerBase.cs
This commit is contained in:
Bjarke Berg
2023-01-06 10:30:58 +01:00
18 changed files with 99 additions and 88 deletions

View File

@@ -1,9 +1,9 @@
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Json;
using Umbraco.Cms.Api.Common.Json;
namespace Umbraco.Cms.Api.Management.Configuration;
namespace Umbraco.Cms.Api.Common.Configuration;
public class ConfigureMvcJsonOptions : IConfigureOptions<MvcOptions>
{

View File

@@ -2,9 +2,9 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Api.Management.Configuration;
using Umbraco.Cms.Api.Common.Configuration;
namespace Umbraco.Cms.Api.Management.DependencyInjection;
namespace Umbraco.Cms.Api.Common.DependencyInjection;
public static class MvcBuilderExtensions
{

View File

@@ -1,4 +1,4 @@
namespace Umbraco.Cms.Api.Management.Filters;
namespace Umbraco.Cms.Api.Common.Filters;
[AttributeUsage(AttributeTargets.Class)]
public class JsonOptionsNameAttribute : Attribute

View File

@@ -1,7 +1,7 @@
using Microsoft.AspNetCore.Http;
using Umbraco.Cms.Api.Management.Filters;
using Umbraco.Cms.Api.Common.Filters;
namespace Umbraco.Cms.Api.Management.Json;
namespace Umbraco.Cms.Api.Common.Json;
public static class HttpContextJsonExtensions
{

View File

@@ -2,7 +2,7 @@
using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Extensions.Logging;
namespace Umbraco.Cms.Api.Management.Json;
namespace Umbraco.Cms.Api.Common.Json;
public class NamedSystemTextJsonInputFormatter : SystemTextJsonInputFormatter
{

View File

@@ -1,7 +1,7 @@
using System.Text.Json;
using Microsoft.AspNetCore.Mvc.Formatters;
namespace Umbraco.Cms.Api.Management.Json;
namespace Umbraco.Cms.Api.Common.Json;
public class NamedSystemTextJsonOutputFormatter : SystemTextJsonOutputFormatter
{

View File

@@ -4,6 +4,7 @@ using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Management.Services;
@@ -13,18 +14,15 @@ namespace Umbraco.Cms.Api.Management.Controllers.Dictionary;
public class ImportDictionaryController : DictionaryControllerBase
{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IDictionaryService _dictionaryService;
private readonly IWebHostEnvironment _webHostEnvironment;
private readonly ILoadDictionaryItemService _loadDictionaryItemService;
public ImportDictionaryController(
IHostingEnvironment hostingEnvironment,
IDictionaryService dictionaryService,
IWebHostEnvironment webHostEnvironment,
ILoadDictionaryItemService loadDictionaryItemService)
{
_hostingEnvironment = hostingEnvironment;
_dictionaryService = dictionaryService;
_webHostEnvironment = webHostEnvironment;
_loadDictionaryItemService = loadDictionaryItemService;
@@ -41,7 +39,7 @@ public class ImportDictionaryController : DictionaryControllerBase
return NotFound();
}
var filePath = Path.Combine(_hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), file);
var filePath = Path.Combine(_webHostEnvironment.MapPathContentRoot(Constants.SystemDirectories.Data), file);
if (_webHostEnvironment.ContentRootFileProvider.GetFileInfo(filePath) is null)
{
return await Task.FromResult(NotFound());

View File

@@ -1,6 +1,6 @@
using System.Linq.Expressions;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Filters;
using Umbraco.Cms.Api.Common.Filters;
using Umbraco.Cms.Core.Security;
using Umbraco.New.Cms.Core;

View File

@@ -4,9 +4,9 @@ public static class Paths
{
public const string BackOfficeApiEndpointTemplate = "security/back-office";
public static string BackOfficeApiAuthorizationEndpoint = BackOfficeApiEndpointPath($"{BackOfficeApiEndpointTemplate}/authorize");
public static readonly string BackOfficeApiAuthorizationEndpoint = BackOfficeApiEndpointPath($"{BackOfficeApiEndpointTemplate}/authorize");
public static string BackOfficeApiTokenEndpoint = BackOfficeApiEndpointPath($"{BackOfficeApiEndpointTemplate}/token");
public static readonly string BackOfficeApiTokenEndpoint = BackOfficeApiEndpointPath($"{BackOfficeApiEndpointTemplate}/token");
private static string BackOfficeApiEndpointPath(string relativePath) => $"/umbraco/management/api/v1.0/{relativePath}";
}

View File

@@ -12,16 +12,18 @@ using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using Microsoft.OpenApi.Models;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Composing;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Api.Common.Configuration;
using Umbraco.Cms.Api.Common.DependencyInjection;
using Umbraco.Cms.Api.Management.Controllers.Security;
using Umbraco.Cms.Api.Management.DependencyInjection;
using Umbraco.Cms.Api.Management.Json;
using Umbraco.Cms.Api.Management.OpenApi;
using Umbraco.Cms.Infrastructure.Serialization;
using Umbraco.Cms.Web.Common.ApplicationBuilder;
using Umbraco.Extensions;
using Umbraco.New.Cms.Core;
using Umbraco.New.Cms.Core.Models.Configuration;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
@@ -80,18 +82,22 @@ public class ManagementApiComposer : IComposer
if (string.IsNullOrWhiteSpace(relativePath))
{
throw new Exception($"There is no relative path for controller action {e.ActionDescriptor.RouteValues["controller"]}");
throw new Exception(
$"There is no relative path for controller action {e.ActionDescriptor.RouteValues["controller"]}");
}
// Remove the prefixed base path with version, e.g. /umbraco/management/api/v1/tracked-reference/{id} => tracked-reference/{id}
var unprefixedRelativePath = OperationIdRegexes.VersionPrefixRegex().Replace(relativePath, string.Empty);
var unprefixedRelativePath =
OperationIdRegexes.VersionPrefixRegex().Replace(relativePath, string.Empty);
// Remove template placeholders, e.g. tracked-reference/{id} => tracked-reference/Id
var formattedOperationId = OperationIdRegexes.TemplatePlaceholdersRegex().Replace(unprefixedRelativePath, m => $"By{m.Groups[1].Value.ToFirstUpper()}");
var formattedOperationId = OperationIdRegexes.TemplatePlaceholdersRegex()
.Replace(unprefixedRelativePath, m => $"By{m.Groups[1].Value.ToFirstUpper()}");
// Remove dashes (-) and slashes (/) and convert the following letter to uppercase with
// the word "By" in front, e.g. tracked-reference/Id => TrackedReferenceById
formattedOperationId = OperationIdRegexes.ToCamelCaseRegex().Replace(formattedOperationId, m => m.Groups[1].Value.ToUpper());
formattedOperationId = OperationIdRegexes.ToCamelCaseRegex()
.Replace(formattedOperationId, m => m.Groups[1].Value.ToUpper());
// Return the operation ID with the formatted http method verb in front, e.g. GetTrackedReferenceById
return $"{httpMethod}{formattedOperationId.ToFirstUpper()}";
@@ -102,33 +108,41 @@ public class ManagementApiComposer : IComposer
{
Title = ApiTitle,
Version = DefaultApiVersion.ToString(),
Description = "This shows all APIs available in this version of Umbraco - including all the legacy apis that are available for backward compatibility"
Description =
"This shows all APIs available in this version of Umbraco - including all the legacy apis that are available for backward compatibility"
});
swaggerGenOptions.DocInclusionPredicate((_, api) => !string.IsNullOrWhiteSpace(api.GroupName));
swaggerGenOptions.TagActionsBy(api => new [] { api.GroupName });
swaggerGenOptions.TagActionsBy(api => new[] { api.GroupName });
// see https://github.com/domaindrivendev/Swashbuckle.AspNetCore#change-operation-sort-order-eg-for-ui-sorting
string ActionSortKeySelector(ApiDescription apiDesc)
=> $"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.ActionDescriptor.RouteValues["action"]}_{apiDesc.HttpMethod}";
{
return
$"{apiDesc.GroupName}_{apiDesc.ActionDescriptor.AttributeRouteInfo?.Template ?? apiDesc.ActionDescriptor.RouteValues["controller"]}_{apiDesc.ActionDescriptor.RouteValues["action"]}_{apiDesc.HttpMethod}";
}
swaggerGenOptions.OrderActionsBy(ActionSortKeySelector);
swaggerGenOptions.AddSecurityDefinition("OAuth", new OpenApiSecurityScheme
{
In = ParameterLocation.Header,
Name = "Umbraco",
Type = SecuritySchemeType.OAuth2,
Description = "Umbraco Authentication",
Flows = new OpenApiOAuthFlows
swaggerGenOptions.AddSecurityDefinition(
"OAuth",
new OpenApiSecurityScheme
{
AuthorizationCode = new OpenApiOAuthFlow
In = ParameterLocation.Header,
Name = "Umbraco",
Type = SecuritySchemeType.OAuth2,
Description = "Umbraco Authentication",
Flows = new OpenApiOAuthFlows
{
AuthorizationUrl = new Uri(Controllers.Security.Paths.BackOfficeApiAuthorizationEndpoint, UriKind.Relative),
TokenUrl = new Uri(Controllers.Security.Paths.BackOfficeApiTokenEndpoint, UriKind.Relative)
AuthorizationCode = new OpenApiOAuthFlow
{
AuthorizationUrl =
new Uri(Paths.BackOfficeApiAuthorizationEndpoint, UriKind.Relative),
TokenUrl = new Uri(Paths.BackOfficeApiTokenEndpoint, UriKind.Relative)
}
}
}
});
});
swaggerGenOptions.AddSecurityRequirement(new OpenApiSecurityRequirement
{
@@ -137,13 +151,9 @@ public class ManagementApiComposer : IComposer
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Id = "OAuth",
Type = ReferenceType.SecurityScheme
}
Reference = new OpenApiReference { Id = "OAuth", Type = ReferenceType.SecurityScheme }
},
new List<string> { }
new List<string>()
}
});
@@ -166,7 +176,7 @@ public class ManagementApiComposer : IComposer
{
// any generic JSON options go here
})
.AddJsonOptions(Constants.JsonOptionsNames.BackOffice, options =>
.AddJsonOptions(Umbraco.New.Cms.Core.Constants.JsonOptionsNames.BackOffice, options =>
{
// all back-office specific JSON options go here
options.JsonSerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
@@ -189,8 +199,10 @@ public class ManagementApiComposer : IComposer
applicationBuilder.UseWhen(
httpContext =>
{
GlobalSettings? settings = httpContext.RequestServices.GetRequiredService<IOptions<GlobalSettings>>().Value;
IHostingEnvironment hostingEnvironment = httpContext.RequestServices.GetRequiredService<IHostingEnvironment>();
GlobalSettings? settings = httpContext.RequestServices
.GetRequiredService<IOptions<GlobalSettings>>().Value;
IHostingEnvironment hostingEnvironment =
httpContext.RequestServices.GetRequiredService<IHostingEnvironment>();
var officePath = settings.GetBackOfficePath(hostingEnvironment);
return httpContext.Request.Path.Value?.StartsWith($"{officePath}/management/api/") ?? false;
@@ -211,7 +223,7 @@ public class ManagementApiComposer : IComposer
Detail = exception.StackTrace,
Status = StatusCodes.Status500InternalServerError,
Instance = exception.GetType().Name,
Type = "Error",
Type = "Error"
};
await context.Response.WriteAsJsonAsync(response);
}));
@@ -230,14 +242,19 @@ public class ManagementApiComposer : IComposer
applicationBuilder.UseSwagger(swaggerOptions =>
{
swaggerOptions.RouteTemplate = $"{officePath.TrimStart(Core.Constants.CharArrays.ForwardSlash)}/swagger/{{documentName}}/swagger.json";
swaggerOptions.RouteTemplate =
$"{officePath.TrimStart(Constants.CharArrays.ForwardSlash)}/swagger/{{documentName}}/swagger.json";
});
applicationBuilder.UseSwaggerUI(swaggerUiOptions =>
applicationBuilder.UseSwaggerUI(
swaggerUiOptions =>
{
swaggerUiOptions.SwaggerEndpoint($"{officePath}/swagger/v1/swagger.json", $"{ApiTitle} {DefaultApiVersion}");
swaggerUiOptions.RoutePrefix = $"{officePath.TrimStart(Core.Constants.CharArrays.ForwardSlash)}/swagger";
swaggerUiOptions.SwaggerEndpoint(
$"{officePath}/swagger/v1/swagger.json",
$"{ApiTitle} {DefaultApiVersion}");
swaggerUiOptions.RoutePrefix =
$"{officePath.TrimStart(Constants.CharArrays.ForwardSlash)}/swagger";
swaggerUiOptions.OAuthClientId(Constants.OauthClientIds.Swagger);
swaggerUiOptions.OAuthClientId(New.Cms.Core.Constants.OauthClientIds.Swagger);
swaggerUiOptions.OAuthUsePkce();
});
}
@@ -257,11 +274,12 @@ public class ManagementApiComposer : IComposer
// Serve contract
endpoints.MapGet($"{officePath}/management/api/openapi.json", async context =>
{
await context.Response.SendFileAsync(new EmbeddedFileProvider(GetType().Assembly).GetFileInfo("OpenApi.json"));
await context.Response.SendFileAsync(
new EmbeddedFileProvider(GetType().Assembly).GetFileInfo("OpenApi.json"));
});
});
}
));
}));
});
}
}

View File

@@ -1,22 +1,24 @@
using System.Xml;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Hosting;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Hosting;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Api.Management.Models;
using Umbraco.Extensions;
using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment;
namespace Umbraco.Cms.Api.Management.Services;
public class UploadFileService : IUploadFileService
{
private readonly IHostingEnvironment _hostingEnvironment;
private readonly IHostEnvironment _hostEnvironment;
private readonly ILocalizedTextService _localizedTextService;
public UploadFileService(IHostingEnvironment hostingEnvironment, ILocalizedTextService localizedTextService)
public UploadFileService(IHostEnvironment hostEnvironment, ILocalizedTextService localizedTextService)
{
_hostingEnvironment = hostingEnvironment;
_hostEnvironment = hostEnvironment;
_localizedTextService = localizedTextService;
}
@@ -25,7 +27,7 @@ public class UploadFileService : IUploadFileService
var formFileUploadResult = new FormFileUploadResult();
var fileName = file.FileName.Trim(Constants.CharArrays.DoubleQuote);
var ext = fileName.Substring(fileName.LastIndexOf('.') + 1).ToLower();
var root = _hostingEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads);
var root = _hostEnvironment.MapPathContentRoot(Constants.SystemDirectories.TempFileUploads);
formFileUploadResult.TemporaryPath = Path.Combine(root, fileName);
if (!Path.GetFullPath(formFileUploadResult.TemporaryPath).StartsWith(Path.GetFullPath(root)))

View File

@@ -15,9 +15,6 @@
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.1" />
<PackageReference Include="OpenIddict.AspNetCore" Version="3.1.1" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="3.1.1" />
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="7.0.0" />
<PackageReference Include="OpenIddict.AspNetCore" Version="3.1.1" />
<PackageReference Include="OpenIddict.EntityFrameworkCore" Version="3.1.1" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.4.0" />
</ItemGroup>

View File

@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models;
namespace Umbraco.Cms.Core.Logging.Viewer;
@@ -9,17 +9,17 @@ public interface ILogViewer
/// <summary>
/// Get all saved searches from your chosen data source
/// </summary>
IReadOnlyList<SavedLogSearch>? GetSavedSearches();
IReadOnlyList<SavedLogSearch> GetSavedSearches();
/// <summary>
/// Adds a new saved search to chosen data source and returns the updated searches
/// </summary>
IReadOnlyList<SavedLogSearch>? AddSavedSearch(string? name, string? query);
IReadOnlyList<SavedLogSearch> AddSavedSearch(string? name, string? query);
/// <summary>
/// Deletes a saved search to chosen data source and returns the remaining searches
/// </summary>
IReadOnlyList<SavedLogSearch>? DeleteSavedSearch(string? name, string? query);
IReadOnlyList<SavedLogSearch> DeleteSavedSearch(string? name, string? query);
/// <summary>
/// A count of number of errors

View File

@@ -2,9 +2,9 @@ namespace Umbraco.Cms.Core.Logging.Viewer;
public interface ILogViewerConfig
{
IReadOnlyList<SavedLogSearch>? GetSavedSearches();
IReadOnlyList<SavedLogSearch> GetSavedSearches();
IReadOnlyList<SavedLogSearch>? AddSavedSearch(string? name, string? query);
IReadOnlyList<SavedLogSearch> AddSavedSearch(string? name, string? query);
IReadOnlyList<SavedLogSearch>? DeleteSavedSearch(string? name, string? query);
IReadOnlyList<SavedLogSearch> DeleteSavedSearch(string? name, string? query);
}

View File

@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using IScope = Umbraco.Cms.Infrastructure.Scoping.IScope;
@@ -16,15 +16,15 @@ public class LogViewerConfig : ILogViewerConfig
_scopeProvider = scopeProvider;
}
public IReadOnlyList<SavedLogSearch>? GetSavedSearches()
public IReadOnlyList<SavedLogSearch> GetSavedSearches()
{
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
IEnumerable<ILogViewerQuery>? logViewerQueries = _logViewerQueryRepository.GetMany();
SavedLogSearch[]? result = logViewerQueries?.Select(x => new SavedLogSearch() { Name = x.Name, Query = x.Query }).ToArray();
IEnumerable<ILogViewerQuery> logViewerQueries = _logViewerQueryRepository.GetMany();
SavedLogSearch[] result = logViewerQueries.Select(x => new SavedLogSearch() { Name = x.Name, Query = x.Query }).ToArray();
return result;
}
public IReadOnlyList<SavedLogSearch>? AddSavedSearch(string? name, string? query)
public IReadOnlyList<SavedLogSearch> AddSavedSearch(string? name, string? query)
{
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
_logViewerQueryRepository.Save(new LogViewerQuery(name, query));
@@ -32,7 +32,7 @@ public class LogViewerConfig : ILogViewerConfig
return GetSavedSearches();
}
public IReadOnlyList<SavedLogSearch>? DeleteSavedSearch(string? name, string? query)
public IReadOnlyList<SavedLogSearch> DeleteSavedSearch(string? name, string? query)
{
using IScope scope = _scopeProvider.CreateScope(autoComplete: true);
ILogViewerQuery? item = name is null ? null : _logViewerQueryRepository.GetByName(name);

View File

@@ -1,8 +1,6 @@
using System.Collections.ObjectModel;
using Microsoft.Extensions.DependencyInjection;
using System.Collections.ObjectModel;
using Serilog;
using Serilog.Events;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Extensions;
@@ -12,26 +10,24 @@ public abstract class SerilogLogViewerSourceBase : ILogViewer
{
private readonly ILogLevelLoader _logLevelLoader;
private readonly ILogViewerConfig _logViewerConfig;
private readonly ILogger _serilogLog;
protected SerilogLogViewerSourceBase(ILogViewerConfig logViewerConfig, ILogLevelLoader logLevelLoader, ILogger serilogLog)
{
_logViewerConfig = logViewerConfig;
_logLevelLoader = logLevelLoader;
_serilogLog = serilogLog;
}
public abstract bool CanHandleLargeLogs { get; }
public abstract bool CheckCanOpenLogs(LogTimePeriod logTimePeriod);
public virtual IReadOnlyList<SavedLogSearch>? GetSavedSearches()
public virtual IReadOnlyList<SavedLogSearch> GetSavedSearches()
=> _logViewerConfig.GetSavedSearches();
public virtual IReadOnlyList<SavedLogSearch>? AddSavedSearch(string? name, string? query)
public virtual IReadOnlyList<SavedLogSearch> AddSavedSearch(string? name, string? query)
=> _logViewerConfig.AddSavedSearch(name, query);
public virtual IReadOnlyList<SavedLogSearch>? DeleteSavedSearch(string? name, string? query)
public virtual IReadOnlyList<SavedLogSearch> DeleteSavedSearch(string? name, string? query)
=> _logViewerConfig.DeleteSavedSearch(name, query);
public int GetNumberOfErrors(LogTimePeriod logTimePeriod)

View File

@@ -2,7 +2,7 @@
using System.Text.Json.Nodes;
using System.Text.Json.Serialization;
namespace Umbraco.Cms.Api.Management.Json;
namespace Umbraco.Cms.Infrastructure.Serialization;
public class JsonObjectConverter : JsonConverter<object>
{

View File

@@ -129,14 +129,14 @@ public class LogViewerController : BackOfficeNotificationsController
}
[HttpGet]
public IEnumerable<SavedLogSearch>? GetSavedSearches() => _logViewer.GetSavedSearches();
public IEnumerable<SavedLogSearch> GetSavedSearches() => _logViewer.GetSavedSearches();
[HttpPost]
public IEnumerable<SavedLogSearch>? PostSavedSearch(SavedLogSearch item) =>
public IEnumerable<SavedLogSearch> PostSavedSearch(SavedLogSearch item) =>
_logViewer.AddSavedSearch(item.Name, item.Query);
[HttpPost]
public IEnumerable<SavedLogSearch>? DeleteSavedSearch(SavedLogSearch item) =>
public IEnumerable<SavedLogSearch> DeleteSavedSearch(SavedLogSearch item) =>
_logViewer.DeleteSavedSearch(item.Name, item.Query);
[HttpGet]