Merge branch 'v12/dev' into v13/dev
# Conflicts: # build/azure-pipelines.yml # src/Umbraco.Core/Configuration/Models/SecuritySettings.cs # version.json
This commit is contained in:
20
.github/CONTRIBUTING.md
vendored
20
.github/CONTRIBUTING.md
vendored
@@ -19,6 +19,7 @@ We also encourage community members to feel free to comment on others' pull requ
|
||||
* [What can I contribute?](#what-can-i-contribute)
|
||||
+ [Making larger changes](#making-larger-changes)
|
||||
+ [Pull request or package?](#pull-request-or-package)
|
||||
+ [Unwanted changes](#unwanted-changes)
|
||||
+ [Ownership and copyright](#ownership-and-copyright)
|
||||
- [Finding your first issue: Up for grabs](#finding-your-first-issue-up-for-grabs)
|
||||
- [Making your changes](#making-your-changes)
|
||||
@@ -69,6 +70,25 @@ If you're unsure about whether your changes belong in the core Umbraco CMS or if
|
||||
|
||||
If it doesn’t fit in CMS right now, we will likely encourage you to make it into a package instead. A package is a great way to check out popularity of a feature, learn how people use it, validate good usability and fix bugs. Eventually, a package could "graduate" to be included in the CMS.
|
||||
|
||||
#### Unwanted changes
|
||||
While most changes are welcome, there are certain types of changes that are discouraged and might get your pull request refused.
|
||||
Of course this will depend heavily on the specific change, but please take the following examples in mind.
|
||||
|
||||
- **Breaking changes (code and/or behavioral) 💥** - sometimes it can be a bit hard to know if a change is breaking or not. Fortunately, if it relates to code, the build will fail and warn you.
|
||||
- **Large refactors 🤯** - the larger the refactor, the larger the probability of introducing new bugs/issues.
|
||||
- **Changes to obsolete code and/or property editors ✍️**
|
||||
- **Adding new config options 🦾** - while having more flexibility is (most of the times) better, having too many options can also become overwhelming/confusing, especially if there are other (good/simple) ways to achieve it.
|
||||
- **Whitespace changes 🫥** - while some of our files might not follow the formatting/whitespace rules (mostly old ones), changing several of them in one go would cause major merge conflicts with open pull requests or other work in progress. Do feel free to fix these when you are working on another issue/feature and end up "touching" those files!
|
||||
- **Adding new extension/helper methods ✋** - keep in mind that more code also means more to maintain, so if a helper is only meaningful for a few, it might not be worth adding it to the core.
|
||||
|
||||
While these are only a few examples, it is important to ask yourself these questions before making a pull request:
|
||||
|
||||
- How many will benefit from this change?
|
||||
- Are there other ways to achieve this? And if so, how do they compare?
|
||||
- How maintainable is the change?
|
||||
- What would be the effort to test it properly?
|
||||
- Do the benefits outweigh the risks?
|
||||
|
||||
#### Ownership and copyright
|
||||
|
||||
It is your responsibility to make sure that you're allowed to share the code you're providing us. For example, you should have permission from your employer or customer to share code.
|
||||
|
||||
1
.github/README.md
vendored
1
.github/README.md
vendored
@@ -4,6 +4,7 @@
|
||||
[](CONTRIBUTING.md)
|
||||
[](https://twitter.com/intent/follow?screen_name=umbraco)
|
||||
[](https://discord.gg/umbraco)
|
||||
[](https://discord-chats.umbraco.com)
|
||||
[](https://umbraco.visualstudio.com/Umbraco%20Cms/_build?definitionId=301)
|
||||
[](https://github.com/codespaces/new?hide_repo_select=true&ref=contrib&repo=10601208&machine=basicLinux32gb&devcontainer_path=.devcontainer%2Fdevcontainer.json&location=WestEurope)
|
||||
|
||||
|
||||
@@ -38,8 +38,8 @@
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="all" IsImplicitlyDefined="true" />
|
||||
<PackageReference Include="Nerdbank.GitVersioning" Version="3.5.113" PrivateAssets="all" IsImplicitlyDefined="true" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.406" PrivateAssets="all" IsImplicitlyDefined="true" />
|
||||
<PackageReference Include="Nerdbank.GitVersioning" Version="3.6.133" PrivateAssets="all" IsImplicitlyDefined="true" />
|
||||
<PackageReference Include="StyleCop.Analyzers" Version="1.2.0-beta.507" PrivateAssets="all" IsImplicitlyDefined="true" />
|
||||
<PackageReference Include="Umbraco.Code" Version="2.0.0" PrivateAssets="all" IsImplicitlyDefined="true" />
|
||||
<PackageReference Include="Umbraco.GitVersioning.Extensions" Version="0.2.0" PrivateAssets="all" IsImplicitlyDefined="true" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -43,7 +43,7 @@ parameters:
|
||||
default: ' '
|
||||
|
||||
variables:
|
||||
nodeVersion: 18.16.0
|
||||
nodeVersion: 18.16.x
|
||||
dotnetVersion: 8.x
|
||||
dotnetIncludePreviewVersions: true
|
||||
solution: umbraco.sln
|
||||
@@ -99,29 +99,12 @@ stages:
|
||||
command: restore
|
||||
projects: $(solution)
|
||||
- task: DotNetCoreCLI@2
|
||||
displayName: Run dotnet build
|
||||
name: build
|
||||
displayName: Run dotnet build and generate NuGet packages
|
||||
inputs:
|
||||
command: build
|
||||
projects: $(solution)
|
||||
arguments: '--configuration $(buildConfiguration) --no-restore -p:ContinuousIntegrationBuild=true'
|
||||
- script: |
|
||||
version="$(Build.BuildNumber)"
|
||||
echo "Version: $version"
|
||||
|
||||
major="$(echo $version | cut -d '.' -f 1)"
|
||||
echo "Major version: $major"
|
||||
|
||||
echo "##vso[task.setvariable variable=majorVersion;isOutput=true]$major"
|
||||
displayName: Set major version
|
||||
name: determineMajorVersion
|
||||
- script: dotnet pack $(solution) --configuration $(buildConfiguration) --no-build --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg
|
||||
displayName: Run dotnet pack
|
||||
- script: |
|
||||
sha="$(Build.SourceVersion)"
|
||||
sha=${sha:0:7}
|
||||
buildnumber="$(Build.BuildNumber)_$(Build.BuildId)_$sha"
|
||||
echo "##vso[build.updatebuildnumber]$buildnumber"
|
||||
displayName: Update build number
|
||||
arguments: '--configuration $(buildConfiguration) --no-restore --property:ContinuousIntegrationBuild=true --property:GeneratePackageOnBuild=true --property:PackageOutputPath=$(Build.ArtifactStagingDirectory)/nupkg'
|
||||
- task: PublishPipelineArtifact@1
|
||||
displayName: Publish nupkg
|
||||
inputs:
|
||||
@@ -134,11 +117,11 @@ stages:
|
||||
artifactName: build_output
|
||||
|
||||
- stage: Build_Docs
|
||||
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.buildApiDocs}}))
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.buildApiDocs}}))
|
||||
displayName: Prepare API Documentation
|
||||
dependsOn: Build
|
||||
variables:
|
||||
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['determineMajorVersion.majorVersion'] ]
|
||||
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'] ]
|
||||
jobs:
|
||||
# C# API Reference
|
||||
- job:
|
||||
@@ -192,9 +175,9 @@ stages:
|
||||
vmImage: 'ubuntu-latest'
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
displayName: Use Node.js 10.15.0
|
||||
displayName: Use Node.js 10.15.x
|
||||
inputs:
|
||||
versionSpec: 10.15.0 # Won't work with higher versions
|
||||
versionSpec: 10.15.x # Won't work with higher versions
|
||||
- script: |
|
||||
npm ci --no-fund --no-audit --prefer-offline
|
||||
npx gulp docs
|
||||
@@ -262,6 +245,8 @@ stages:
|
||||
- stage: Integration
|
||||
displayName: Integration Tests
|
||||
dependsOn: Build
|
||||
variables:
|
||||
releaseTestFilter: eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True')
|
||||
jobs:
|
||||
# Integration Tests (SQLite)
|
||||
- job:
|
||||
@@ -294,7 +279,7 @@ stages:
|
||||
command: test
|
||||
projects: '**/*.Tests.Integration.csproj'
|
||||
testRunTitle: Integration Tests SQLite - $(Agent.OS)
|
||||
${{ if or( parameters.forceReleaseTestFilter, startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')) }}:
|
||||
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--configuration $(buildConfiguration) ${{parameters.integrationNonReleaseTestFilter}}'
|
||||
@@ -308,7 +293,7 @@ stages:
|
||||
command: test
|
||||
projects: '**/*.Tests.Integration.csproj'
|
||||
testRunTitle: Integration Tests SQLite - $(Agent.OS)
|
||||
${{ if or( parameters.forceReleaseTestFilter, startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')) }}:
|
||||
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--configuration $(buildConfiguration) ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
|
||||
@@ -319,7 +304,7 @@ stages:
|
||||
# Integration Tests (SQL Server)
|
||||
- job:
|
||||
timeoutInMinutes: 120
|
||||
condition: or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.sqlServerIntegrationTests}})
|
||||
condition: or(eq(stageDependencies.Build.A.outputs['build.NBGV_PublicRelease'], 'True'), ${{parameters.sqlServerIntegrationTests}})
|
||||
displayName: Integration Tests (SQL Server)
|
||||
strategy:
|
||||
matrix:
|
||||
@@ -359,7 +344,7 @@ stages:
|
||||
command: test
|
||||
projects: '**/*.Tests.Integration.csproj'
|
||||
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
|
||||
${{ if or( parameters.forceReleaseTestFilter, startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')) }}:
|
||||
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.integrationNonReleaseTestFilter}}'
|
||||
@@ -374,7 +359,7 @@ stages:
|
||||
command: test
|
||||
projects: '**/*.Tests.Integration.csproj'
|
||||
testRunTitle: Integration Tests SQL Server - $(Agent.OS)
|
||||
${{ if or( parameters.forceReleaseTestFilter, startsWith(variables['Build.SourceBranch'], 'refs/heads/release/')) }}:
|
||||
${{ if or(variables.releaseTestFilter, parameters.forceReleaseTestFilter) }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationReleaseTestFilter}}'
|
||||
${{ else }}:
|
||||
arguments: '--configuration $(buildConfiguration) --no-build ${{parameters.nonWindowsIntegrationNonReleaseTestFilter}}'
|
||||
@@ -535,7 +520,7 @@ stages:
|
||||
- Unit
|
||||
- Integration
|
||||
# - E2E # TODO: Enable when stable.
|
||||
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.myGetDeploy}}))
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.myGetDeploy}}))
|
||||
jobs:
|
||||
- job:
|
||||
displayName: Push to pre-release feed
|
||||
@@ -558,7 +543,7 @@ stages:
|
||||
dependsOn:
|
||||
- Deploy_MyGet
|
||||
- Build_Docs
|
||||
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.nuGetDeploy}}))
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.nuGetDeploy}}))
|
||||
jobs:
|
||||
- job:
|
||||
displayName: Push to NuGet
|
||||
@@ -581,12 +566,12 @@ stages:
|
||||
pool:
|
||||
vmImage: 'windows-latest' # Apparently AzureFileCopy is windows only :(
|
||||
variables:
|
||||
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['determineMajorVersion.majorVersion'] ]
|
||||
umbracoMajorVersion: $[ stageDependencies.Build.A.outputs['build.NBGV_VersionMajor'] ]
|
||||
displayName: Upload API Documention
|
||||
dependsOn:
|
||||
- Build
|
||||
- Deploy_NuGet
|
||||
condition: and(succeeded(), or(startsWith(variables['Build.SourceBranch'], 'refs/heads/release/'), ${{parameters.uploadApiDocs}}))
|
||||
condition: and(succeeded(), or(eq(dependencies.Build.outputs['A.build.NBGV_PublicRelease'], 'True'), ${{parameters.uploadApiDocs}}))
|
||||
jobs:
|
||||
- job:
|
||||
displayName: Upload C# Docs
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
using System.Security.Cryptography;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.IdentityModel.Tokens;
|
||||
using Umbraco.Cms.Api.Common.Security;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Infrastructure.HostedServices;
|
||||
|
||||
namespace Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
|
||||
public static class UmbracoBuilderAuthExtensions
|
||||
{
|
||||
private static bool _initialized;
|
||||
|
||||
public static IUmbracoBuilder AddUmbracoOpenIddict(this IUmbracoBuilder builder)
|
||||
{
|
||||
if (_initialized is false)
|
||||
{
|
||||
ConfigureOpenIddict(builder);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
private static void ConfigureOpenIddict(IUmbracoBuilder builder)
|
||||
{
|
||||
builder.Services.AddOpenIddict()
|
||||
// Register the OpenIddict server components.
|
||||
.AddServer(options =>
|
||||
{
|
||||
// Enable the authorization and token endpoints.
|
||||
// - important: member endpoints MUST be added before backoffice endpoints to ensure that auto-discovery works for members
|
||||
// FIXME: swap paths here so member API is first (see comment above)
|
||||
options
|
||||
.SetAuthorizationEndpointUris(
|
||||
Paths.MemberApi.AuthorizationEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetTokenEndpointUris(
|
||||
Paths.MemberApi.TokenEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetLogoutEndpointUris(
|
||||
Paths.MemberApi.LogoutEndpoint.TrimStart(Constants.CharArrays.ForwardSlash))
|
||||
.SetRevocationEndpointUris(
|
||||
Paths.MemberApi.RevokeEndpoint.TrimStart(Constants.CharArrays.ForwardSlash));
|
||||
|
||||
// Enable authorization code flow with PKCE
|
||||
options
|
||||
.AllowAuthorizationCodeFlow()
|
||||
.RequireProofKeyForCodeExchange()
|
||||
.AllowRefreshTokenFlow();
|
||||
|
||||
// Register the ASP.NET Core host and configure for custom authentication endpoint.
|
||||
options
|
||||
.UseAspNetCore()
|
||||
.EnableAuthorizationEndpointPassthrough()
|
||||
.EnableLogoutEndpointPassthrough();
|
||||
|
||||
// Enable reference tokens
|
||||
// - see https://documentation.openiddict.com/configuration/token-storage.html
|
||||
options
|
||||
.UseReferenceAccessTokens()
|
||||
.UseReferenceRefreshTokens();
|
||||
|
||||
// Use ASP.NET Core Data Protection for tokens instead of JWT.
|
||||
// This is more secure, and has the added benefit of having a high throughput
|
||||
// but means that all servers (such as in a load balanced setup)
|
||||
// needs to use the same application name and key ring,
|
||||
// however this is already recommended for load balancing, so should be fine.
|
||||
// See https://documentation.openiddict.com/configuration/token-formats.html#switching-to-data-protection-tokens
|
||||
// and https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview?view=aspnetcore-7.0
|
||||
// for more information
|
||||
options.UseDataProtection();
|
||||
|
||||
// Register encryption and signing credentials to protect tokens.
|
||||
// Note that for tokens generated/validated using ASP.NET Core Data Protection,
|
||||
// a separate key ring is used, distinct from the credentials discussed in
|
||||
// https://documentation.openiddict.com/configuration/encryption-and-signing-credentials.html
|
||||
// More details can be found here: https://github.com/openiddict/openiddict-core/issues/1892#issuecomment-1737308506
|
||||
// "When using ASP.NET Core Data Protection to generate opaque tokens, the signing and encryption credentials
|
||||
// registered via Add*Key/Certificate() are not used". But since OpenIddict requires the registration of such,
|
||||
// we can generate random keys per instance without them taking effect.
|
||||
// - see also https://github.com/openiddict/openiddict-core/issues/1231
|
||||
options
|
||||
.AddEncryptionKey(new SymmetricSecurityKey(RandomNumberGenerator.GetBytes(32))) // generate a cryptographically secure random 256-bits key
|
||||
.AddSigningKey(new RsaSecurityKey(RSA.Create(keySizeInBits: 2048))); // generate RSA key with recommended size of 2048-bits
|
||||
})
|
||||
|
||||
// Register the OpenIddict validation components.
|
||||
.AddValidation(options =>
|
||||
{
|
||||
// Import the configuration from the local OpenIddict server instance.
|
||||
options.UseLocalServer();
|
||||
|
||||
// Register the ASP.NET Core host.
|
||||
options.UseAspNetCore();
|
||||
|
||||
// Enable token entry validation
|
||||
// - see https://documentation.openiddict.com/configuration/token-storage.html#enabling-token-entry-validation-at-the-api-level
|
||||
options.EnableTokenEntryValidation();
|
||||
|
||||
// Use ASP.NET Core Data Protection for tokens instead of JWT. (see note in AddServer)
|
||||
options.UseDataProtection();
|
||||
});
|
||||
|
||||
builder.Services.AddHostedService<OpenIddictCleanup>();
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ using Microsoft.Extensions.Hosting;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Swashbuckle.AspNetCore.SwaggerUI;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Web.Common.ApplicationBuilder;
|
||||
@@ -32,17 +33,8 @@ public class SwaggerRouteTemplatePipelineFilter : UmbracoPipelineFilter
|
||||
{
|
||||
swaggerOptions.RouteTemplate = SwaggerRouteTemplate(applicationBuilder);
|
||||
});
|
||||
applicationBuilder.UseSwaggerUI(
|
||||
swaggerUiOptions =>
|
||||
{
|
||||
swaggerUiOptions.RoutePrefix = SwaggerUiRoutePrefix(applicationBuilder);
|
||||
|
||||
foreach ((var name, OpenApiInfo? apiInfo) in swaggerGenOptions.Value.SwaggerGeneratorOptions.SwaggerDocs
|
||||
.OrderBy(x => x.Value.Title))
|
||||
{
|
||||
swaggerUiOptions.SwaggerEndpoint($"{name}/swagger.json", $"{apiInfo.Title}");
|
||||
}
|
||||
});
|
||||
applicationBuilder.UseSwaggerUI(swaggerUiOptions => SwaggerUiConfiguration(swaggerUiOptions, swaggerGenOptions.Value, applicationBuilder));
|
||||
}
|
||||
|
||||
protected virtual bool SwaggerIsEnabled(IApplicationBuilder applicationBuilder)
|
||||
@@ -57,6 +49,22 @@ public class SwaggerRouteTemplatePipelineFilter : UmbracoPipelineFilter
|
||||
protected virtual string SwaggerUiRoutePrefix(IApplicationBuilder applicationBuilder)
|
||||
=> $"{GetUmbracoPath(applicationBuilder).TrimStart(Constants.CharArrays.ForwardSlash)}/swagger";
|
||||
|
||||
protected virtual void SwaggerUiConfiguration(
|
||||
SwaggerUIOptions swaggerUiOptions,
|
||||
SwaggerGenOptions swaggerGenOptions,
|
||||
IApplicationBuilder applicationBuilder)
|
||||
{
|
||||
swaggerUiOptions.RoutePrefix = SwaggerUiRoutePrefix(applicationBuilder);
|
||||
|
||||
foreach ((var name, OpenApiInfo? apiInfo) in swaggerGenOptions.SwaggerGeneratorOptions.SwaggerDocs
|
||||
.OrderBy(x => x.Value.Title))
|
||||
{
|
||||
swaggerUiOptions.SwaggerEndpoint($"{name}/swagger.json", $"{apiInfo.Title}");
|
||||
}
|
||||
|
||||
swaggerUiOptions.OAuthUsePkce();
|
||||
}
|
||||
|
||||
private string GetUmbracoPath(IApplicationBuilder applicationBuilder)
|
||||
{
|
||||
GlobalSettings settings = applicationBuilder.ApplicationServices.GetRequiredService<IOptions<GlobalSettings>>().Value;
|
||||
|
||||
20
src/Umbraco.Cms.Api.Common/Security/Paths.cs
Normal file
20
src/Umbraco.Cms.Api.Common/Security/Paths.cs
Normal file
@@ -0,0 +1,20 @@
|
||||
namespace Umbraco.Cms.Api.Common.Security;
|
||||
|
||||
public static class Paths
|
||||
{
|
||||
public static class MemberApi
|
||||
{
|
||||
public const string EndpointTemplate = "security/member";
|
||||
|
||||
public static readonly string AuthorizationEndpoint = EndpointPath($"{EndpointTemplate}/authorize");
|
||||
|
||||
public static readonly string TokenEndpoint = EndpointPath($"{EndpointTemplate}/token");
|
||||
|
||||
public static readonly string LogoutEndpoint = EndpointPath($"{EndpointTemplate}/signout");
|
||||
|
||||
public static readonly string RevokeEndpoint = EndpointPath($"{EndpointTemplate}/revoke");
|
||||
|
||||
// NOTE: we're NOT using /api/v1.0/ here because it will clash with the Delivery API docs
|
||||
private static string EndpointPath(string relativePath) => $"/umbraco/delivery/api/v1/{relativePath}";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Cms.Api.Common.Security;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers;
|
||||
using Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// This configures member authentication for the Delivery API in Swagger. Consult the docs for
|
||||
/// member authentication within the Delivery API for instructions on how to use this.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class is not used by the core CMS due to the required installation dependencies (local login page among other things).
|
||||
/// </remarks>
|
||||
public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
|
||||
{
|
||||
private const string AuthSchemeName = "Umbraco Member";
|
||||
|
||||
public void Configure(SwaggerGenOptions options)
|
||||
{
|
||||
options.AddSecurityDefinition(
|
||||
AuthSchemeName,
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
In = ParameterLocation.Header,
|
||||
Name = AuthSchemeName,
|
||||
Type = SecuritySchemeType.OAuth2,
|
||||
Description = "Umbraco Member Authentication",
|
||||
Flows = new OpenApiOAuthFlows
|
||||
{
|
||||
AuthorizationCode = new OpenApiOAuthFlow
|
||||
{
|
||||
AuthorizationUrl = new Uri(Paths.MemberApi.AuthorizationEndpoint, UriKind.Relative),
|
||||
TokenUrl = new Uri(Paths.MemberApi.TokenEndpoint, UriKind.Relative)
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// add security requirements for content API operations
|
||||
options.OperationFilter<DeliveryApiSecurityFilter>();
|
||||
}
|
||||
|
||||
private class DeliveryApiSecurityFilter : SwaggerFilterBase<ContentApiControllerBase>, IOperationFilter
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (CanApply(context) is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
operation.Security = new List<OpenApiSecurityRequirement>
|
||||
{
|
||||
new OpenApiSecurityRequirement
|
||||
{
|
||||
{
|
||||
new OpenApiSecurityScheme
|
||||
{
|
||||
Reference = new OpenApiReference
|
||||
{
|
||||
Type = ReferenceType.SecurityScheme,
|
||||
Id = AuthSchemeName,
|
||||
}
|
||||
},
|
||||
new string[] { }
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -11,14 +13,41 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
[ApiVersion("1.0")]
|
||||
public class ByIdContentApiController : ContentApiItemControllerBase
|
||||
{
|
||||
private readonly IRequestMemberAccessService _requestMemberAccessService;
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
public ByIdContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder, publicAccessService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IRequestMemberAccessService>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
public ByIdContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService,
|
||||
IRequestMemberAccessService requestMemberAccessService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
requestMemberAccessService)
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ByIdContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IRequestMemberAccessService requestMemberAccessService)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
=> _requestMemberAccessService = requestMemberAccessService;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a content item by id.
|
||||
/// </summary>
|
||||
@@ -28,6 +57,7 @@ public class ByIdContentApiController : ContentApiItemControllerBase
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ById(Guid id)
|
||||
{
|
||||
@@ -38,9 +68,10 @@ public class ByIdContentApiController : ContentApiItemControllerBase
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
if (IsProtected(contentItem))
|
||||
IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItem, _requestMemberAccessService);
|
||||
if (deniedAccessResult is not null)
|
||||
{
|
||||
return Unauthorized();
|
||||
return deniedAccessResult;
|
||||
}
|
||||
|
||||
IApiContentResponse? apiContentResponse = ApiContentResponseBuilder.Build(contentItem);
|
||||
@@ -49,6 +80,6 @@ public class ByIdContentApiController : ContentApiItemControllerBase
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
return await Task.FromResult(Ok(apiContentResponse));
|
||||
return Ok(apiContentResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -12,14 +14,41 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
[ApiVersion("1.0")]
|
||||
public class ByIdsContentApiController : ContentApiItemControllerBase
|
||||
{
|
||||
private readonly IRequestMemberAccessService _requestMemberAccessService;
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
public ByIdsContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder, publicAccessService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IRequestMemberAccessService>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
public ByIdsContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService,
|
||||
IRequestMemberAccessService requestMemberAccessService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
requestMemberAccessService)
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ByIdsContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IRequestMemberAccessService requestMemberAccessService)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
=> _requestMemberAccessService = requestMemberAccessService;
|
||||
|
||||
/// <summary>
|
||||
/// Gets content items by ids.
|
||||
/// </summary>
|
||||
@@ -28,12 +57,19 @@ public class ByIdsContentApiController : ContentApiItemControllerBase
|
||||
[HttpGet("item")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IEnumerable<IApiContentResponse>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult> Item([FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
public async Task<IActionResult> Item([FromQuery(Name = "id")] HashSet<Guid> ids)
|
||||
{
|
||||
IEnumerable<IPublishedContent> contentItems = ApiPublishedContentCache.GetByIds(ids);
|
||||
IPublishedContent[] contentItems = ApiPublishedContentCache.GetByIds(ids).ToArray();
|
||||
|
||||
IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItems, _requestMemberAccessService);
|
||||
if (deniedAccessResult is not null)
|
||||
{
|
||||
return deniedAccessResult;
|
||||
}
|
||||
|
||||
IApiContentResponse[] apiContentItems = contentItems
|
||||
.Where(contentItem => !IsProtected(contentItem))
|
||||
.Select(ApiContentResponseBuilder.Build)
|
||||
.WhereNotNull()
|
||||
.ToArray();
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -16,7 +18,9 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
private readonly IRequestRoutingService _requestRoutingService;
|
||||
private readonly IRequestRedirectService _requestRedirectService;
|
||||
private readonly IRequestPreviewService _requestPreviewService;
|
||||
private readonly IRequestMemberAccessService _requestMemberAccessService;
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
public ByRouteContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
@@ -24,11 +28,49 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
IRequestRoutingService requestRoutingService,
|
||||
IRequestRedirectService requestRedirectService,
|
||||
IRequestPreviewService requestPreviewService)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder, publicAccessService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
requestRoutingService,
|
||||
requestRedirectService,
|
||||
requestPreviewService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IRequestMemberAccessService>())
|
||||
{
|
||||
}
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
public ByRouteContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService,
|
||||
IRequestRoutingService requestRoutingService,
|
||||
IRequestRedirectService requestRedirectService,
|
||||
IRequestPreviewService requestPreviewService,
|
||||
IRequestMemberAccessService requestMemberAccessService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilder,
|
||||
requestRoutingService,
|
||||
requestRedirectService,
|
||||
requestPreviewService,
|
||||
requestMemberAccessService)
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public ByRouteContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IRequestRoutingService requestRoutingService,
|
||||
IRequestRedirectService requestRedirectService,
|
||||
IRequestPreviewService requestPreviewService,
|
||||
IRequestMemberAccessService requestMemberAccessService)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
{
|
||||
_requestRoutingService = requestRoutingService;
|
||||
_requestRedirectService = requestRedirectService;
|
||||
_requestPreviewService = requestPreviewService;
|
||||
_requestMemberAccessService = requestMemberAccessService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -44,6 +86,7 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(IApiContentResponse), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
|
||||
[ProducesResponseType(StatusCodes.Status403Forbidden)]
|
||||
[ProducesResponseType(StatusCodes.Status404NotFound)]
|
||||
public async Task<IActionResult> ByRoute(string path = "")
|
||||
{
|
||||
@@ -55,9 +98,10 @@ public class ByRouteContentApiController : ContentApiItemControllerBase
|
||||
IPublishedContent? contentItem = GetContent(path);
|
||||
if (contentItem is not null)
|
||||
{
|
||||
if (IsProtected(contentItem))
|
||||
IActionResult? deniedAccessResult = await HandleMemberAccessAsync(contentItem, _requestMemberAccessService);
|
||||
if (deniedAccessResult is not null)
|
||||
{
|
||||
return Unauthorized();
|
||||
return deniedAccessResult;
|
||||
}
|
||||
|
||||
return await Task.FromResult(Ok(ApiContentResponseBuilder.Build(contentItem)));
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Umbraco.Cms.Api.Common.Builders;
|
||||
using Umbraco.Cms.Api.Delivery.Filters;
|
||||
using Umbraco.Cms.Api.Delivery.Routing;
|
||||
@@ -44,4 +45,14 @@ public abstract class ContentApiControllerBase : DeliveryApiControllerBase
|
||||
.WithDetail($"Content query status \"{status}\" was not expected here")
|
||||
.Build()),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// Creates a 403 Forbidden result.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Use this method instead of <see cref="ControllerBase.Forbid()"/> on the controller base. The latter will yield
|
||||
/// a redirect to an access denied URL because of the default cookie auth scheme. This method ensures that a proper
|
||||
/// 403 Forbidden status code is returned to the client.
|
||||
/// </remarks>
|
||||
protected IActionResult Forbidden() => new StatusCodeResult(StatusCodes.Status403Forbidden);
|
||||
}
|
||||
|
||||
@@ -1,22 +1,58 @@
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
|
||||
public abstract class ContentApiItemControllerBase : ContentApiControllerBase
|
||||
{
|
||||
// TODO: Remove this in V14 when the obsolete constructors have been removed
|
||||
private readonly IPublicAccessService _publicAccessService;
|
||||
|
||||
[Obsolete($"Please use the constructor that does not accept {nameof(IPublicAccessService)}. Will be removed in V14.")]
|
||||
protected ContentApiItemControllerBase(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder,
|
||||
IPublicAccessService publicAccessService)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
=> _publicAccessService = publicAccessService;
|
||||
: this(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
{
|
||||
}
|
||||
|
||||
// NOTE: we're going to test for protected content at item endpoint level, because the check has already been
|
||||
// performed at content index time for the query endpoint and we don't want that extra overhead when
|
||||
// returning multiple items.
|
||||
protected ContentApiItemControllerBase(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilder)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilder)
|
||||
=> _publicAccessService = StaticServiceProvider.Instance.GetRequiredService<IPublicAccessService>();
|
||||
|
||||
[Obsolete($"Please use {nameof(IPublicAccessService)} to test for content protection. Will be removed in V14.")]
|
||||
protected bool IsProtected(IPublishedContent content) => _publicAccessService.IsProtected(content.Path);
|
||||
|
||||
protected async Task<IActionResult?> HandleMemberAccessAsync(IPublishedContent contentItem, IRequestMemberAccessService requestMemberAccessService)
|
||||
{
|
||||
PublicAccessStatus accessStatus = await requestMemberAccessService.MemberHasAccessToAsync(contentItem);
|
||||
return accessStatus is PublicAccessStatus.AccessAccepted
|
||||
? null
|
||||
: accessStatus is PublicAccessStatus.AccessDenied
|
||||
? Forbidden()
|
||||
: Unauthorized();
|
||||
}
|
||||
|
||||
protected async Task<IActionResult?> HandleMemberAccessAsync(IEnumerable<IPublishedContent> contentItems, IRequestMemberAccessService requestMemberAccessService)
|
||||
{
|
||||
foreach (IPublishedContent content in contentItems)
|
||||
{
|
||||
IActionResult? result = await HandleMemberAccessAsync(content, requestMemberAccessService);
|
||||
// if any of the content items yield an error based on the current member access, return that error
|
||||
if (result is not null)
|
||||
{
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Common.ViewModels.Pagination;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
@@ -15,14 +17,33 @@ namespace Umbraco.Cms.Api.Delivery.Controllers.Content;
|
||||
[ApiVersion("1.0")]
|
||||
public class QueryContentApiController : ContentApiControllerBase
|
||||
{
|
||||
private readonly IRequestMemberAccessService _requestMemberAccessService;
|
||||
private readonly IApiContentQueryService _apiContentQueryService;
|
||||
|
||||
[Obsolete($"Please use the constructor that accepts {nameof(IRequestMemberAccessService)}. Will be removed in V14.")]
|
||||
public QueryContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilderBuilder,
|
||||
IApiContentQueryService apiContentQueryService)
|
||||
: this(
|
||||
apiPublishedContentCache,
|
||||
apiContentResponseBuilderBuilder,
|
||||
apiContentQueryService,
|
||||
StaticServiceProvider.Instance.GetRequiredService<IRequestMemberAccessService>())
|
||||
{
|
||||
}
|
||||
|
||||
[ActivatorUtilitiesConstructor]
|
||||
public QueryContentApiController(
|
||||
IApiPublishedContentCache apiPublishedContentCache,
|
||||
IApiContentResponseBuilder apiContentResponseBuilderBuilder,
|
||||
IApiContentQueryService apiContentQueryService,
|
||||
IRequestMemberAccessService requestMemberAccessService)
|
||||
: base(apiPublishedContentCache, apiContentResponseBuilderBuilder)
|
||||
=> _apiContentQueryService = apiContentQueryService;
|
||||
{
|
||||
_apiContentQueryService = apiContentQueryService;
|
||||
_requestMemberAccessService = requestMemberAccessService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paginated list of content item(s) from query.
|
||||
@@ -45,7 +66,8 @@ public class QueryContentApiController : ContentApiControllerBase
|
||||
int skip = 0,
|
||||
int take = 10)
|
||||
{
|
||||
Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> queryAttempt = _apiContentQueryService.ExecuteQuery(fetch, filter, sort, skip, take);
|
||||
ProtectedAccess protectedAccess = await _requestMemberAccessService.MemberAccessAsync();
|
||||
Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> queryAttempt = _apiContentQueryService.ExecuteQuery(fetch, filter, sort, protectedAccess, skip, take);
|
||||
|
||||
if (queryAttempt.Success is false)
|
||||
{
|
||||
@@ -62,6 +84,6 @@ public class QueryContentApiController : ContentApiControllerBase
|
||||
Items = apiContentItems
|
||||
};
|
||||
|
||||
return await Task.FromResult(Ok(model));
|
||||
return Ok(model);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
using System.Security.Claims;
|
||||
using Asp.Versioning;
|
||||
using Microsoft.AspNetCore;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Server.AspNetCore;
|
||||
using Umbraco.Cms.Api.Delivery.Routing;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Web.Common.Security;
|
||||
using Umbraco.Extensions;
|
||||
using SignInResult = Microsoft.AspNetCore.Mvc.SignInResult;
|
||||
using IdentitySignInResult = Microsoft.AspNetCore.Identity.SignInResult;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Controllers.Security;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
[ApiController]
|
||||
[VersionedDeliveryApiRoute(Common.Security.Paths.MemberApi.EndpointTemplate)]
|
||||
[ApiExplorerSettings(IgnoreApi = true)]
|
||||
public class MemberController : DeliveryApiControllerBase
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IMemberSignInManager _memberSignInManager;
|
||||
private readonly IMemberManager _memberManager;
|
||||
private readonly DeliveryApiSettings _deliveryApiSettings;
|
||||
private readonly ILogger<MemberController> _logger;
|
||||
|
||||
public MemberController(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IMemberSignInManager memberSignInManager,
|
||||
IMemberManager memberManager,
|
||||
IOptions<DeliveryApiSettings> deliveryApiSettings,
|
||||
ILogger<MemberController> logger)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_memberSignInManager = memberSignInManager;
|
||||
_memberManager = memberManager;
|
||||
_logger = logger;
|
||||
_deliveryApiSettings = deliveryApiSettings.Value;
|
||||
}
|
||||
|
||||
[HttpGet("authorize")]
|
||||
[MapToApiVersion("1.0")]
|
||||
public async Task<IActionResult> Authorize()
|
||||
{
|
||||
// in principle this is not necessary for now, since the member application has been removed, thus making
|
||||
// the member client ID invalid for the authentication code flow. However, if we ever add additional flows
|
||||
// to the API, we should perform this check, so we might as well include it upfront.
|
||||
if (_deliveryApiSettings.MemberAuthorizationIsEnabled() is false)
|
||||
{
|
||||
return BadRequest("Member authorization is not allowed.");
|
||||
}
|
||||
|
||||
HttpContext context = _httpContextAccessor.GetRequiredHttpContext();
|
||||
OpenIddictRequest? request = context.GetOpenIddictServerRequest();
|
||||
if (request is null)
|
||||
{
|
||||
return BadRequest("Unable to obtain OpenID data from the current request.");
|
||||
}
|
||||
|
||||
// make sure this endpoint ONLY handles member authentication
|
||||
if (request.ClientId is not Constants.OAuthClientIds.Member)
|
||||
{
|
||||
return BadRequest("The specified client ID cannot be used here.");
|
||||
}
|
||||
|
||||
return request.IdentityProvider.IsNullOrWhiteSpace()
|
||||
? await AuthorizeInternal(request)
|
||||
: await AuthorizeExternal(request);
|
||||
}
|
||||
|
||||
[HttpGet("signout")]
|
||||
[MapToApiVersion("1.0")]
|
||||
public async Task<IActionResult> Signout()
|
||||
{
|
||||
await _memberSignInManager.SignOutAsync();
|
||||
return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> AuthorizeInternal(OpenIddictRequest request)
|
||||
{
|
||||
// retrieve the user principal stored in the authentication cookie.
|
||||
AuthenticateResult cookieAuthResult = await HttpContext.AuthenticateAsync(IdentityConstants.ApplicationScheme);
|
||||
var userName = cookieAuthResult.Succeeded
|
||||
? cookieAuthResult.Principal?.Identity?.Name
|
||||
: null;
|
||||
|
||||
if (userName is null)
|
||||
{
|
||||
return Challenge(IdentityConstants.ApplicationScheme);
|
||||
}
|
||||
|
||||
MemberIdentityUser? member = await _memberManager.FindByNameAsync(userName);
|
||||
if (member is null)
|
||||
{
|
||||
_logger.LogError("The member with username {userName} was successfully authorized, but could not be retrieved by the member manager", userName);
|
||||
return BadRequest("The member could not be found.");
|
||||
}
|
||||
|
||||
return await SignInMember(member, request);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> AuthorizeExternal(OpenIddictRequest request)
|
||||
{
|
||||
var provider = request.IdentityProvider ?? throw new ArgumentException("No identity provider found in request", nameof(request));
|
||||
ExternalLoginInfo? loginInfo = await _memberSignInManager.GetExternalLoginInfoAsync();
|
||||
|
||||
if (loginInfo?.Principal is null)
|
||||
{
|
||||
AuthenticationProperties properties = _memberSignInManager.ConfigureExternalAuthenticationProperties(provider, null);
|
||||
return Challenge(properties, provider);
|
||||
}
|
||||
|
||||
// NOTE: if we're going to support 2FA for members, we need to:
|
||||
// - use SecuritySettings.MemberBypassTwoFactorForExternalLogins instead of the hardcoded value (true) for "bypassTwoFactor".
|
||||
// - handle IdentitySignInResult.TwoFactorRequired
|
||||
IdentitySignInResult result = await _memberSignInManager.ExternalLoginSignInAsync(loginInfo, false, true);
|
||||
if (result == IdentitySignInResult.Success)
|
||||
{
|
||||
// get the member and perform sign-in
|
||||
MemberIdentityUser? member = await _memberManager.FindByLoginAsync(loginInfo.LoginProvider, loginInfo.ProviderKey);
|
||||
if (member is null)
|
||||
{
|
||||
_logger.LogError("A member was successfully authorized using external authentication, but could not be retrieved by the member manager");
|
||||
return BadRequest("The member could not be found.");
|
||||
}
|
||||
|
||||
// update member authentication tokens if succeeded
|
||||
await _memberSignInManager.UpdateExternalAuthenticationTokensAsync(loginInfo);
|
||||
return await SignInMember(member, request);
|
||||
}
|
||||
|
||||
var errorProperties = new AuthenticationProperties(new Dictionary<string, string?>
|
||||
{
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.Error] = OpenIddictConstants.Errors.AccessDenied,
|
||||
[OpenIddictServerAspNetCoreConstants.Properties.ErrorDescription] = "The member is not allowed to access this resource."
|
||||
});
|
||||
return Forbid(errorProperties, provider);
|
||||
}
|
||||
|
||||
private async Task<IActionResult> SignInMember(MemberIdentityUser member, OpenIddictRequest request)
|
||||
{
|
||||
ClaimsPrincipal memberPrincipal = await _memberSignInManager.CreateUserPrincipalAsync(member);
|
||||
memberPrincipal.SetClaim(OpenIddictConstants.Claims.Subject, member.Key.ToString());
|
||||
|
||||
IList<string> roles = await _memberManager.GetRolesAsync(member);
|
||||
memberPrincipal.SetClaim(Constants.OAuthClaims.MemberKey, member.Key.ToString());
|
||||
memberPrincipal.SetClaim(Constants.OAuthClaims.MemberRoles, string.Join(",", roles));
|
||||
|
||||
Claim[] claims = memberPrincipal.Claims.ToArray();
|
||||
foreach (Claim claim in claims.Where(claim => claim.Type is not Constants.Security.SecurityStampClaimType))
|
||||
{
|
||||
claim.SetDestinations(OpenIddictConstants.Destinations.AccessToken);
|
||||
}
|
||||
|
||||
if (request.GetScopes().Contains(OpenIddictConstants.Scopes.OfflineAccess))
|
||||
{
|
||||
// "offline_access" scope is required to use refresh tokens
|
||||
memberPrincipal.SetScopes(OpenIddictConstants.Scopes.OfflineAccess);
|
||||
}
|
||||
|
||||
return new SignInResult(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme, memberPrincipal);
|
||||
}
|
||||
}
|
||||
@@ -1,15 +1,21 @@
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Common.DependencyInjection;
|
||||
using Umbraco.Cms.Api.Delivery.Accessors;
|
||||
using Umbraco.Cms.Api.Delivery.Configuration;
|
||||
using Umbraco.Cms.Api.Delivery.Handlers;
|
||||
using Umbraco.Cms.Api.Delivery.Json;
|
||||
using Umbraco.Cms.Api.Delivery.Rendering;
|
||||
using Umbraco.Cms.Api.Delivery.Routing;
|
||||
using Umbraco.Cms.Api.Delivery.Security;
|
||||
using Umbraco.Cms.Api.Delivery.Services;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Infrastructure.Security;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
@@ -29,6 +35,8 @@ public static class UmbracoBuilderExtensions
|
||||
builder.Services.AddSingleton<IApiContentQueryService, ApiContentQueryService>();
|
||||
builder.Services.AddSingleton<IApiContentQueryProvider, ApiContentQueryProvider>();
|
||||
builder.Services.AddSingleton<IApiMediaQueryService, ApiMediaQueryService>();
|
||||
builder.Services.AddTransient<IMemberApplicationManager, MemberApplicationManager>();
|
||||
builder.Services.AddTransient<IRequestMemberAccessService, RequestMemberAccessService>();
|
||||
|
||||
builder.Services.ConfigureOptions<ConfigureUmbracoDeliveryApiSwaggerGenOptions>();
|
||||
builder.AddUmbracoApiOpenApiUI();
|
||||
@@ -44,6 +52,16 @@ public static class UmbracoBuilderExtensions
|
||||
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
|
||||
});
|
||||
|
||||
builder.Services.AddAuthentication();
|
||||
builder.AddUmbracoOpenIddict();
|
||||
builder.AddNotificationAsyncHandler<UmbracoApplicationStartingNotification, InitializeMemberApplicationNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<MemberSavedNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<MemberDeletedNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<AssignedMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
|
||||
builder.AddNotificationAsyncHandler<RemovedMemberRolesNotification, RevokeMemberAuthenticationTokensNotificationHandler>();
|
||||
|
||||
// FIXME: remove this when Delivery API V1 is removed
|
||||
builder.Services.AddSingleton<MatcherPolicy, DeliveryApiItemsEndpointsMatcherPolicy>();
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.OpenApi.Any;
|
||||
using Microsoft.OpenApi.Models;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal abstract class SwaggerDocumentationFilterBase<TBaseController> : IOperationFilter, IParameterFilter
|
||||
internal abstract class SwaggerDocumentationFilterBase<TBaseController>
|
||||
: SwaggerFilterBase<TBaseController>, IOperationFilter, IParameterFilter
|
||||
where TBaseController : Controller
|
||||
{
|
||||
public void Apply(OpenApiOperation operation, OperationFilterContext context)
|
||||
{
|
||||
if (CanApply(context.MethodInfo))
|
||||
if (CanApply(context))
|
||||
{
|
||||
ApplyOperation(operation, context);
|
||||
}
|
||||
@@ -20,7 +19,7 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController> : IOpera
|
||||
|
||||
public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
|
||||
{
|
||||
if (CanApply(context.ParameterInfo.Member))
|
||||
if (CanApply(context))
|
||||
{
|
||||
ApplyParameter(parameter, context);
|
||||
}
|
||||
@@ -76,7 +75,4 @@ internal abstract class SwaggerDocumentationFilterBase<TBaseController> : IOpera
|
||||
|
||||
private string QueryParameterDescription(string description)
|
||||
=> $"{description}. Refer to [the documentation]({DocumentationLink}#query-parameters) for more details on this.";
|
||||
|
||||
private bool CanApply(MemberInfo member)
|
||||
=> member.DeclaringType?.Implements<TBaseController>() is true;
|
||||
}
|
||||
|
||||
19
src/Umbraco.Cms.Api.Delivery/Filters/SwaggerFilterBase.cs
Normal file
19
src/Umbraco.Cms.Api.Delivery/Filters/SwaggerFilterBase.cs
Normal file
@@ -0,0 +1,19 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Swashbuckle.AspNetCore.SwaggerGen;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Filters;
|
||||
|
||||
internal abstract class SwaggerFilterBase<TBaseController>
|
||||
where TBaseController : Controller
|
||||
{
|
||||
protected bool CanApply(OperationFilterContext context)
|
||||
=> CanApply(context.MethodInfo);
|
||||
|
||||
protected bool CanApply(ParameterFilterContext context)
|
||||
=> CanApply(context.ParameterInfo.Member);
|
||||
|
||||
private bool CanApply(MemberInfo member)
|
||||
=> member.DeclaringType?.Implements<TBaseController>() is true;
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Security;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Handlers;
|
||||
|
||||
internal sealed class InitializeMemberApplicationNotificationHandler : INotificationAsyncHandler<UmbracoApplicationStartingNotification>
|
||||
{
|
||||
private readonly IMemberApplicationManager _memberApplicationManager;
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
private readonly ILogger<InitializeMemberApplicationNotificationHandler> _logger;
|
||||
private readonly DeliveryApiSettings _deliveryApiSettings;
|
||||
|
||||
public InitializeMemberApplicationNotificationHandler(
|
||||
IMemberApplicationManager memberApplicationManager,
|
||||
IRuntimeState runtimeState,
|
||||
IOptions<DeliveryApiSettings> deliveryApiSettings,
|
||||
ILogger<InitializeMemberApplicationNotificationHandler> logger)
|
||||
{
|
||||
_memberApplicationManager = memberApplicationManager;
|
||||
_runtimeState = runtimeState;
|
||||
_logger = logger;
|
||||
_deliveryApiSettings = deliveryApiSettings.Value;
|
||||
}
|
||||
|
||||
public async Task HandleAsync(UmbracoApplicationStartingNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_runtimeState.Level != RuntimeLevel.Run)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_deliveryApiSettings.MemberAuthorization?.AuthorizationCodeFlow?.Enabled is not true)
|
||||
{
|
||||
await _memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls) is false)
|
||||
{
|
||||
await _memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls.Any()
|
||||
&& ValidateRedirectUrls(_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls) is false)
|
||||
{
|
||||
await _memberApplicationManager.DeleteMemberApplicationAsync(cancellationToken);
|
||||
return;
|
||||
}
|
||||
|
||||
await _memberApplicationManager.EnsureMemberApplicationAsync(
|
||||
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LoginRedirectUrls,
|
||||
_deliveryApiSettings.MemberAuthorization.AuthorizationCodeFlow.LogoutRedirectUrls,
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
private bool ValidateRedirectUrls(Uri[] redirectUrls)
|
||||
{
|
||||
if (redirectUrls.Any() is false)
|
||||
{
|
||||
_logger.LogWarning("No redirect URLs defined for Delivery API member authentication - cannot enable member authentication");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (redirectUrls.All(url => url.IsAbsoluteUri) is false)
|
||||
{
|
||||
_logger.LogWarning("All redirect URLs defined for Delivery API member authentication must be absolute - cannot enable member authentication");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Abstractions;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Handlers;
|
||||
|
||||
internal sealed class RevokeMemberAuthenticationTokensNotificationHandler
|
||||
: INotificationAsyncHandler<MemberSavedNotification>,
|
||||
INotificationAsyncHandler<MemberDeletedNotification>,
|
||||
INotificationAsyncHandler<AssignedMemberRolesNotification>,
|
||||
INotificationAsyncHandler<RemovedMemberRolesNotification>
|
||||
{
|
||||
private readonly IMemberService _memberService;
|
||||
private readonly IOpenIddictTokenManager _tokenManager;
|
||||
private readonly bool _enabled;
|
||||
private readonly ILogger<RevokeMemberAuthenticationTokensNotificationHandler> _logger;
|
||||
|
||||
public RevokeMemberAuthenticationTokensNotificationHandler(
|
||||
IMemberService memberService,
|
||||
IOpenIddictTokenManager tokenManager,
|
||||
IOptions<DeliveryApiSettings> deliveryApiSettings,
|
||||
ILogger<RevokeMemberAuthenticationTokensNotificationHandler> logger)
|
||||
{
|
||||
_memberService = memberService;
|
||||
_tokenManager = tokenManager;
|
||||
_logger = logger;
|
||||
_enabled = deliveryApiSettings.Value.MemberAuthorizationIsEnabled();
|
||||
}
|
||||
|
||||
public async Task HandleAsync(MemberSavedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_enabled is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (IMember member in notification.SavedEntities.Where(member => member.IsLockedOut || member.IsApproved is false))
|
||||
{
|
||||
// member is locked out and/or un-approved, make sure we revoke all tokens
|
||||
await RevokeTokensAsync(member);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandleAsync(MemberDeletedNotification notification, CancellationToken cancellationToken)
|
||||
{
|
||||
if (_enabled is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (IMember member in notification.DeletedEntities)
|
||||
{
|
||||
await RevokeTokensAsync(member);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task HandleAsync(AssignedMemberRolesNotification notification, CancellationToken cancellationToken)
|
||||
=> await MemberRolesChangedAsync(notification);
|
||||
|
||||
public async Task HandleAsync(RemovedMemberRolesNotification notification, CancellationToken cancellationToken)
|
||||
=> await MemberRolesChangedAsync(notification);
|
||||
|
||||
private async Task RevokeTokensAsync(IMember member)
|
||||
{
|
||||
var tokens = await _tokenManager.FindBySubjectAsync(member.Key.ToString()).ToArrayAsync();
|
||||
if (tokens.Any() is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logger.LogInformation("Deleting {count} active tokens for member with ID {id}", tokens.Length, member.Id);
|
||||
foreach (var token in tokens)
|
||||
{
|
||||
await _tokenManager.DeleteAsync(token);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task MemberRolesChangedAsync(MemberRolesNotification notification)
|
||||
{
|
||||
if (_enabled is false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var memberId in notification.MemberIds)
|
||||
{
|
||||
IMember? member = _memberService.GetById(memberId);
|
||||
if (member is null)
|
||||
{
|
||||
_logger.LogWarning("Unable to find member with ID {id}", memberId);
|
||||
continue;
|
||||
}
|
||||
|
||||
await RevokeTokensAsync(member);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Mvc.Controllers;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Microsoft.AspNetCore.Routing.Matching;
|
||||
using Umbraco.Cms.Api.Delivery.Controllers;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Routing;
|
||||
|
||||
// FIXME: remove this when Delivery API V1 is removed
|
||||
internal sealed class DeliveryApiItemsEndpointsMatcherPolicy : MatcherPolicy, IEndpointSelectorPolicy
|
||||
{
|
||||
public override int Order => 100;
|
||||
|
||||
public bool AppliesToEndpoints(IReadOnlyList<Endpoint> endpoints)
|
||||
{
|
||||
for (var i = 0; i < endpoints.Count; i++)
|
||||
{
|
||||
ControllerActionDescriptor? controllerActionDescriptor = endpoints[i].Metadata.GetMetadata<ControllerActionDescriptor>();
|
||||
if (IsByIdsController(controllerActionDescriptor) || IsByRouteController(controllerActionDescriptor))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates)
|
||||
{
|
||||
var hasIdQueryParameter = httpContext.Request.Query.ContainsKey("id");
|
||||
for (var i = 0; i < candidates.Count; i++)
|
||||
{
|
||||
ControllerActionDescriptor? controllerActionDescriptor = candidates[i].Endpoint?.Metadata.GetMetadata<ControllerActionDescriptor>();
|
||||
if (IsByIdsController(controllerActionDescriptor))
|
||||
{
|
||||
candidates.SetValidity(i, hasIdQueryParameter);
|
||||
}
|
||||
else if (IsByRouteController(controllerActionDescriptor))
|
||||
{
|
||||
candidates.SetValidity(i, hasIdQueryParameter is false);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static bool IsByIdsController(ControllerActionDescriptor? controllerActionDescriptor)
|
||||
=> IsControllerType<ByIdsContentApiController>(controllerActionDescriptor) || IsControllerType<ByIdsMediaApiController>(controllerActionDescriptor);
|
||||
|
||||
private static bool IsByRouteController(ControllerActionDescriptor? controllerActionDescriptor)
|
||||
=> IsControllerType<ByRouteContentApiController>(controllerActionDescriptor) || IsControllerType<ByPathMediaApiController>(controllerActionDescriptor);
|
||||
|
||||
private static bool IsControllerType<T>(ControllerActionDescriptor? controllerActionDescriptor)
|
||||
=> controllerActionDescriptor?.MethodInfo.DeclaringType == typeof(T);
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
using OpenIddict.Abstractions;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.Security;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Security;
|
||||
|
||||
public class MemberApplicationManager : OpenIdDictApplicationManagerBase, IMemberApplicationManager
|
||||
{
|
||||
private readonly IRuntimeState _runtimeState;
|
||||
|
||||
public MemberApplicationManager(IOpenIddictApplicationManager applicationManager, IRuntimeState runtimeState)
|
||||
: base(applicationManager)
|
||||
=> _runtimeState = runtimeState;
|
||||
|
||||
public async Task EnsureMemberApplicationAsync(IEnumerable<Uri> loginRedirectUrls, IEnumerable<Uri> logoutRedirectUrls, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_runtimeState.Level < RuntimeLevel.Run)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Uri[] loginRedirectUrlsArray = loginRedirectUrls as Uri[] ?? loginRedirectUrls.ToArray();
|
||||
if (loginRedirectUrlsArray.All(r => r.IsAbsoluteUri) is false)
|
||||
{
|
||||
throw new ArgumentException("Expected absolute login redirect URLs for Delivery API member authentication", nameof(loginRedirectUrls));
|
||||
}
|
||||
|
||||
Uri[] logoutRedirectUrlsArray = logoutRedirectUrls as Uri[] ?? logoutRedirectUrls.ToArray();
|
||||
if (logoutRedirectUrlsArray.All(r => r.IsAbsoluteUri) is false)
|
||||
{
|
||||
throw new ArgumentException("Expected absolute logout redirect URLs for Delivery API member authentication", nameof(logoutRedirectUrlsArray));
|
||||
}
|
||||
|
||||
var applicationDescriptor = new OpenIddictApplicationDescriptor
|
||||
{
|
||||
DisplayName = "Umbraco member access",
|
||||
ClientId = Constants.OAuthClientIds.Member,
|
||||
Type = OpenIddictConstants.ClientTypes.Public,
|
||||
Permissions =
|
||||
{
|
||||
OpenIddictConstants.Permissions.Endpoints.Authorization,
|
||||
OpenIddictConstants.Permissions.Endpoints.Token,
|
||||
OpenIddictConstants.Permissions.Endpoints.Logout,
|
||||
OpenIddictConstants.Permissions.Endpoints.Revocation,
|
||||
OpenIddictConstants.Permissions.GrantTypes.AuthorizationCode,
|
||||
OpenIddictConstants.Permissions.GrantTypes.RefreshToken,
|
||||
OpenIddictConstants.Permissions.ResponseTypes.Code
|
||||
}
|
||||
};
|
||||
|
||||
foreach (Uri redirectUrl in loginRedirectUrlsArray)
|
||||
{
|
||||
applicationDescriptor.RedirectUris.Add(redirectUrl);
|
||||
}
|
||||
|
||||
foreach (Uri redirectUrl in logoutRedirectUrlsArray)
|
||||
{
|
||||
applicationDescriptor.PostLogoutRedirectUris.Add(redirectUrl);
|
||||
}
|
||||
|
||||
await CreateOrUpdate(applicationDescriptor, cancellationToken);
|
||||
}
|
||||
|
||||
public async Task DeleteMemberApplicationAsync(CancellationToken cancellationToken = default)
|
||||
=> await Delete(Constants.OAuthClientIds.Member, cancellationToken);
|
||||
}
|
||||
@@ -3,9 +3,12 @@ using Examine.Lucene.Providers;
|
||||
using Examine.Lucene.Search;
|
||||
using Examine.Search;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Infrastructure.Examine;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -18,6 +21,7 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider
|
||||
{
|
||||
private const string ItemIdFieldName = "itemId";
|
||||
private readonly IExamineManager _examineManager;
|
||||
private readonly DeliveryApiSettings _deliveryApiSettings;
|
||||
private readonly ILogger<ApiContentQueryProvider> _logger;
|
||||
private readonly string _fallbackGuidValue;
|
||||
private readonly Dictionary<string, FieldType> _fieldTypes;
|
||||
@@ -25,9 +29,11 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider
|
||||
public ApiContentQueryProvider(
|
||||
IExamineManager examineManager,
|
||||
ContentIndexHandlerCollection indexHandlers,
|
||||
IOptions<DeliveryApiSettings> deliveryApiSettings,
|
||||
ILogger<ApiContentQueryProvider> logger)
|
||||
{
|
||||
_examineManager = examineManager;
|
||||
_deliveryApiSettings = deliveryApiSettings.Value;
|
||||
_logger = logger;
|
||||
|
||||
// A fallback value is needed for Examine queries in case we don't have a value - we can't pass null or empty string
|
||||
@@ -41,7 +47,27 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider
|
||||
.ToDictionary(field => field.FieldName, field => field.FieldType, StringComparer.InvariantCultureIgnoreCase);
|
||||
}
|
||||
|
||||
public PagedModel<Guid> ExecuteQuery(SelectorOption selectorOption, IList<FilterOption> filterOptions, IList<SortOption> sortOptions, string culture, bool preview, int skip, int take)
|
||||
[Obsolete($"Use the {nameof(ExecuteQuery)} method that accepts {nameof(ProtectedAccess)}. Will be removed in V14.")]
|
||||
public PagedModel<Guid> ExecuteQuery(
|
||||
SelectorOption selectorOption,
|
||||
IList<FilterOption> filterOptions,
|
||||
IList<SortOption> sortOptions,
|
||||
string culture,
|
||||
bool preview,
|
||||
int skip,
|
||||
int take)
|
||||
=> ExecuteQuery(selectorOption, filterOptions, sortOptions, culture, ProtectedAccess.None, preview, skip, take);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public PagedModel<Guid> ExecuteQuery(
|
||||
SelectorOption selectorOption,
|
||||
IList<FilterOption> filterOptions,
|
||||
IList<SortOption> sortOptions,
|
||||
string culture,
|
||||
ProtectedAccess protectedAccess,
|
||||
bool preview,
|
||||
int skip,
|
||||
int take)
|
||||
{
|
||||
if (!_examineManager.TryGetIndex(Constants.UmbracoIndexes.DeliveryApiContentIndexName, out IIndex? index))
|
||||
{
|
||||
@@ -49,7 +75,7 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider
|
||||
return new PagedModel<Guid>();
|
||||
}
|
||||
|
||||
IBooleanOperation queryOperation = BuildSelectorOperation(selectorOption, index, culture, preview);
|
||||
IBooleanOperation queryOperation = BuildSelectorOperation(selectorOption, index, culture, protectedAccess, preview);
|
||||
|
||||
ApplyFiltering(filterOptions, queryOperation);
|
||||
ApplySorting(sortOptions, queryOperation);
|
||||
@@ -77,7 +103,7 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider
|
||||
FieldName = UmbracoExamineFieldNames.CategoryFieldName, Values = new[] { "content" }
|
||||
};
|
||||
|
||||
private IBooleanOperation BuildSelectorOperation(SelectorOption selectorOption, IIndex index, string culture, bool preview)
|
||||
private IBooleanOperation BuildSelectorOperation(SelectorOption selectorOption, IIndex index, string culture, ProtectedAccess protectedAccess, bool preview)
|
||||
{
|
||||
// Needed for enabling leading wildcards searches
|
||||
BaseLuceneSearcher searcher = index.Searcher as BaseLuceneSearcher ?? throw new InvalidOperationException($"Index searcher must be of type {nameof(BaseLuceneSearcher)}.");
|
||||
@@ -92,8 +118,12 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider
|
||||
? query.Field(selectorOption.FieldName, selectorOption.Values.First())
|
||||
: query.GroupedOr(new[] { selectorOption.FieldName }, selectorOption.Values);
|
||||
|
||||
// Item culture must be either the requested culture or "none"
|
||||
selectorOperation.And().GroupedOr(new[] { UmbracoExamineFieldNames.DeliveryApiContentIndex.Culture }, culture.ToLowerInvariant().IfNullOrWhiteSpace(_fallbackGuidValue), "none");
|
||||
AddCultureQuery(culture, selectorOperation);
|
||||
|
||||
if (_deliveryApiSettings.MemberAuthorizationIsEnabled())
|
||||
{
|
||||
AddProtectedAccessQuery(protectedAccess, selectorOperation);
|
||||
}
|
||||
|
||||
// when not fetching for preview, make sure the "published" field is "y"
|
||||
if (preview is false)
|
||||
@@ -104,6 +134,45 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider
|
||||
return selectorOperation;
|
||||
}
|
||||
|
||||
private void AddCultureQuery(string culture, IBooleanOperation selectorOperation) =>
|
||||
selectorOperation
|
||||
.And()
|
||||
.GroupedOr(
|
||||
// Item culture must be either the requested culture or "none"
|
||||
new[] { UmbracoExamineFieldNames.DeliveryApiContentIndex.Culture },
|
||||
culture.ToLowerInvariant().IfNullOrWhiteSpace(_fallbackGuidValue),
|
||||
"none");
|
||||
|
||||
private void AddProtectedAccessQuery(ProtectedAccess protectedAccess, IBooleanOperation selectorOperation)
|
||||
{
|
||||
var protectedAccessValues = new List<string>();
|
||||
if (protectedAccess.MemberKey is not null)
|
||||
{
|
||||
protectedAccessValues.Add($"u:{protectedAccess.MemberKey}");
|
||||
}
|
||||
|
||||
if (protectedAccess.MemberRoles?.Any() is true)
|
||||
{
|
||||
protectedAccessValues.AddRange(protectedAccess.MemberRoles.Select(r => $"r:{r}"));
|
||||
}
|
||||
|
||||
if (protectedAccessValues.Any())
|
||||
{
|
||||
selectorOperation.And(
|
||||
inner => inner
|
||||
.Field(UmbracoExamineFieldNames.DeliveryApiContentIndex.Protected, "n")
|
||||
.Or(protectedAccessInner => protectedAccessInner
|
||||
.GroupedOr(
|
||||
new[] { UmbracoExamineFieldNames.DeliveryApiContentIndex.ProtectedAccess },
|
||||
protectedAccessValues.ToArray())),
|
||||
BooleanOperation.Or);
|
||||
}
|
||||
else
|
||||
{
|
||||
selectorOperation.And().Field(UmbracoExamineFieldNames.DeliveryApiContentIndex.Protected, "n");
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplyFiltering(IList<FilterOption> filterOptions, IBooleanOperation queryOperation)
|
||||
{
|
||||
void HandleExact(IQuery query, string fieldName, string[] values)
|
||||
|
||||
@@ -2,6 +2,7 @@ using Umbraco.Cms.Api.Delivery.Indexing.Selectors;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Extensions;
|
||||
@@ -36,8 +37,23 @@ internal sealed class ApiContentQueryService : IApiContentQueryService
|
||||
_requestPreviewService = requestPreviewService;
|
||||
}
|
||||
|
||||
[Obsolete($"Use the {nameof(ExecuteQuery)} method that accepts {nameof(ProtectedAccess)}. Will be removed in V14.")]
|
||||
public Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> ExecuteQuery(
|
||||
string? fetch,
|
||||
IEnumerable<string> filters,
|
||||
IEnumerable<string> sorts,
|
||||
int skip,
|
||||
int take)
|
||||
=> ExecuteQuery(fetch, filters, sorts, ProtectedAccess.None, skip, take);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take)
|
||||
public Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> ExecuteQuery(
|
||||
string? fetch,
|
||||
IEnumerable<string> filters,
|
||||
IEnumerable<string> sorts,
|
||||
ProtectedAccess protectedAccess,
|
||||
int skip,
|
||||
int take)
|
||||
{
|
||||
var emptyResult = new PagedModel<Guid>();
|
||||
|
||||
@@ -77,7 +93,7 @@ internal sealed class ApiContentQueryService : IApiContentQueryService
|
||||
var culture = _variationContextAccessor.VariationContext?.Culture ?? string.Empty;
|
||||
var isPreview = _requestPreviewService.IsPreview();
|
||||
|
||||
PagedModel<Guid> result = _apiContentQueryProvider.ExecuteQuery(selectorOption, filterOptions, sortOptions, culture, isPreview, skip, take);
|
||||
PagedModel<Guid> result = _apiContentQueryProvider.ExecuteQuery(selectorOption, filterOptions, sortOptions, culture, protectedAccess, isPreview, skip, take);
|
||||
return Attempt.SucceedWithStatus(ApiContentQueryOperationStatus.Success, result);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
using System.Security.Claims;
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Options;
|
||||
using OpenIddict.Abstractions;
|
||||
using OpenIddict.Validation.AspNetCore;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Api.Delivery.Services;
|
||||
|
||||
internal sealed class RequestMemberAccessService : IRequestMemberAccessService
|
||||
{
|
||||
private readonly IHttpContextAccessor _httpContextAccessor;
|
||||
private readonly IPublicAccessService _publicAccessService;
|
||||
private readonly IPublicAccessChecker _publicAccessChecker;
|
||||
private readonly DeliveryApiSettings _deliveryApiSettings;
|
||||
|
||||
public RequestMemberAccessService(
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IPublicAccessService publicAccessService,
|
||||
IPublicAccessChecker publicAccessChecker,
|
||||
IOptions<DeliveryApiSettings> deliveryApiSettings)
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
_publicAccessService = publicAccessService;
|
||||
_publicAccessChecker = publicAccessChecker;
|
||||
|
||||
_deliveryApiSettings = deliveryApiSettings.Value;
|
||||
}
|
||||
|
||||
public async Task<PublicAccessStatus> MemberHasAccessToAsync(IPublishedContent content)
|
||||
{
|
||||
PublicAccessEntry? publicAccessEntry = _publicAccessService.GetEntryForContent(content.Path);
|
||||
if (publicAccessEntry is null)
|
||||
{
|
||||
return PublicAccessStatus.AccessAccepted;
|
||||
}
|
||||
|
||||
ClaimsPrincipal? requestPrincipal = await GetRequestPrincipal();
|
||||
if (requestPrincipal is null)
|
||||
{
|
||||
return PublicAccessStatus.NotLoggedIn;
|
||||
}
|
||||
|
||||
return await _publicAccessChecker.HasMemberAccessToContentAsync(content.Id, requestPrincipal);
|
||||
}
|
||||
|
||||
public async Task<ProtectedAccess> MemberAccessAsync()
|
||||
{
|
||||
ClaimsPrincipal? requestPrincipal = await GetRequestPrincipal();
|
||||
return new ProtectedAccess(MemberKey(requestPrincipal), MemberRoles(requestPrincipal));
|
||||
}
|
||||
|
||||
private async Task<ClaimsPrincipal?> GetRequestPrincipal()
|
||||
{
|
||||
// exit fast if no member authorization is enabled whatsoever
|
||||
if (_deliveryApiSettings.MemberAuthorizationIsEnabled() is false)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
HttpContext httpContext = _httpContextAccessor.GetRequiredHttpContext();
|
||||
AuthenticateResult result = await httpContext.AuthenticateAsync(OpenIddictValidationAspNetCoreDefaults.AuthenticationScheme);
|
||||
return result.Succeeded
|
||||
? result.Principal
|
||||
: null;
|
||||
}
|
||||
|
||||
private static Guid? MemberKey(ClaimsPrincipal? claimsPrincipal)
|
||||
=> claimsPrincipal is not null && Guid.TryParse(claimsPrincipal.GetClaim(Constants.OAuthClaims.MemberKey), out Guid memberKey)
|
||||
? memberKey
|
||||
: null;
|
||||
|
||||
private static string[]? MemberRoles(ClaimsPrincipal? claimsPrincipal)
|
||||
=> claimsPrincipal?.GetClaim(Constants.OAuthClaims.MemberRoles)?.Split(Constants.CharArrays.Comma);
|
||||
}
|
||||
@@ -49,6 +49,7 @@ public sealed class ImageSharpImageUrlGenerator : IImageUrlGenerator
|
||||
/// Initializes a new instance of the <see cref="ImageSharpImageUrlGenerator" /> class.
|
||||
/// </summary>
|
||||
/// <param name="supportedImageFileTypes">The supported image file types/extensions.</param>
|
||||
/// <param name="options">The ImageSharp middleware options.</param>
|
||||
/// <param name="requestAuthorizationUtilities">Contains helpers that allow authorization of image requests.</param>
|
||||
/// <remarks>
|
||||
/// This constructor is only used for testing.
|
||||
|
||||
@@ -22,7 +22,7 @@ internal class SqlServerEFCoreDistributedLockingMechanism<T> : IDistributedLocki
|
||||
private readonly Lazy<IEFCoreScopeAccessor<T>> _scopeAccessor; // Hooray it's a circular dependency.
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SqlServerDistributedLockingMechanism" /> class.
|
||||
/// Initializes a new instance of the <see cref="SqlServerEFCoreDistributedLockingMechanism{T}"/> class.
|
||||
/// </summary>
|
||||
public SqlServerEFCoreDistributedLockingMechanism(
|
||||
ILogger<SqlServerEFCoreDistributedLockingMechanism<T>> logger,
|
||||
|
||||
@@ -35,7 +35,7 @@ public abstract class CacheRefresherBase<TNotification> : ICacheRefresher
|
||||
public abstract string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ICacheRefresherNotificationFactory" /> for <see cref="TNotification" />
|
||||
/// Gets the <see cref="ICacheRefresherNotificationFactory" /> for <typeparamref name="TNotification"/>.
|
||||
/// </summary>
|
||||
protected ICacheRefresherNotificationFactory NotificationFactory { get; }
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ public abstract class JsonCacheRefresherBase<TNotification, TJsonPayload> : Cach
|
||||
where TNotification : CacheRefresherNotification
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsonCacheRefresherBase{TInstanceType}" />.
|
||||
/// Initializes a new instance of the <see cref="JsonCacheRefresherBase{TNotification, TJsonPayload}"/> class.
|
||||
/// </summary>
|
||||
protected JsonCacheRefresherBase(
|
||||
AppCaches appCaches,
|
||||
|
||||
@@ -37,7 +37,7 @@ public class EventClearingObservableCollection<TValue> : ObservableCollection<TV
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all event handlers for the <see cref="CollectionChanged" /> event
|
||||
/// Clears all event handlers for the <see cref="INotifyCollectionChanged.CollectionChanged" /> event.
|
||||
/// </summary>
|
||||
public void ClearCollectionChangedEvents() => _changed = null;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
|
||||
namespace Umbraco.Cms.Core.Collections;
|
||||
@@ -84,7 +84,7 @@ public class ObservableDictionary<TKey, TValue> : ObservableCollection<TValue>,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all <see cref="CollectionChanged" /> event handlers
|
||||
/// Clears all <see cref="INotifyCollectionChanged.CollectionChanged" /> event handlers
|
||||
/// </summary>
|
||||
public void ClearCollectionChangedEvents() => _changed = null;
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Reflection;
|
||||
using Umbraco.Cms.Core.Semver;
|
||||
|
||||
namespace Umbraco.Cms.Core.Configuration;
|
||||
|
||||
@@ -54,6 +54,19 @@ public class DeliveryApiSettings
|
||||
/// </summary>
|
||||
public MediaSettings Media { get; set; } = new ();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the member authorization settings for the Delivery API.
|
||||
/// </summary>
|
||||
public MemberAuthorizationSettings? MemberAuthorization { get; set; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating if any member authorization type is enabled for the Delivery API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This method is intended for future extension - see remark in <see cref="MemberAuthorizationSettings"/>.
|
||||
/// </remarks>
|
||||
public bool MemberAuthorizationIsEnabled() => MemberAuthorization?.AuthorizationCodeFlow?.Enabled is true;
|
||||
|
||||
/// <summary>
|
||||
/// Typed configuration options for the Media APIs of the Delivery API.
|
||||
/// </summary>
|
||||
@@ -84,4 +97,45 @@ public class DeliveryApiSettings
|
||||
[DefaultValue(StaticPublicAccess)]
|
||||
public bool PublicAccess { get; set; } = StaticPublicAccess;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Typed configuration options for member authorization settings for the Delivery API.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This class is intended for future extension, if/when adding support for additional
|
||||
/// authorization flows (i.e. non-interactive authorization flows).
|
||||
/// </remarks>
|
||||
public class MemberAuthorizationSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the Authorization Code Flow configuration for the Delivery API.
|
||||
/// </summary>
|
||||
public AuthorizationCodeFlowSettings? AuthorizationCodeFlow { get; set; } = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Typed configuration options for the Authorization Code Flow settings for the Delivery API.
|
||||
/// </summary>
|
||||
public class AuthorizationCodeFlowSettings
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether Authorization Code Flow should be enabled for the Delivery API.
|
||||
/// </summary>
|
||||
/// <value><c>true</c> if Authorization Code Flow should be enabled; otherwise, <c>false</c>.</value>
|
||||
[DefaultValue(StaticEnabled)]
|
||||
public bool Enabled { get; set; } = StaticEnabled;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URLs allowed to use as redirect targets after a successful login (session authorization).
|
||||
/// </summary>
|
||||
/// <value>The URLs allowed as redirect targets.</value>
|
||||
public Uri[] LoginRedirectUrls { get; set; } = Array.Empty<Uri>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URLs allowed to use as redirect targets after a successful logout (session termination).
|
||||
/// </summary>
|
||||
/// <value>The URLs allowed as redirect targets.</value>
|
||||
/// <remarks>These are only required if logout is to be used.</remarks>
|
||||
public Uri[] LogoutRedirectUrls { get; set; } = Array.Empty<Uri>();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ public static partial class Constants
|
||||
public static readonly char[] Comma = { ',' };
|
||||
|
||||
/// <summary>
|
||||
/// Char array containing only &
|
||||
/// Char array containing only &
|
||||
/// </summary>
|
||||
public static readonly char[] Ampersand = { '&' };
|
||||
|
||||
@@ -88,7 +88,7 @@ public static partial class Constants
|
||||
public static readonly char[] QuestionMark = { '?' };
|
||||
|
||||
/// <summary>
|
||||
/// Char array containing ? &
|
||||
/// Char array containing ? &
|
||||
/// </summary>
|
||||
public static readonly char[] QuestionMarkAmpersand = { '?', '&' };
|
||||
|
||||
|
||||
17
src/Umbraco.Core/Constants-OAuthClaims.cs
Normal file
17
src/Umbraco.Core/Constants-OAuthClaims.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
namespace Umbraco.Cms.Core;
|
||||
|
||||
public static partial class Constants
|
||||
{
|
||||
public static class OAuthClaims
|
||||
{
|
||||
/// <summary>
|
||||
/// Key for authenticated member.
|
||||
/// </summary>
|
||||
public const string MemberKey = "umbraco-member-key";
|
||||
|
||||
/// <summary>
|
||||
/// Roles for authenticated member.
|
||||
/// </summary>
|
||||
public const string MemberRoles = "umbraco-member-roles";
|
||||
}
|
||||
}
|
||||
12
src/Umbraco.Core/Constants-OAuthClientIds.cs
Normal file
12
src/Umbraco.Core/Constants-OAuthClientIds.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
namespace Umbraco.Cms.Core;
|
||||
|
||||
public static partial class Constants
|
||||
{
|
||||
public static class OAuthClientIds
|
||||
{
|
||||
/// <summary>
|
||||
/// Client ID used for member access.
|
||||
/// </summary>
|
||||
public const string Member = "umbraco-member";
|
||||
}
|
||||
}
|
||||
@@ -33,8 +33,8 @@ public class DelegateEqualityComparer<T> : IEqualityComparer<T>
|
||||
/// <returns>
|
||||
/// true if the specified objects are equal; otherwise, false.
|
||||
/// </returns>
|
||||
/// <param name="x">The first object of type <paramref name="T" /> to compare.</param>
|
||||
/// <param name="y">The second object of type <paramref name="T" /> to compare.</param>
|
||||
/// <param name="x">The first object of type <typeparamref name="T"/> to compare.</param>
|
||||
/// <param name="y">The second object of type <typeparamref name="T"/> to compare.</param>
|
||||
public bool Equals(T? x, T? y) => _equals.Invoke(x, y);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
@@ -7,6 +8,16 @@ namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
/// </summary>
|
||||
public interface IApiContentQueryProvider
|
||||
{
|
||||
[Obsolete($"Use the {nameof(ExecuteQuery)} method that accepts {nameof(ProtectedAccess)}. Will be removed in V14.")]
|
||||
PagedModel<Guid> ExecuteQuery(
|
||||
SelectorOption selectorOption,
|
||||
IList<FilterOption> filterOptions,
|
||||
IList<SortOption> sortOptions,
|
||||
string culture,
|
||||
bool preview,
|
||||
int skip,
|
||||
int take);
|
||||
|
||||
/// <summary>
|
||||
/// Returns a page of item ids that passed the search criteria.
|
||||
/// </summary>
|
||||
@@ -15,10 +26,19 @@ public interface IApiContentQueryProvider
|
||||
/// <param name="sortOptions">The sorting options of the search criteria.</param>
|
||||
/// <param name="culture">The requested culture.</param>
|
||||
/// <param name="preview">Whether or not to search for preview content.</param>
|
||||
/// <param name="protectedAccess">Defines the limitations for querying protected content.</param>
|
||||
/// <param name="skip">Number of search results to skip (for pagination).</param>
|
||||
/// <param name="take">Number of search results to retrieve (for pagination).</param>
|
||||
/// <returns>A paged model containing the resulting IDs and the total number of results that matching the search criteria.</returns>
|
||||
PagedModel<Guid> ExecuteQuery(SelectorOption selectorOption, IList<FilterOption> filterOptions, IList<SortOption> sortOptions, string culture, bool preview, int skip, int take);
|
||||
PagedModel<Guid> ExecuteQuery(
|
||||
SelectorOption selectorOption,
|
||||
IList<FilterOption> filterOptions,
|
||||
IList<SortOption> sortOptions,
|
||||
string culture,
|
||||
ProtectedAccess protectedAccess,
|
||||
bool preview,
|
||||
int skip,
|
||||
int take) => new();
|
||||
|
||||
/// <summary>
|
||||
/// Returns a selector option that can be applied to fetch "all content" (i.e. if a selector option is not present when performing a search).
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
@@ -8,6 +9,9 @@ namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
/// </summary>
|
||||
public interface IApiContentQueryService
|
||||
{
|
||||
[Obsolete($"Use the {nameof(ExecuteQuery)} method that accepts {nameof(ProtectedAccess)}. Will be removed in V14.")]
|
||||
Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take);
|
||||
|
||||
/// <summary>
|
||||
/// Returns an attempt with a collection of item ids that passed the search criteria as a paged model.
|
||||
/// </summary>
|
||||
@@ -16,6 +20,8 @@ public interface IApiContentQueryService
|
||||
/// <param name="sorts">Optional sort query parameters values.</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <param name="protectedAccess">Defines the limitations for querying protected content.</param>
|
||||
/// <returns>A paged model of item ids that are returned after applying the search queries in an attempt.</returns>
|
||||
Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take);
|
||||
Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, ProtectedAccess protectedAccess, int skip, int take)
|
||||
=> default;
|
||||
}
|
||||
|
||||
12
src/Umbraco.Core/DeliveryApi/IRequestMemberAccessService.cs
Normal file
12
src/Umbraco.Core/DeliveryApi/IRequestMemberAccessService.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public interface IRequestMemberAccessService
|
||||
{
|
||||
Task<PublicAccessStatus> MemberHasAccessToAsync(IPublishedContent content);
|
||||
|
||||
Task<ProtectedAccess> MemberAccessAsync();
|
||||
}
|
||||
@@ -1,11 +1,16 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public sealed class NoopApiContentQueryService : IApiContentQueryService
|
||||
{
|
||||
/// <inheritdoc />
|
||||
[Obsolete($"Use the {nameof(ExecuteQuery)} method that accepts {nameof(ProtectedAccess)}. Will be removed in V14.")]
|
||||
public Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, int skip, int take)
|
||||
=> Attempt.SucceedWithStatus(ApiContentQueryOperationStatus.Success, new PagedModel<Guid>());
|
||||
|
||||
/// <inheritdoc />
|
||||
public Attempt<PagedModel<Guid>, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable<string> filters, IEnumerable<string> sorts, ProtectedAccess protectedAccess, int skip, int take)
|
||||
=> Attempt.SucceedWithStatus(ApiContentQueryOperationStatus.Success, new PagedModel<Guid>());
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Security;
|
||||
|
||||
namespace Umbraco.Cms.Core.DeliveryApi;
|
||||
|
||||
public sealed class NoopRequestMemberAccessService : IRequestMemberAccessService
|
||||
{
|
||||
public Task<PublicAccessStatus> MemberHasAccessToAsync(IPublishedContent content) => Task.FromResult(PublicAccessStatus.AccessAccepted);
|
||||
|
||||
public Task<ProtectedAccess> MemberAccessAsync() => Task.FromResult(ProtectedAccess.None);
|
||||
}
|
||||
@@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Provides extension methods to the <see cref="IFactory" /> class.
|
||||
/// Provides extension methods to the <see cref="IServiceProvider" /> class.
|
||||
/// </summary>
|
||||
public static class ServiceProviderExtensions
|
||||
{
|
||||
@@ -28,7 +28,7 @@ public static class ServiceProviderExtensions
|
||||
/// <summary>
|
||||
/// Creates an instance of a service, with arguments.
|
||||
/// </summary>
|
||||
/// <param name="serviceProvider">The <see cref="IServiceProvider" /></param>
|
||||
/// <param name="serviceProvider">The <see cref="IServiceProvider" />.</param>
|
||||
/// <param name="type">The type of the instance.</param>
|
||||
/// <param name="args">Named arguments.</param>
|
||||
/// <returns>An instance of the specified type.</returns>
|
||||
|
||||
@@ -158,6 +158,12 @@
|
||||
<key alias="morePublishingOptions">Više opcija za objavljivanje</key>
|
||||
<key alias="submitChanges">Pošalji</key>
|
||||
</area>
|
||||
<area alias="auditTrailsMedia">
|
||||
<key alias="delete">Mediji je izbrisan</key>
|
||||
<key alias="move">Mediji premješten</key>
|
||||
<key alias="copy">Mediji kopiran</key>
|
||||
<key alias="save">Mediji spremljen</key>
|
||||
</area>
|
||||
<area alias="auditTrails">
|
||||
<key alias="atViewingFor">Pregled za</key>
|
||||
<key alias="delete">Sadržaj je izbrisan</key>
|
||||
@@ -2653,7 +2659,7 @@ Da upravljate svojom web lokacijom, jednostavno otvorite Umbraco backoffice i po
|
||||
</key>
|
||||
<key alias="learningBaseDescription">
|
||||
<![CDATA[
|
||||
<p>Želite savladati Umbraco? Provedite nekoliko minuta učeći neke najbolje prakse gledajući jedan od ovih videozapisa o korištenju Umbraco-a <a href="https://www.youtube.com/c/UmbracoLearningBase" target="_blank" rel="noopener"> Umbraco Learning Base Youtube kanal</a>. Ovdje možete pronaći gomilu video materijala koji pokriva mnoge aspekte Umbraco-a.</p>
|
||||
<p>Želite savladati Umbraco? Provedite nekoliko minuta učeći neke najbolje prakse gledajući jedan od ovih videozapisa o korištenju Umbraco-a <a class="btn-link -underline" href="https://www.youtube.com/c/UmbracoLearningBase" target="_blank" rel="noopener"> Umbraco Learning Base Youtube kanal</a>. Ovdje možete pronaći gomilu video materijala koji pokriva mnoge aspekte Umbraco-a.</p>
|
||||
]]>
|
||||
</key>
|
||||
<key alias="getStarted">Za početak</key>
|
||||
|
||||
@@ -151,6 +151,12 @@
|
||||
<key alias="confirmActionConfirm">Potvrdit</key>
|
||||
<key alias="morePublishingOptions">Další možnosti publikování</key>
|
||||
</area>
|
||||
<area alias="auditTrailsMedia">
|
||||
<key alias="delete">Média smazán</key>
|
||||
<key alias="move">Média přesunut</key>
|
||||
<key alias="copy">Média zkopírován</key>
|
||||
<key alias="save">Média uložen</key>
|
||||
</area>
|
||||
<area alias="auditTrails">
|
||||
<key alias="atViewingFor">Zobrazení pro</key>
|
||||
<key alias="delete">Obsah smazán</key>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -157,6 +157,12 @@
|
||||
<key alias="morePublishingOptions">Flere publiseringsmuligheder</key>
|
||||
<key alias="submitChanges">Indsæt</key>
|
||||
</area>
|
||||
<area alias="auditTrailsMedia">
|
||||
<key alias="delete">Brugeren har slettet medie</key>
|
||||
<key alias="move">Brugeren har flyttet medie</key>
|
||||
<key alias="copy">Brugeren har kopieret medie</key>
|
||||
<key alias="save">Brugeren har gemt medie</key>
|
||||
</area>
|
||||
<area alias="auditTrails">
|
||||
<key alias="atViewingFor">For</key>
|
||||
<key alias="delete">Brugeren har slettet indholdet</key>
|
||||
|
||||
@@ -159,6 +159,12 @@
|
||||
<key alias="morePublishingOptions">Mehr Veröffentlichungs Optionen</key>
|
||||
<key alias="submitChanges">Senden</key>
|
||||
</area>
|
||||
<area alias="auditTrailsMedia">
|
||||
<key alias="delete">Medie gelöscht</key>
|
||||
<key alias="move">Medie verschoben</key>
|
||||
<key alias="copy">Medie kopiert</key>
|
||||
<key alias="save">Medie gesichert</key>
|
||||
</area>
|
||||
<area alias="auditTrails">
|
||||
<key alias="atViewingFor">Anzeigen als</key>
|
||||
<key alias="delete">Inhalt gelöscht</key>
|
||||
|
||||
@@ -159,6 +159,12 @@
|
||||
<key alias="morePublishingOptions">More publishing options</key>
|
||||
<key alias="submitChanges">Submit</key>
|
||||
</area>
|
||||
<area alias="auditTrailsMedia">
|
||||
<key alias="delete">Media deleted</key>
|
||||
<key alias="move">Media moved</key>
|
||||
<key alias="copy">Media copied</key>
|
||||
<key alias="save">Media saved</key>
|
||||
</area>
|
||||
<area alias="auditTrails">
|
||||
<key alias="atViewingFor">Viewing for</key>
|
||||
<key alias="delete">Content deleted</key>
|
||||
@@ -2520,6 +2526,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
<key alias="addImageCaption">Add image caption</key>
|
||||
<key alias="searchContentTree">Search content tree</key>
|
||||
<key alias="maxAmount">Maximum amount</key>
|
||||
<key alias="expandChildItems">Expand child items for</key>
|
||||
<key alias="openContextNode">Open context node for</key>
|
||||
</area>
|
||||
<area alias="references">
|
||||
<key alias="tabName">References</key>
|
||||
@@ -2672,7 +2680,7 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
</key>
|
||||
<key alias="learningBaseDescription">
|
||||
<![CDATA[
|
||||
<p>Want to master Umbraco? Spend a few minutes learning some best practices by visiting <a href="https://www.youtube.com/c/UmbracoLearningBase" target="_blank" rel="noopener">the Umbraco Learning Base Youtube channel</a>. Here you can find a bunch of video material covering many aspects of Umbraco.</p>
|
||||
<p>Want to master Umbraco? Spend a few minutes learning some best practices by visiting <a class="btn-link -underline" href="https://www.youtube.com/c/UmbracoLearningBase" target="_blank" rel="noopener">the Umbraco Learning Base Youtube channel</a>. Here you can find a bunch of video material covering many aspects of Umbraco.</p>
|
||||
]]>
|
||||
</key>
|
||||
<key alias="getStarted">To get you started</key>
|
||||
|
||||
@@ -160,6 +160,12 @@
|
||||
<key alias="morePublishingOptions">More publishing options</key>
|
||||
<key alias="submitChanges">Submit</key>
|
||||
</area>
|
||||
<area alias="auditTrailsMedia">
|
||||
<key alias="delete">Media deleted</key>
|
||||
<key alias="move">Media moved</key>
|
||||
<key alias="copy">Media copied</key>
|
||||
<key alias="save">Media saved</key>
|
||||
</area>
|
||||
<area alias="auditTrails">
|
||||
<key alias="atViewingFor">Viewing for</key>
|
||||
<key alias="delete">Content deleted</key>
|
||||
@@ -1195,6 +1201,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
</body>
|
||||
</html>
|
||||
]]> </key>
|
||||
<key alias="mfaSecurityCodeSubject">Umbraco: Security Code</key>
|
||||
<key alias="mfaSecurityCodeMessage">Your security code is: %0%</key>
|
||||
<key alias="2faTitle">One last step</key>
|
||||
<key alias="2faText">You have enabled 2-factor authentication and must verify your identity.</key>
|
||||
<key alias="2faMultipleText">Please choose a 2-factor provider</key>
|
||||
@@ -2624,6 +2632,8 @@ To manage your website, simply open the Umbraco backoffice and start adding cont
|
||||
<key alias="addImageCaption">Add image caption</key>
|
||||
<key alias="searchContentTree">Search content tree</key>
|
||||
<key alias="maxAmount">Maximum amount</key>
|
||||
<key alias="expandChildItems">Expand child items for</key>
|
||||
<key alias="openContextNode">Open context node for</key>
|
||||
</area>
|
||||
<area alias="references">
|
||||
<key alias="tabName">References</key>
|
||||
|
||||
@@ -150,6 +150,12 @@
|
||||
<key alias="confirmActionConfirm">Confirmer</key>
|
||||
<key alias="morePublishingOptions">Options de publication supplémentaires</key>
|
||||
</area>
|
||||
<area alias="auditTrailsMedia">
|
||||
<key alias="delete">Media supprimé</key>
|
||||
<key alias="move">Media déplacé</key>
|
||||
<key alias="copy">Media copié</key>
|
||||
<key alias="save">Media sauvegardé</key>
|
||||
</area>
|
||||
<area alias="auditTrails">
|
||||
<key alias="atViewingFor">Aperçu pour</key>
|
||||
<key alias="delete">Contenu supprimé</key>
|
||||
|
||||
@@ -158,6 +158,12 @@
|
||||
<key alias="morePublishingOptions">Više opcija za objavljivanje</key>
|
||||
<key alias="submitChanges">Pošalji</key>
|
||||
</area>
|
||||
<area alias="auditTrailsMedia">
|
||||
<key alias="delete">Mediji je obrisan</key>
|
||||
<key alias="move">Mediji premješten</key>
|
||||
<key alias="copy">Mediji kopiran</key>
|
||||
<key alias="save">Mediji spremljen</key>
|
||||
</area>
|
||||
<area alias="auditTrails">
|
||||
<key alias="atViewingFor">Pregled za</key>
|
||||
<key alias="delete">Sadržaj je obrisan</key>
|
||||
@@ -2610,7 +2616,7 @@ Da bi upravljali svojom web lokacijom, jednostavno otvorite Umbraco backoffice i
|
||||
</key>
|
||||
<key alias="learningBaseDescription">
|
||||
<![CDATA[
|
||||
<p>Želite savladati Umbraco? Provedite nekoliko minuta učeći najbolje prakse gledajući jedan od ovih videozapisa o korištenju Umbraco-a <a href="https://www.youtube.com/c/UmbracoLearningBase" target="_blank" rel="noopener"> Umbraco Learning Base Youtube kanal</a>. Ovdje možete pronaći gomilu video materijala koji pokriva mnoge aspekte Umbraco-a.</p>
|
||||
<p>Želite savladati Umbraco? Provedite nekoliko minuta učeći najbolje prakse gledajući jedan od ovih videozapisa o korištenju Umbraco-a <a class="btn-link -underline" href="https://www.youtube.com/c/UmbracoLearningBase" target="_blank" rel="noopener"> Umbraco Learning Base Youtube kanal</a>. Ovdje možete pronaći gomilu video materijala koji pokriva mnoge aspekte Umbraco-a.</p>
|
||||
]]>
|
||||
</key>
|
||||
<key alias="getStarted">Za početak</key>
|
||||
|
||||
@@ -163,6 +163,12 @@
|
||||
<key alias="submitChanges">Invia</key>
|
||||
<key alias="submitChangesAndClose">Invia e chiudi</key>
|
||||
</area>
|
||||
<area alias="auditTrailsMedia">
|
||||
<key alias="delete">Media eliminato</key>
|
||||
<key alias="move">Media spostato</key>
|
||||
<key alias="copy">Media copiato</key>
|
||||
<key alias="save">Media salvato</key>
|
||||
</area>
|
||||
<area alias="auditTrails">
|
||||
<key alias="atViewingFor">Visualizzazione per</key>
|
||||
<key alias="delete">Contenuto eliminato</key>
|
||||
|
||||
@@ -112,6 +112,12 @@
|
||||
zijn op de huidige node, tenzij een domein hieronder ook van toepassing is.]]></key>
|
||||
<key alias="setDomains">Domeinen</key>
|
||||
</area>
|
||||
<area alias="auditTrailsMedia">
|
||||
<key alias="delete">Media verwijderd</key>
|
||||
<key alias="move">Media verplaatst</key>
|
||||
<key alias="copy">Media gekopieerd</key>
|
||||
<key alias="save">Media bewaard</key>
|
||||
</area>
|
||||
<area alias="auditTrails">
|
||||
<key alias="atViewingFor">Tonen voor</key>
|
||||
<key alias="delete">Inhoud verwijderd</key>
|
||||
@@ -537,6 +543,7 @@
|
||||
</area>
|
||||
<area alias="dictionary">
|
||||
<key alias="noItems">Er zijn geen woordenboekitems.</key>
|
||||
<key alias="noItemsFound">Er zijn geen woordenboekitems gevonden.</key>
|
||||
<key alias="createNew">Woordenboekitem aanmaken</key>
|
||||
</area>
|
||||
<area alias="dictionaryItem">
|
||||
@@ -738,7 +745,7 @@
|
||||
<key alias="history">Geschiedenis</key>
|
||||
<key alias="icon">Icoon</key>
|
||||
<key alias="id">Id</key>
|
||||
<key alias="import">Import</key>
|
||||
<key alias="import">Importeren</key>
|
||||
<key alias="excludeFromSubFolders">Alleen in deze map zoeken</key>
|
||||
<key alias="info">Info</key>
|
||||
<key alias="innerMargin">Binnenste marge</key>
|
||||
|
||||
@@ -67,6 +67,12 @@
|
||||
<key alias="setLanguageHelp"><![CDATA[Sätt kulturen för noder under aktuell nod,<br /> eller ärv kulturen från föregående noder. Appliceras även<br />
|
||||
på befintlig nod.]]></key>
|
||||
</area>
|
||||
<area alias="auditTrailsMedia">
|
||||
<key alias="delete">Media raderat</key>
|
||||
<key alias="move">Media flyttat</key>
|
||||
<key alias="copy">Media kopierat</key>
|
||||
<key alias="save">Media sparat</key>
|
||||
</area>
|
||||
<area alias="auditTrails">
|
||||
<key alias="atViewingFor">Visar för</key>
|
||||
<key alias="delete">Innehållet raderat</key>
|
||||
@@ -103,6 +109,12 @@
|
||||
<key alias="blockHasChanges">Du har gjort ändringar i detta innehåll. Är du säker på att du vill ta bort dem?</key>
|
||||
<key alias="confirmCancelBlockCreationHeadline">Ignorera skapandet</key>
|
||||
<key alias="confirmCancelBlockCreationMessage"><![CDATA[Är du säker på att du vill avbryta skapandet?]]></key>
|
||||
<key alias="areaValidationEntriesShort"><![CDATA[<strong>%0%</strong> måste vara närvarande åtminstone <strong>%2%</strong> time(s).]]></key>
|
||||
<key alias="areaValidationEntriesExceed"><![CDATA[<strong>%0%</strong> måste maximalt finnas <strong>%3%</strong> time(s).]]></key>
|
||||
</area>
|
||||
<area alias="validation">
|
||||
<key alias="entriesShort"><![CDATA[Minsta %0% poster, kräver <strong>%1%</strong> mer.]]></key>
|
||||
<key alias="entriesExceed"><![CDATA[Max %0% poster, <strong>%1%</strong> för många.]]></key>
|
||||
</area>
|
||||
<area alias="blueprints">
|
||||
<key alias="createBlueprintFrom"><![CDATA[Skapa en ny innehållsmall för <em>%0%</em>]]></key>
|
||||
@@ -240,6 +252,60 @@
|
||||
<key alias="memberIntro">Komma igång</key>
|
||||
<key alias="formsInstall">Installera Umbraco Forms</key>
|
||||
</area>
|
||||
<area alias="visuallyHiddenTexts">
|
||||
<key alias="goBack">Backa</key>
|
||||
<key alias="activeListLayout">Aktiv layout:</key>
|
||||
<key alias="jumpTo">Hoppa till</key>
|
||||
<key alias="group">grupp</key>
|
||||
<key alias="passed">godkänd</key>
|
||||
<key alias="warning">varning</key>
|
||||
<key alias="failed">underkänd</key>
|
||||
<key alias="suggestion">förslag</key>
|
||||
<key alias="checkPassed">Godkänd</key>
|
||||
<key alias="checkFailed">Underkänd</key>
|
||||
<key alias="openBackofficeSearch">Öppna sökfunktion (backoffice)</key>
|
||||
<key alias="openCloseBackofficeHelp">Öppna/stäng hjälpfunktion</key>
|
||||
<key alias="openCloseBackofficeProfileOptions">Öppna/stäng personliga inställningar</key>
|
||||
<key alias="assignDomainDescription">Redigera språk och värdnamn för %0%</key>
|
||||
<key alias="createDescription">Skapa en ny nod under %0%</key>
|
||||
<key alias="protectDescription">Ändra behörigheter för %0%</key>
|
||||
<key alias="rightsDescription">Redigera behörigheter för %0%</key>
|
||||
<key alias="sortDescription">Ändra sortering av %0%</key>
|
||||
<key alias="createblueprintDescription">Skapa innehållsmall baserad på %0%</key>
|
||||
<key alias="openContextMenu">Öppna kontextmeny för </key>
|
||||
<key alias="currentLanguage">Aktuellt språk</key>
|
||||
<key alias="switchLanguage">Byt språk till</key>
|
||||
<key alias="createNewFolder">Skapa ny mapp</key>
|
||||
<key alias="newPartialView">Del av vy</key>
|
||||
<key alias="newPartialViewMacro">Del av vy (makro)</key>
|
||||
<key alias="newMember">Medlem</key>
|
||||
<key alias="newDataType">Datatyp</key>
|
||||
<key alias="redirectDashboardSearchLabel">Sök bland omdirigeringar</key>
|
||||
<key alias="userGroupSearchLabel">Sök bland användargrupper</key>
|
||||
<key alias="userSearchLabel">Sök bland användare</key>
|
||||
<key alias="createItem">Skapa post</key>
|
||||
<key alias="create">Skapa</key>
|
||||
<key alias="edit">Redigera</key>
|
||||
<key alias="name">Namn</key>
|
||||
<key alias="addNewRow">Lägg till ny rad</key>
|
||||
<key alias="tabExpand">Visa fler alternativ</key>
|
||||
<key alias="searchOverlayTitle">Sök i Umbraco backoffice</key>
|
||||
<key alias="searchOverlayDescription">Sök efter innehåll, media etc i hela Umbraco.</key>
|
||||
<key alias="searchInputDescription">När det finns automatförslag, använd pil upp eller ner, eller använd tabbtangenten. Använd enter för att välja.
|
||||
</key>
|
||||
<key alias="path">Sökväg:</key>
|
||||
<key alias="foundIn">Hittad i</key>
|
||||
<key alias="hasTranslation">Har översättning</key>
|
||||
<key alias="noTranslation">Saknar översättning</key>
|
||||
<key alias="dictionaryListCaption">Post i ordlista</key>
|
||||
<key alias="contextMenuDescription">Välj ett av alternativen för att redigera noden.</key>
|
||||
<key alias="contextDialogDescription">Utför %0% på noden %1%</key>
|
||||
<key alias="addImageCaption">Lägg till bildtext</key>
|
||||
<key alias="searchContentTree">Sök i innehållsträdet</key>
|
||||
<key alias="maxAmount">Maximalt värde</key>
|
||||
<key alias="expandChildItems">Visa underliggande noder för</key>
|
||||
<key alias="openContextNode">Öppna kontext för</key>
|
||||
</area>
|
||||
<area alias="prompt">
|
||||
<key alias="stay">Stanna</key>
|
||||
<key alias="discardChanges">Ignorera ändringar</key>
|
||||
|
||||
@@ -156,6 +156,12 @@
|
||||
<key alias="morePublishingOptions">Daha fazla yayınlama seçeneği</key>
|
||||
<key alias="submitChanges">Gönder</key>
|
||||
</area>
|
||||
<area alias="auditTrailsMedia">
|
||||
<key alias="delete">Medya silindi</key>
|
||||
<key alias="move">Medya taşındı</key>
|
||||
<key alias="copy">Medya kopyalandı</key>
|
||||
<key alias="save">Medya kaydedildi</key>
|
||||
</area>
|
||||
<area alias="auditTrails">
|
||||
<key alias="atViewingFor">Görüntüleniyor</key>
|
||||
<key alias="delete">İçerik silindi</key>
|
||||
|
||||
@@ -167,7 +167,7 @@ public static class ExpressionHelper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="MethodInfo" /> from an <see cref="Expression{Action{T}}" /> provided it refers to a method call.
|
||||
/// Gets a <see cref="MethodInfo" /> from an <see cref="Expression{TDelegate}"/> of <see cref="Action{T}"/> provided it refers to a method call.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <param name="fromExpression">From expression.</param>
|
||||
@@ -254,7 +254,7 @@ public static class ExpressionHelper
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a <see cref="MemberInfo" /> from an <see cref="Expression{Func{T, TReturn}}" /> provided it refers to member
|
||||
/// Gets a <see cref="MemberInfo" /> from an <see cref="Expression{TDelegate}" /> of <see cref="Func{T, TReturn}"/> provided it refers to member
|
||||
/// access.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
|
||||
@@ -320,6 +320,8 @@ public static class ContentExtensions
|
||||
/// Stores a file.
|
||||
/// </summary>
|
||||
/// <param name="content"><see cref="IContentBase" />A content item.</param>
|
||||
/// <param name="mediaFileManager">The media file manager.</param>
|
||||
/// <param name="contentTypeBaseServiceProvider">The content type base service provider.</param>
|
||||
/// <param name="propertyTypeAlias">The property alias.</param>
|
||||
/// <param name="filename">The name of the file.</param>
|
||||
/// <param name="filestream">A stream containing the file data.</param>
|
||||
|
||||
@@ -11,7 +11,7 @@ using Umbraco.Cms.Core;
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for Dictionary & ConcurrentDictionary
|
||||
/// Extension methods for Dictionary & ConcurrentDictionary.
|
||||
/// </summary>
|
||||
public static class DictionaryExtensions
|
||||
{
|
||||
@@ -254,7 +254,7 @@ public static class DictionaryExtensions
|
||||
|
||||
/// <summary>
|
||||
/// Converts a dictionary object to a query string representation such as:
|
||||
/// firstname=shannon&lastname=deminick
|
||||
/// firstname=shannon&lastname=deminick.
|
||||
/// </summary>
|
||||
/// <param name="d"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
@@ -353,7 +353,8 @@ public static class EnumerableExtensions
|
||||
/// <summary>
|
||||
/// Transforms an enumerable.
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
/// <typeparam name="TSource"></typeparam>
|
||||
/// <typeparam name="TTarget"></typeparam>
|
||||
/// <param name="source"></param>
|
||||
/// <param name="transform"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
@@ -1253,7 +1253,7 @@ public static class PublishedContentExtensions
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This is the same as calling
|
||||
/// <see cref="Umbraco.Web.PublishedContentExtensions.AncestorOrSelf(IPublishedContent, int)" /> with <c>maxLevel</c>
|
||||
/// <see cref="AncestorOrSelf(IPublishedContent, int)" /> with <c>maxLevel</c>
|
||||
/// set to 1.
|
||||
/// </remarks>
|
||||
public static IPublishedContent Root(this IPublishedContent content) => content.AncestorOrSelf(1);
|
||||
@@ -1270,7 +1270,7 @@ public static class PublishedContentExtensions
|
||||
/// </returns>
|
||||
/// <remarks>
|
||||
/// This is the same as calling
|
||||
/// <see cref="Umbraco.Web.PublishedContentExtensions.AncestorOrSelf{T}(IPublishedContent, int)" /> with
|
||||
/// <see cref="AncestorOrSelf{T}(IPublishedContent, int)" /> with
|
||||
/// <c>maxLevel</c> set to 1.
|
||||
/// </remarks>
|
||||
public static T? Root<T>(this IPublishedContent content)
|
||||
|
||||
@@ -134,27 +134,6 @@ public static class PublishedElementExtensions
|
||||
|
||||
#endregion
|
||||
|
||||
#region CheckVariation
|
||||
/// <summary>
|
||||
/// Method to check if VariationContext culture differs from culture parameter, if so it will update the VariationContext for the PublishedValueFallback.
|
||||
/// </summary>
|
||||
/// <param name="publishedValueFallback">The requested PublishedValueFallback.</param>
|
||||
/// <param name="culture">The requested culture.</param>
|
||||
/// <param name="segment">The requested segment.</param>
|
||||
/// <returns></returns>
|
||||
private static void EventuallyUpdateVariationContext(IPublishedValueFallback publishedValueFallback, string? culture, string? segment)
|
||||
{
|
||||
IVariationContextAccessor? variationContextAccessor = publishedValueFallback.VariationContextAccessor;
|
||||
|
||||
//If there is a difference in requested culture and the culture that is set in the VariationContext, it will pick wrong localized content.
|
||||
//This happens for example using links to localized content in a RichText Editor.
|
||||
if (!string.IsNullOrEmpty(culture) && variationContextAccessor?.VariationContext?.Culture != culture)
|
||||
{
|
||||
variationContextAccessor!.VariationContext = new VariationContext(culture, segment);
|
||||
}
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Value<T>
|
||||
|
||||
/// <summary>
|
||||
@@ -195,8 +174,6 @@ public static class PublishedElementExtensions
|
||||
{
|
||||
IPublishedProperty? property = content.GetProperty(alias);
|
||||
|
||||
EventuallyUpdateVariationContext(publishedValueFallback, culture, segment);
|
||||
|
||||
// if we have a property, and it has a value, return that value
|
||||
if (property != null && property.HasValue(culture, segment))
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ public static class RequestHandlerSettingsExtension
|
||||
return RequestHandlerSettings.DefaultCharCollection;
|
||||
}
|
||||
|
||||
/// Merges CharCollection and UserDefinedCharCollection, prioritizing UserDefinedCharCollection.
|
||||
// Merges CharCollection and UserDefinedCharCollection, prioritizing UserDefinedCharCollection.
|
||||
return MergeUnique(requestHandlerSettings.UserDefinedCharCollection, RequestHandlerSettings.DefaultCharCollection);
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ public static class StringExtensions
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// This methods ensures that the resulting URL is structured correctly, that there's only one '?' and that things are
|
||||
/// delimited properly with '&'
|
||||
/// delimited properly with '&'
|
||||
/// </remarks>
|
||||
public static string AppendQueryStringToUrl(this string url, params string[] queryStrings)
|
||||
{
|
||||
|
||||
@@ -28,7 +28,7 @@ public static class XmlExtensions
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If
|
||||
/// <param name="variables" />
|
||||
/// <paramref name="variables" />
|
||||
/// is <c>null</c>, or is empty, or contains only one single
|
||||
/// value which itself is <c>null</c>, then variables are ignored.
|
||||
/// </para>
|
||||
@@ -51,7 +51,7 @@ public static class XmlExtensions
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If
|
||||
/// <param name="variables" />
|
||||
/// <paramref name="variables" />
|
||||
/// is <c>null</c>, or is empty, or contains only one single
|
||||
/// value which itself is <c>null</c>, then variables are ignored.
|
||||
/// </para>
|
||||
@@ -74,7 +74,7 @@ public static class XmlExtensions
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If
|
||||
/// <param name="variables" />
|
||||
/// <paramref name="variables" />
|
||||
/// is <c>null</c>, or is empty, or contains only one single
|
||||
/// value which itself is <c>null</c>, then variables are ignored.
|
||||
/// </para>
|
||||
@@ -102,7 +102,7 @@ public static class XmlExtensions
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If
|
||||
/// <param name="variables" />
|
||||
/// <paramref name="variables" />
|
||||
/// is <c>null</c>, or is empty, or contains only one single
|
||||
/// value which itself is <c>null</c>, then variables are ignored.
|
||||
/// </para>
|
||||
@@ -130,7 +130,7 @@ public static class XmlExtensions
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If
|
||||
/// <param name="variables" />
|
||||
/// <paramref name="variables" />
|
||||
/// is <c>null</c>, or is empty, or contains only one single
|
||||
/// value which itself is <c>null</c>, then variables are ignored.
|
||||
/// </para>
|
||||
@@ -153,7 +153,7 @@ public static class XmlExtensions
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If
|
||||
/// <param name="variables" />
|
||||
/// <paramref name="variables" />
|
||||
/// is <c>null</c>, or is empty, or contains only one single
|
||||
/// value which itself is <c>null</c>, then variables are ignored.
|
||||
/// </para>
|
||||
@@ -176,7 +176,7 @@ public static class XmlExtensions
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If
|
||||
/// <param name="variables" />
|
||||
/// <paramref name="variables" />
|
||||
/// is <c>null</c>, or is empty, or contains only one single
|
||||
/// value which itself is <c>null</c>, then variables are ignored.
|
||||
/// </para>
|
||||
@@ -203,7 +203,7 @@ public static class XmlExtensions
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// If
|
||||
/// <param name="variables" />
|
||||
/// <paramref name="variables" />
|
||||
/// is <c>null</c>, or is empty, or contains only one single
|
||||
/// value which itself is <c>null</c>, then variables are ignored.
|
||||
/// </para>
|
||||
|
||||
@@ -121,7 +121,6 @@ public sealed class MediaFileManager
|
||||
/// <param name="content"></param>
|
||||
/// <param name="mediaFilePath">The file path if a file was found</param>
|
||||
/// <param name="propertyTypeAlias"></param>
|
||||
/// <param name="variationContextAccessor"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
|
||||
@@ -22,14 +22,14 @@ public class UniqueMediaPathScheme : IMediaPathScheme
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// Returning null so that <see cref="MediaFileSystem.DeleteMediaFiles" /> does *not*
|
||||
/// Returning null so that <see cref="MediaFileManager.DeleteMediaFiles(IEnumerable{string})" /> does *not*
|
||||
/// delete any directory. This is because the above shortening of the Guid to 8 chars
|
||||
/// means we're increasing the risk of collision, and we don't want to delete files
|
||||
/// belonging to other media items.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// And, at the moment, we cannot delete directory "only if it is empty" because of
|
||||
/// race conditions. We'd need to implement locks in <see cref="MediaFileSystem" /> for
|
||||
/// race conditions. We'd need to implement locks in <see cref="MediaFileManager" /> for
|
||||
/// this.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
|
||||
@@ -113,7 +113,7 @@ public class DisposableTimer : DisposableObjectSlim
|
||||
/// <summary>
|
||||
/// Disposes resources.
|
||||
/// </summary>
|
||||
/// <remarks>Overrides abstract class <see cref="DisposableObject" /> which handles required locking.</remarks>
|
||||
/// <remarks>Overrides abstract class <see cref="DisposableObjectSlim" /> which handles required locking.</remarks>
|
||||
protected override void DisposeResources()
|
||||
{
|
||||
Stopwatch.Stop();
|
||||
|
||||
@@ -141,7 +141,7 @@ public sealed class ProfilingLogger : IProfilingLogger
|
||||
public void LogTrace(string messageTemplate, params object[] propertyValues)
|
||||
=> Logger.LogTrace(messageTemplate, propertyValues);
|
||||
|
||||
///<inheritdoc>/>
|
||||
///<inheritdoc/>
|
||||
public bool IsEnabled(LogLevel logLevel)
|
||||
{
|
||||
switch (logLevel)
|
||||
|
||||
@@ -15,7 +15,7 @@ public interface IEmbedProvider
|
||||
/// <summary>
|
||||
/// A collection of querystring request parameters to append to the API URL
|
||||
/// </summary>
|
||||
/// <example>?key=value&key2=value2</example>
|
||||
/// <example>?key=value&key2=value2</example>
|
||||
Dictionary<string, string> RequestParams { get; }
|
||||
|
||||
string? GetMarkup(string url, int maxWidth = 0, int maxHeight = 0);
|
||||
|
||||
@@ -20,9 +20,7 @@ namespace Umbraco.Cms.Core.Models.Blocks
|
||||
/// <param name="content">The content.</param>
|
||||
/// <param name="settingsUdi">The settings UDI.</param>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <param name="rowSpan">The number of rows to span</param>
|
||||
/// <param name="columnSpan">The number of columns to span</param>
|
||||
/// <exception cref="System.ArgumentNullException">contentUdi
|
||||
/// <exception cref="ArgumentNullException">contentUdi
|
||||
/// or
|
||||
/// content</exception>
|
||||
public BlockGridItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings)
|
||||
@@ -114,8 +112,6 @@ namespace Umbraco.Cms.Core.Models.Blocks
|
||||
/// <param name="content">The content.</param>
|
||||
/// <param name="settingsUdi">The settings UDI.</param>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <param name="rowSpan">The number of rows to span</param>
|
||||
/// <param name="columnSpan">The number of columns to span</param>
|
||||
public BlockGridItem(Udi contentUdi, T content, Udi settingsUdi, IPublishedElement settings)
|
||||
: base(contentUdi, content, settingsUdi, settings)
|
||||
{
|
||||
@@ -147,8 +143,6 @@ namespace Umbraco.Cms.Core.Models.Blocks
|
||||
/// <param name="content">The content.</param>
|
||||
/// <param name="settingsUdi">The settings udi.</param>
|
||||
/// <param name="settings">The settings.</param>
|
||||
/// <param name="rowSpan">The number of rows to span</param>
|
||||
/// <param name="columnSpan">The number of columns to span</param>
|
||||
public BlockGridItem(Udi contentUdi, TContent content, Udi settingsUdi, TSettings settings)
|
||||
: base(contentUdi, content, settingsUdi, settings)
|
||||
{
|
||||
|
||||
@@ -44,7 +44,8 @@ public interface IBlockReference<TSettings> : IBlockReference
|
||||
/// <summary>
|
||||
/// Represents a data item reference with content and settings for a Block editor implementation.
|
||||
/// </summary>
|
||||
/// <typeparam name="TSettings">The type of the content.</typeparam>
|
||||
/// <typeparam name="TContent">The type of the content.</typeparam>
|
||||
/// <typeparam name="TSettings">The type of the settings.</typeparam>
|
||||
public interface IBlockReference<TContent, TSettings> : IBlockReference<TSettings>
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -59,6 +59,7 @@ public static class ContentRepositoryExtensions
|
||||
/// </summary>
|
||||
/// <param name="content"></param>
|
||||
/// <param name="date"></param>
|
||||
/// <param name="publishing"></param>
|
||||
/// <remarks>
|
||||
/// This is so that in an operation where (for example) 2 languages are updates like french and english, it is possible
|
||||
/// that
|
||||
|
||||
@@ -182,7 +182,6 @@ public sealed class CultureImpact
|
||||
/// </summary>
|
||||
/// <param name="culture">The culture code.</param>
|
||||
/// <param name="isDefault">A value indicating whether the culture is the default culture.</param>
|
||||
/// <param name="allowEditInvariantFromNonDefault">A value indicating if publishing invariant properties from non-default language.</param>
|
||||
[Obsolete("Use ICultureImpactService instead.")]
|
||||
public static CultureImpact Explicit(string? culture, bool isDefault)
|
||||
{
|
||||
@@ -211,7 +210,6 @@ public sealed class CultureImpact
|
||||
/// <param name="culture">The culture code.</param>
|
||||
/// <param name="isDefault">A value indicating whether the culture is the default culture.</param>
|
||||
/// <param name="content">The content item.</param>
|
||||
/// <param name="allowEditInvariantFromNonDefault">A value indicating if publishing invariant properties from non-default language.</param>
|
||||
/// <remarks>
|
||||
/// <para>Validates that the culture is compatible with the variation.</para>
|
||||
/// </remarks>
|
||||
|
||||
16
src/Umbraco.Core/Models/DeliveryApi/ProtectedAccess.cs
Normal file
16
src/Umbraco.Core/Models/DeliveryApi/ProtectedAccess.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
namespace Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
|
||||
public sealed class ProtectedAccess
|
||||
{
|
||||
public static ProtectedAccess None => new(null, null);
|
||||
|
||||
public ProtectedAccess(Guid? memberKey, string[]? memberRoles)
|
||||
{
|
||||
MemberKey = memberKey;
|
||||
MemberRoles = memberRoles;
|
||||
}
|
||||
|
||||
public Guid? MemberKey { get; }
|
||||
|
||||
public string[]? MemberRoles { get; }
|
||||
}
|
||||
@@ -10,6 +10,7 @@ public interface IMediaUrlGenerator
|
||||
/// </summary>
|
||||
/// <param name="propertyEditorAlias">The property editor alias</param>
|
||||
/// <param name="value">The value of the property</param>
|
||||
/// <param name="mediaPath">The media path</param>
|
||||
/// <returns>
|
||||
/// True if a media path was returned
|
||||
/// </returns>
|
||||
|
||||
@@ -58,7 +58,7 @@ public class Media : ContentBase, IMedia
|
||||
/// <summary>
|
||||
/// Changes the <see cref="IMediaType" /> for the current Media object
|
||||
/// </summary>
|
||||
/// <param name="contentType">New MediaType for this Media</param>
|
||||
/// <param name="mediaType">New MediaType for this Media</param>
|
||||
/// <remarks>Leaves PropertyTypes intact after change</remarks>
|
||||
internal void ChangeContentType(IMediaType mediaType) => ChangeContentType(mediaType, false);
|
||||
|
||||
@@ -66,7 +66,7 @@ public class Media : ContentBase, IMedia
|
||||
/// Changes the <see cref="IMediaType" /> for the current Media object and removes PropertyTypes,
|
||||
/// which are not part of the new MediaType.
|
||||
/// </summary>
|
||||
/// <param name="contentType">New MediaType for this Media</param>
|
||||
/// <param name="mediaType">New MediaType for this Media</param>
|
||||
/// <param name="clearProperties">Boolean indicating whether to clear PropertyTypes upon change</param>
|
||||
internal void ChangeContentType(IMediaType mediaType, bool clearProperties)
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using Umbraco.Cms.Core.Models.Entities;
|
||||
namespace Umbraco.Cms.Core.Models.Membership;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an <see cref="IContent" /> -> user group & permission key value pair collection
|
||||
/// Represents an <see cref="IContent" /> -> user group & permission key value pair collection
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This implements <see cref="IEntity" /> purely so it can be used with the repository layer which is why it's
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
namespace Umbraco.Cms.Core.Models.Membership;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an entity -> user group & permission key value pair collection
|
||||
/// Represents an entity -> user group & permission key value pair collection
|
||||
/// </summary>
|
||||
public class EntityPermissionSet
|
||||
{
|
||||
@@ -20,7 +20,7 @@ public class EntityPermissionSet
|
||||
public virtual int EntityId { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The key/value pairs of user group id & single permission
|
||||
/// The key/value pairs of user group id & single permission
|
||||
/// </summary>
|
||||
public EntityPermissionCollection PermissionsSet { get; }
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@ namespace Umbraco.Cms.Core.Models;
|
||||
/// <summary>
|
||||
/// Represents a paged result for a model collection
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
[DataContract(Name = "pagedCollection", Namespace = "")]
|
||||
public abstract class PagedResult
|
||||
{
|
||||
|
||||
@@ -3,7 +3,7 @@ using Umbraco.Cms.Core.Cache;
|
||||
namespace Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
/// <summary>
|
||||
/// Implements <see cref="IVariationContextAccessor" /> on top of <see cref="IHttpContextAccessor" />.
|
||||
/// Implements <see cref="IVariationContextAccessor" /> on top of <see cref="IRequestCache" />.
|
||||
/// </summary>
|
||||
public class HttpContextVariationContextAccessor : IVariationContextAccessor
|
||||
{
|
||||
|
||||
@@ -5,6 +5,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent;
|
||||
/// </summary>
|
||||
public interface IPublishedValueFallback
|
||||
{
|
||||
[Obsolete("Scheduled for removal in v14")]
|
||||
/// <summary>
|
||||
/// VariationContextAccessor that is not required to be implemented, therefore throws NotImplementedException as default.
|
||||
/// </summary>
|
||||
|
||||
@@ -20,6 +20,7 @@ public class PublishedValueFallback : IPublishedValueFallback
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
}
|
||||
|
||||
[Obsolete("Scheduled for removal in v14")]
|
||||
public IVariationContextAccessor VariationContextAccessor { get { return _variationContextAccessor; } }
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -6,7 +6,9 @@ namespace Umbraco.Cms.Core.Models;
|
||||
/// Represents a range with a minimum and maximum value.
|
||||
/// </summary>
|
||||
/// <typeparam name="T">The type of the minimum and maximum values.</typeparam>
|
||||
/// <seealso cref="IEquatable{Range{T}}" />
|
||||
/// <remarks>
|
||||
/// See also <see cref="IEquatable{T}"/> of <see cref="Range{T}"/>
|
||||
/// </remarks>
|
||||
public class Range<T> : IEquatable<Range<T>>
|
||||
where T : IComparable<T>
|
||||
{
|
||||
|
||||
@@ -24,7 +24,6 @@ public sealed class ContentPublishedNotification : EnumerableObjectNotification<
|
||||
public ContentPublishedNotification(IEnumerable<IContent> target, EventMessages messages, bool includeDescendants)
|
||||
: base(target, messages) => IncludeDescendants = includeDescendants;
|
||||
|
||||
/// </summary>
|
||||
public IEnumerable<IContent> PublishedEntities => Target;
|
||||
|
||||
public bool IncludeDescendants { get; }
|
||||
|
||||
@@ -3,7 +3,7 @@ using System.Text;
|
||||
namespace Umbraco.Cms.Core.Notifications;
|
||||
|
||||
/// <summary>
|
||||
/// Contains event data for the <see cref="ModelBindingException" /> event.
|
||||
/// Contains event data for the <see cref="T:Umbraco.Cms.Web.Common.ModelBinders.ModelBindingException" /> event.
|
||||
/// </summary>
|
||||
public class ModelBindingErrorNotification : INotification
|
||||
{
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
namespace Umbraco.Cms.Core.Notifications;
|
||||
|
||||
/// <summary>
|
||||
/// Notification that occurs at the very end of the Umbraco boot process (after all <see cref="IComponent" />s are
|
||||
/// Notification that occurs at the very end of the Umbraco boot process (after all <see cref="Composing.IComponent" />s are
|
||||
/// initialized).
|
||||
/// </summary>
|
||||
/// <seealso cref="Umbraco.Cms.Core.Notifications.IUmbracoApplicationLifetimeNotification" />
|
||||
/// <seealso cref="IUmbracoApplicationLifetimeNotification" />
|
||||
public class UmbracoApplicationStartingNotification : IUmbracoApplicationLifetimeNotification
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -2,9 +2,9 @@ namespace Umbraco.Cms.Core.Notifications;
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Notification that occurs when Umbraco is shutting down (after all <see cref="IComponent" />s are terminated).
|
||||
/// Notification that occurs when Umbraco is shutting down (after all <see cref="Composing.IComponent" />s are terminated).
|
||||
/// </summary>
|
||||
/// <seealso cref="Umbraco.Cms.Core.Notifications.IUmbracoApplicationLifetimeNotification" />
|
||||
/// <seealso cref="IUmbracoApplicationLifetimeNotification" />
|
||||
public class UmbracoApplicationStoppingNotification : IUmbracoApplicationLifetimeNotification
|
||||
{
|
||||
/// <summary>
|
||||
|
||||
@@ -22,7 +22,7 @@ public interface IPackageInstallation
|
||||
/// <summary>
|
||||
/// Reads the package xml and returns the <see cref="CompiledPackage" /> model
|
||||
/// </summary>
|
||||
/// <param name="packageFile"></param>
|
||||
/// <param name="packageXmlFile"></param>
|
||||
/// <returns></returns>
|
||||
CompiledPackage ReadPackage(XDocument? packageXmlFile);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ using Umbraco.Cms.Core.Models;
|
||||
namespace Umbraco.Cms.Core.Persistence.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a repository for <see cref="ICacheInstruction" /> entities.
|
||||
/// Represents a repository for <see cref="CacheInstruction" /> entities.
|
||||
/// </summary>
|
||||
public interface ICacheInstructionRepository : IRepository
|
||||
{
|
||||
|
||||
@@ -36,9 +36,8 @@ public interface IDocumentRepository : IContentRepository<int, IContent>, IReadR
|
||||
/// Gets <see cref="IContent" /> objects having an expiration date before (lower than, or equal to) a specified date.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The content returned from this method may be culture variant, in which case the resulting
|
||||
/// <see cref="IContent.ContentSchedule" /> should be queried
|
||||
/// for which culture(s) have been scheduled.
|
||||
/// The content returned from this method may be culture variant, in which case you can use
|
||||
/// <see cref="Umbraco.Extensions.ContentExtensions.GetStatus(IContent, ContentScheduleCollection, string?)" /> to get the status for a specific culture.
|
||||
/// </remarks>
|
||||
IEnumerable<IContent> GetContentForExpiration(DateTime date);
|
||||
|
||||
@@ -46,9 +45,8 @@ public interface IDocumentRepository : IContentRepository<int, IContent>, IReadR
|
||||
/// Gets <see cref="IContent" /> objects having a release date before (lower than, or equal to) a specified date.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The content returned from this method may be culture variant, in which case the resulting
|
||||
/// <see cref="IContent.ContentSchedule" /> should be queried
|
||||
/// for which culture(s) have been scheduled.
|
||||
/// The content returned from this method may be culture variant, in which case you can use
|
||||
/// <see cref="Umbraco.Extensions.ContentExtensions.GetStatus(IContent, ContentScheduleCollection, string?)" /> to get the status for a specific culture.
|
||||
/// </remarks>
|
||||
IEnumerable<IContent> GetContentForRelease(DateTime date);
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a default implementation for
|
||||
/// <see ref="IPropertyIndexValueFactory">, returning a single field to index containing the property value.
|
||||
/// <see ref="IPropertyIndexValueFactory" />, returning a single field to index containing the property value.
|
||||
/// </summary>
|
||||
public class DefaultPropertyIndexValueFactory : IPropertyIndexValueFactory
|
||||
{
|
||||
|
||||
@@ -17,7 +17,7 @@ public class VoidEditor : DataEditor
|
||||
/// Initializes a new instance of the <see cref="VoidEditor" /> class.
|
||||
/// </summary>
|
||||
/// <param name="aliasSuffix">An optional alias suffix.</param>
|
||||
/// <param name="loggerFactory">A logger factory.</param>
|
||||
/// <param name="dataValueEditorFactory">A data value editor factory.</param>
|
||||
/// <remarks>
|
||||
/// The default alias of the editor is "Umbraco.Void". When a suffix is provided,
|
||||
/// it is appended to the alias. Eg if the suffix is "Foo" the alias is "Umbraco.Void.Foo".
|
||||
@@ -39,7 +39,7 @@ public class VoidEditor : DataEditor
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VoidEditor" /> class.
|
||||
/// </summary>
|
||||
/// <param name="loggerFactory">A logger factory.</param>
|
||||
/// <param name="dataValueEditorFactory">A data value editor factory.</param>
|
||||
/// <remarks>The alias of the editor is "Umbraco.Void".</remarks>
|
||||
public VoidEditor(
|
||||
IDataValueEditorFactory dataValueEditorFactory)
|
||||
|
||||
@@ -19,7 +19,7 @@ public interface IPublishedContentCache : IPublishedCache
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If
|
||||
/// <param name="hideTopLevelNode" />
|
||||
/// <paramref name="hideTopLevelNode" />
|
||||
/// is <c>null</c> then the settings value is used.
|
||||
/// </para>
|
||||
/// <para>The value of <paramref name="preview" /> overrides defaults.</para>
|
||||
@@ -40,7 +40,7 @@ public interface IPublishedContentCache : IPublishedCache
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// If
|
||||
/// <param name="hideTopLevelNode" />
|
||||
/// <paramref name="hideTopLevelNode" />
|
||||
/// is <c>null</c> then the settings value is used.
|
||||
/// </para>
|
||||
/// <para>Considers published or unpublished content depending on defaults.</para>
|
||||
|
||||
@@ -47,7 +47,6 @@ public class DefaultUrlProvider : IUrlProvider
|
||||
/// <summary>
|
||||
/// Gets the other URLs of a published content.
|
||||
/// </summary>
|
||||
/// <param name="umbracoContextAccessor">The Umbraco context.</param>
|
||||
/// <param name="id">The published content id.</param>
|
||||
/// <param name="current">The current absolute URL.</param>
|
||||
/// <returns>The other URLs for the published content.</returns>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Routing;
|
||||
|
||||
@@ -101,7 +102,7 @@ public interface IPublishedRequest
|
||||
/// This flag is based on previous Umbraco versions but it is not clear how this flag can be set by developers
|
||||
/// since
|
||||
/// collission checking only occurs in the back office which is launched by
|
||||
/// <see cref="IPublishedRouter.TryRouteRequestAsync(IPublishedRequestBuilder)" />
|
||||
/// <see cref="PublishedRouter.TryRouteRequest(IPublishedRequestBuilder)" />
|
||||
/// for which events do not execute.
|
||||
/// </para>
|
||||
/// <para>
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.Globalization;
|
||||
using System.Net;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Routing;
|
||||
|
||||
@@ -162,7 +163,7 @@ public interface IPublishedRequestBuilder
|
||||
/// This flag is based on previous Umbraco versions but it is not clear how this flag can be set by developers
|
||||
/// since
|
||||
/// collission checking only occurs in the back office which is launched by
|
||||
/// <see cref="IPublishedRouter.TryRouteRequestAsync(IPublishedRequestBuilder)" />
|
||||
/// <see cref="PublishedRouter.TryRouteRequest(IPublishedRequestBuilder)" />
|
||||
/// for which events do not execute.
|
||||
/// </para>
|
||||
/// <para>
|
||||
|
||||
@@ -26,6 +26,7 @@ public interface IPublishedRouter
|
||||
/// Updates the request to use the specified <see cref="IPublishedContent" /> item, or NULL
|
||||
/// </summary>
|
||||
/// <param name="request">The request.</param>
|
||||
/// <param name="publishedContent">The published content.</param>
|
||||
/// <remarks>
|
||||
/// <returns>
|
||||
/// A new <see cref="IPublishedRequest" /> based on values from the original <see cref="IPublishedRequest" />
|
||||
|
||||
@@ -21,7 +21,6 @@ namespace Umbraco.Cms.Core.Routing
|
||||
/// <param name="urlProviders">The list of URL providers.</param>
|
||||
/// <param name="mediaUrlProviders">The list of media URL providers.</param>
|
||||
/// <param name="variationContextAccessor">The current variation accessor.</param>
|
||||
/// <param name="propertyEditorCollection"></param>
|
||||
public UrlProvider(IUmbracoContextAccessor umbracoContextAccessor, IOptions<WebRoutingSettings> routingSettings, UrlProviderCollection urlProviders, MediaUrlProviderCollection mediaUrlProviders, IVariationContextAccessor variationContextAccessor)
|
||||
{
|
||||
_umbracoContextAccessor = umbracoContextAccessor ?? throw new ArgumentNullException(nameof(umbracoContextAccessor));
|
||||
|
||||
@@ -6,6 +6,7 @@ public interface ILockingMechanism : IDisposable
|
||||
/// Read-locks some lock objects lazily.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">Instance id of the scope who is requesting the lock</param>
|
||||
/// <param name="timeout">Timeout for the lock</param>
|
||||
/// <param name="lockIds">Array of lock object identifiers.</param>
|
||||
void ReadLock(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds);
|
||||
|
||||
@@ -15,6 +16,7 @@ public interface ILockingMechanism : IDisposable
|
||||
/// Write-locks some lock objects lazily.
|
||||
/// </summary>
|
||||
/// <param name="instanceId">Instance id of the scope who is requesting the lock</param>
|
||||
/// <param name="timeout">Timeout for the lock</param>
|
||||
/// <param name="lockIds">Array of object identifiers.</param>
|
||||
void WriteLock(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds);
|
||||
|
||||
@@ -24,6 +26,7 @@ public interface ILockingMechanism : IDisposable
|
||||
/// Eagerly acquires a read-lock
|
||||
/// </summary>
|
||||
/// <param name="instanceId"></param>
|
||||
/// <param name="timeout">Timeout for the lock</param>
|
||||
/// <param name="lockIds"></param>
|
||||
void EagerReadLock(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds);
|
||||
|
||||
@@ -33,6 +36,7 @@ public interface ILockingMechanism : IDisposable
|
||||
/// Eagerly acquires a write-lock
|
||||
/// </summary>
|
||||
/// <param name="instanceId"></param>
|
||||
/// <param name="timeout">Timeout for the lock</param>
|
||||
/// <param name="lockIds"></param>
|
||||
void EagerWriteLock(Guid instanceId, TimeSpan? timeout = null, params int[] lockIds);
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user