diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index c0eaf680a3..06fd873638 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -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.
diff --git a/.github/README.md b/.github/README.md
index e633679795..08d7fea3f6 100644
--- a/.github/README.md
+++ b/.github/README.md
@@ -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)
diff --git a/Directory.Build.props b/Directory.Build.props
index a87ecd9ae7..b0b655f4b0 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -38,8 +38,8 @@
-
-
+
+
diff --git a/build/azure-pipelines.yml b/build/azure-pipelines.yml
index ef825ade5a..8113777d0d 100644
--- a/build/azure-pipelines.yml
+++ b/build/azure-pipelines.yml
@@ -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
diff --git a/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs
new file mode 100644
index 0000000000..79ce7cb71e
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Common/DependencyInjection/UmbracoBuilderAuthExtensions.cs
@@ -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();
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Common/OpenApi/SwaggerRouteTemplatePipelineFilter.cs b/src/Umbraco.Cms.Api.Common/OpenApi/SwaggerRouteTemplatePipelineFilter.cs
index f71badb4cd..4e19042e99 100644
--- a/src/Umbraco.Cms.Api.Common/OpenApi/SwaggerRouteTemplatePipelineFilter.cs
+++ b/src/Umbraco.Cms.Api.Common/OpenApi/SwaggerRouteTemplatePipelineFilter.cs
@@ -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>().Value;
diff --git a/src/Umbraco.Cms.Api.Common/Security/Paths.cs b/src/Umbraco.Cms.Api.Common/Security/Paths.cs
new file mode 100644
index 0000000000..44b6935d12
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Common/Security/Paths.cs
@@ -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}";
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Delivery/Configuration/ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions.cs b/src/Umbraco.Cms.Api.Delivery/Configuration/ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions.cs
new file mode 100644
index 0000000000..49f5a6fc56
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Delivery/Configuration/ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions.cs
@@ -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;
+
+///
+/// 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.
+///
+///
+/// This class is not used by the core CMS due to the required installation dependencies (local login page among other things).
+///
+public class ConfigureUmbracoMemberAuthenticationDeliveryApiSwaggerGenOptions : IConfigureOptions
+{
+ 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();
+ }
+
+ private class DeliveryApiSecurityFilter : SwaggerFilterBase, IOperationFilter
+ {
+ public void Apply(OpenApiOperation operation, OperationFilterContext context)
+ {
+ if (CanApply(context) is false)
+ {
+ return;
+ }
+
+ operation.Security = new List
+ {
+ new OpenApiSecurityRequirement
+ {
+ {
+ new OpenApiSecurityScheme
+ {
+ Reference = new OpenApiReference
+ {
+ Type = ReferenceType.SecurityScheme,
+ Id = AuthSchemeName,
+ }
+ },
+ new string[] { }
+ }
+ }
+ };
+ }
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdContentApiController.cs
index d16afc4d6d..877e662da7 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdContentApiController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdContentApiController.cs
@@ -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())
{
}
+ [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;
+
///
/// Gets a content item by id.
///
@@ -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 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);
}
}
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdsContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdsContentApiController.cs
index 5d415fffe6..df7e3b26a4 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdsContentApiController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByIdsContentApiController.cs
@@ -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())
{
}
+ [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;
+
///
/// Gets content items by ids.
///
@@ -28,12 +57,19 @@ public class ByIdsContentApiController : ContentApiItemControllerBase
[HttpGet("item")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)]
- public async Task Item([FromQuery(Name = "id")] HashSet ids)
+ [ProducesResponseType(StatusCodes.Status401Unauthorized)]
+ [ProducesResponseType(StatusCodes.Status403Forbidden)]
+ public async Task Item([FromQuery(Name = "id")] HashSet ids)
{
- IEnumerable 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();
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs
index 2d22887637..4806db45ff 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ByRouteContentApiController.cs
@@ -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())
+ {
+ }
+
+ [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;
}
///
@@ -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 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)));
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs
index ebfa32c479..405da6e15f 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiControllerBase.cs
@@ -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()),
};
+
+ ///
+ /// Creates a 403 Forbidden result.
+ ///
+ ///
+ /// Use this method instead of 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.
+ ///
+ protected IActionResult Forbidden() => new StatusCodeResult(StatusCodes.Status403Forbidden);
}
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiItemControllerBase.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiItemControllerBase.cs
index dad11de009..895cd376cd 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiItemControllerBase.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/ContentApiItemControllerBase.cs
@@ -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();
+
+ [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 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 HandleMemberAccessAsync(IEnumerable 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;
+ }
}
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/QueryContentApiController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/QueryContentApiController.cs
index d4db82d9be..2f4e8af9c8 100644
--- a/src/Umbraco.Cms.Api.Delivery/Controllers/Content/QueryContentApiController.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Content/QueryContentApiController.cs
@@ -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())
+ {
+ }
+
+ [ActivatorUtilitiesConstructor]
+ public QueryContentApiController(
+ IApiPublishedContentCache apiPublishedContentCache,
+ IApiContentResponseBuilder apiContentResponseBuilderBuilder,
+ IApiContentQueryService apiContentQueryService,
+ IRequestMemberAccessService requestMemberAccessService)
: base(apiPublishedContentCache, apiContentResponseBuilderBuilder)
- => _apiContentQueryService = apiContentQueryService;
+ {
+ _apiContentQueryService = apiContentQueryService;
+ _requestMemberAccessService = requestMemberAccessService;
+ }
///
/// 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, ApiContentQueryOperationStatus> queryAttempt = _apiContentQueryService.ExecuteQuery(fetch, filter, sort, skip, take);
+ ProtectedAccess protectedAccess = await _requestMemberAccessService.MemberAccessAsync();
+ Attempt, 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);
}
}
diff --git a/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs b/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs
new file mode 100644
index 0000000000..a9c670b7ad
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Delivery/Controllers/Security/MemberController.cs
@@ -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 _logger;
+
+ public MemberController(
+ IHttpContextAccessor httpContextAccessor,
+ IMemberSignInManager memberSignInManager,
+ IMemberManager memberManager,
+ IOptions deliveryApiSettings,
+ ILogger logger)
+ {
+ _httpContextAccessor = httpContextAccessor;
+ _memberSignInManager = memberSignInManager;
+ _memberManager = memberManager;
+ _logger = logger;
+ _deliveryApiSettings = deliveryApiSettings.Value;
+ }
+
+ [HttpGet("authorize")]
+ [MapToApiVersion("1.0")]
+ public async Task 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 Signout()
+ {
+ await _memberSignInManager.SignOutAsync();
+ return SignOut(OpenIddictServerAspNetCoreDefaults.AuthenticationScheme);
+ }
+
+ private async Task 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 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
+ {
+ [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 SignInMember(MemberIdentityUser member, OpenIddictRequest request)
+ {
+ ClaimsPrincipal memberPrincipal = await _memberSignInManager.CreateUserPrincipalAsync(member);
+ memberPrincipal.SetClaim(OpenIddictConstants.Claims.Subject, member.Key.ToString());
+
+ IList 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);
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs
index d87741746a..86c8583708 100644
--- a/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs
+++ b/src/Umbraco.Cms.Api.Delivery/DependencyInjection/UmbracoBuilderExtensions.cs
@@ -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();
builder.Services.AddSingleton();
builder.Services.AddSingleton();
+ builder.Services.AddTransient();
+ builder.Services.AddTransient();
builder.Services.ConfigureOptions();
builder.AddUmbracoApiOpenApiUI();
@@ -44,6 +52,16 @@ public static class UmbracoBuilderExtensions
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
+ builder.Services.AddAuthentication();
+ builder.AddUmbracoOpenIddict();
+ builder.AddNotificationAsyncHandler();
+ builder.AddNotificationAsyncHandler();
+ builder.AddNotificationAsyncHandler();
+ builder.AddNotificationAsyncHandler();
+ builder.AddNotificationAsyncHandler();
+
+ // FIXME: remove this when Delivery API V1 is removed
+ builder.Services.AddSingleton();
return builder;
}
}
diff --git a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs
index 32791b9b5e..36721cc0f2 100644
--- a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerDocumentationFilterBase.cs
@@ -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 : IOperationFilter, IParameterFilter
+internal abstract class SwaggerDocumentationFilterBase
+ : SwaggerFilterBase, 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 : 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 : 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() is true;
}
diff --git a/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerFilterBase.cs b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerFilterBase.cs
new file mode 100644
index 0000000000..7992ac279a
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Delivery/Filters/SwaggerFilterBase.cs
@@ -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
+ 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() is true;
+}
diff --git a/src/Umbraco.Cms.Api.Delivery/Handlers/InitializeMemberApplicationNotificationHandler.cs b/src/Umbraco.Cms.Api.Delivery/Handlers/InitializeMemberApplicationNotificationHandler.cs
new file mode 100644
index 0000000000..242fd47857
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Delivery/Handlers/InitializeMemberApplicationNotificationHandler.cs
@@ -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
+{
+ private readonly IMemberApplicationManager _memberApplicationManager;
+ private readonly IRuntimeState _runtimeState;
+ private readonly ILogger _logger;
+ private readonly DeliveryApiSettings _deliveryApiSettings;
+
+ public InitializeMemberApplicationNotificationHandler(
+ IMemberApplicationManager memberApplicationManager,
+ IRuntimeState runtimeState,
+ IOptions deliveryApiSettings,
+ ILogger 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;
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Delivery/Handlers/RevokeMemberAuthenticationTokensNotificationHandler.cs b/src/Umbraco.Cms.Api.Delivery/Handlers/RevokeMemberAuthenticationTokensNotificationHandler.cs
new file mode 100644
index 0000000000..81d0fb4132
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Delivery/Handlers/RevokeMemberAuthenticationTokensNotificationHandler.cs
@@ -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,
+ INotificationAsyncHandler,
+ INotificationAsyncHandler,
+ INotificationAsyncHandler
+{
+ private readonly IMemberService _memberService;
+ private readonly IOpenIddictTokenManager _tokenManager;
+ private readonly bool _enabled;
+ private readonly ILogger _logger;
+
+ public RevokeMemberAuthenticationTokensNotificationHandler(
+ IMemberService memberService,
+ IOpenIddictTokenManager tokenManager,
+ IOptions deliveryApiSettings,
+ ILogger 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);
+ }
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Delivery/Routing/DeliveryApiItemsEndpointsMatcherPolicy.cs b/src/Umbraco.Cms.Api.Delivery/Routing/DeliveryApiItemsEndpointsMatcherPolicy.cs
new file mode 100644
index 0000000000..ec1b222cd0
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Delivery/Routing/DeliveryApiItemsEndpointsMatcherPolicy.cs
@@ -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 endpoints)
+ {
+ for (var i = 0; i < endpoints.Count; i++)
+ {
+ ControllerActionDescriptor? controllerActionDescriptor = endpoints[i].Metadata.GetMetadata();
+ 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();
+ 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(controllerActionDescriptor) || IsControllerType(controllerActionDescriptor);
+
+ private static bool IsByRouteController(ControllerActionDescriptor? controllerActionDescriptor)
+ => IsControllerType(controllerActionDescriptor) || IsControllerType(controllerActionDescriptor);
+
+ private static bool IsControllerType(ControllerActionDescriptor? controllerActionDescriptor)
+ => controllerActionDescriptor?.MethodInfo.DeclaringType == typeof(T);
+}
diff --git a/src/Umbraco.Cms.Api.Delivery/Security/MemberApplicationManager.cs b/src/Umbraco.Cms.Api.Delivery/Security/MemberApplicationManager.cs
new file mode 100644
index 0000000000..1e1be7f420
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Delivery/Security/MemberApplicationManager.cs
@@ -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 loginRedirectUrls, IEnumerable 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);
+}
diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs
index 45ae32b4f5..73e5bb4a53 100644
--- a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryProvider.cs
@@ -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 _logger;
private readonly string _fallbackGuidValue;
private readonly Dictionary _fieldTypes;
@@ -25,9 +29,11 @@ internal sealed class ApiContentQueryProvider : IApiContentQueryProvider
public ApiContentQueryProvider(
IExamineManager examineManager,
ContentIndexHandlerCollection indexHandlers,
+ IOptions deliveryApiSettings,
ILogger 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 ExecuteQuery(SelectorOption selectorOption, IList filterOptions, IList 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 ExecuteQuery(
+ SelectorOption selectorOption,
+ IList filterOptions,
+ IList sortOptions,
+ string culture,
+ bool preview,
+ int skip,
+ int take)
+ => ExecuteQuery(selectorOption, filterOptions, sortOptions, culture, ProtectedAccess.None, preview, skip, take);
+
+ ///
+ public PagedModel ExecuteQuery(
+ SelectorOption selectorOption,
+ IList filterOptions,
+ IList 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();
}
- 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();
+ 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 filterOptions, IBooleanOperation queryOperation)
{
void HandleExact(IQuery query, string fieldName, string[] values)
diff --git a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryService.cs b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryService.cs
index dc03a79e19..a81ae211ed 100644
--- a/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryService.cs
+++ b/src/Umbraco.Cms.Api.Delivery/Services/ApiContentQueryService.cs
@@ -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, ApiContentQueryOperationStatus> ExecuteQuery(
+ string? fetch,
+ IEnumerable filters,
+ IEnumerable sorts,
+ int skip,
+ int take)
+ => ExecuteQuery(fetch, filters, sorts, ProtectedAccess.None, skip, take);
+
///
- public Attempt, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable filters, IEnumerable sorts, int skip, int take)
+ public Attempt, ApiContentQueryOperationStatus> ExecuteQuery(
+ string? fetch,
+ IEnumerable filters,
+ IEnumerable sorts,
+ ProtectedAccess protectedAccess,
+ int skip,
+ int take)
{
var emptyResult = new PagedModel();
@@ -77,7 +93,7 @@ internal sealed class ApiContentQueryService : IApiContentQueryService
var culture = _variationContextAccessor.VariationContext?.Culture ?? string.Empty;
var isPreview = _requestPreviewService.IsPreview();
- PagedModel result = _apiContentQueryProvider.ExecuteQuery(selectorOption, filterOptions, sortOptions, culture, isPreview, skip, take);
+ PagedModel result = _apiContentQueryProvider.ExecuteQuery(selectorOption, filterOptions, sortOptions, culture, protectedAccess, isPreview, skip, take);
return Attempt.SucceedWithStatus(ApiContentQueryOperationStatus.Success, result);
}
diff --git a/src/Umbraco.Cms.Api.Delivery/Services/RequestMemberAccessService.cs b/src/Umbraco.Cms.Api.Delivery/Services/RequestMemberAccessService.cs
new file mode 100644
index 0000000000..c0bd9c1b9e
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Delivery/Services/RequestMemberAccessService.cs
@@ -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)
+ {
+ _httpContextAccessor = httpContextAccessor;
+ _publicAccessService = publicAccessService;
+ _publicAccessChecker = publicAccessChecker;
+
+ _deliveryApiSettings = deliveryApiSettings.Value;
+ }
+
+ public async Task 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 MemberAccessAsync()
+ {
+ ClaimsPrincipal? requestPrincipal = await GetRequestPrincipal();
+ return new ProtectedAccess(MemberKey(requestPrincipal), MemberRoles(requestPrincipal));
+ }
+
+ private async Task 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);
+}
diff --git a/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpImageUrlGenerator.cs b/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpImageUrlGenerator.cs
index 5b437902f5..afcd0f35a2 100644
--- a/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpImageUrlGenerator.cs
+++ b/src/Umbraco.Cms.Imaging.ImageSharp/Media/ImageSharpImageUrlGenerator.cs
@@ -49,6 +49,7 @@ public sealed class ImageSharpImageUrlGenerator : IImageUrlGenerator
/// Initializes a new instance of the class.
///
/// The supported image file types/extensions.
+ /// The ImageSharp middleware options.
/// Contains helpers that allow authorization of image requests.
///
/// This constructor is only used for testing.
diff --git a/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs b/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs
index d5d83f8ecf..ef9b9443ae 100644
--- a/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs
+++ b/src/Umbraco.Cms.Persistence.EFCore/Locking/SqlServerEFCoreDistributedLockingMechanism.cs
@@ -22,7 +22,7 @@ internal class SqlServerEFCoreDistributedLockingMechanism : IDistributedLocki
private readonly Lazy> _scopeAccessor; // Hooray it's a circular dependency.
///
- /// Initializes a new instance of the class.
+ /// Initializes a new instance of the class.
///
public SqlServerEFCoreDistributedLockingMechanism(
ILogger> logger,
diff --git a/src/Umbraco.Core/Cache/Refreshers/CacheRefresherBase.cs b/src/Umbraco.Core/Cache/Refreshers/CacheRefresherBase.cs
index 849d42309a..0873d32cb8 100644
--- a/src/Umbraco.Core/Cache/Refreshers/CacheRefresherBase.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/CacheRefresherBase.cs
@@ -35,7 +35,7 @@ public abstract class CacheRefresherBase : ICacheRefresher
public abstract string Name { get; }
///
- /// Gets the for
+ /// Gets the for .
///
protected ICacheRefresherNotificationFactory NotificationFactory { get; }
diff --git a/src/Umbraco.Core/Cache/Refreshers/JsonCacheRefresherBase.cs b/src/Umbraco.Core/Cache/Refreshers/JsonCacheRefresherBase.cs
index b22cff56d2..f638ab34b0 100644
--- a/src/Umbraco.Core/Cache/Refreshers/JsonCacheRefresherBase.cs
+++ b/src/Umbraco.Core/Cache/Refreshers/JsonCacheRefresherBase.cs
@@ -14,7 +14,7 @@ public abstract class JsonCacheRefresherBase : Cach
where TNotification : CacheRefresherNotification
{
///
- /// Initializes a new instance of the .
+ /// Initializes a new instance of the class.
///
protected JsonCacheRefresherBase(
AppCaches appCaches,
diff --git a/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs b/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs
index baf131ca80..3b0994c614 100644
--- a/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs
+++ b/src/Umbraco.Core/Collections/EventClearingObservableCollection.cs
@@ -37,7 +37,7 @@ public class EventClearingObservableCollection : ObservableCollection
- /// Clears all event handlers for the event
+ /// Clears all event handlers for the event.
///
public void ClearCollectionChangedEvents() => _changed = null;
diff --git a/src/Umbraco.Core/Collections/ObservableDictionary.cs b/src/Umbraco.Core/Collections/ObservableDictionary.cs
index 9e52b4dae7..8d920bbe98 100644
--- a/src/Umbraco.Core/Collections/ObservableDictionary.cs
+++ b/src/Umbraco.Core/Collections/ObservableDictionary.cs
@@ -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 : ObservableCollection,
}
///
- /// Clears all event handlers
+ /// Clears all event handlers
///
public void ClearCollectionChangedEvents() => _changed = null;
diff --git a/src/Umbraco.Core/Configuration/IUmbracoVersion.cs b/src/Umbraco.Core/Configuration/IUmbracoVersion.cs
index 3672f28dae..99a5cddfdb 100644
--- a/src/Umbraco.Core/Configuration/IUmbracoVersion.cs
+++ b/src/Umbraco.Core/Configuration/IUmbracoVersion.cs
@@ -1,3 +1,4 @@
+using System.Reflection;
using Umbraco.Cms.Core.Semver;
namespace Umbraco.Cms.Core.Configuration;
diff --git a/src/Umbraco.Core/Configuration/Models/DeliveryApiSettings.cs b/src/Umbraco.Core/Configuration/Models/DeliveryApiSettings.cs
index 69b1943c60..c81102b8d2 100644
--- a/src/Umbraco.Core/Configuration/Models/DeliveryApiSettings.cs
+++ b/src/Umbraco.Core/Configuration/Models/DeliveryApiSettings.cs
@@ -54,6 +54,19 @@ public class DeliveryApiSettings
///
public MediaSettings Media { get; set; } = new ();
+ ///
+ /// Gets or sets the member authorization settings for the Delivery API.
+ ///
+ public MemberAuthorizationSettings? MemberAuthorization { get; set; } = null;
+
+ ///
+ /// Gets a value indicating if any member authorization type is enabled for the Delivery API.
+ ///
+ ///
+ /// This method is intended for future extension - see remark in .
+ ///
+ public bool MemberAuthorizationIsEnabled() => MemberAuthorization?.AuthorizationCodeFlow?.Enabled is true;
+
///
/// Typed configuration options for the Media APIs of the Delivery API.
///
@@ -84,4 +97,45 @@ public class DeliveryApiSettings
[DefaultValue(StaticPublicAccess)]
public bool PublicAccess { get; set; } = StaticPublicAccess;
}
+
+ ///
+ /// Typed configuration options for member authorization settings for the Delivery API.
+ ///
+ ///
+ /// This class is intended for future extension, if/when adding support for additional
+ /// authorization flows (i.e. non-interactive authorization flows).
+ ///
+ public class MemberAuthorizationSettings
+ {
+ ///
+ /// Gets or sets the Authorization Code Flow configuration for the Delivery API.
+ ///
+ public AuthorizationCodeFlowSettings? AuthorizationCodeFlow { get; set; } = null;
+ }
+
+ ///
+ /// Typed configuration options for the Authorization Code Flow settings for the Delivery API.
+ ///
+ public class AuthorizationCodeFlowSettings
+ {
+ ///
+ /// Gets or sets a value indicating whether Authorization Code Flow should be enabled for the Delivery API.
+ ///
+ /// true if Authorization Code Flow should be enabled; otherwise, false.
+ [DefaultValue(StaticEnabled)]
+ public bool Enabled { get; set; } = StaticEnabled;
+
+ ///
+ /// Gets or sets the URLs allowed to use as redirect targets after a successful login (session authorization).
+ ///
+ /// The URLs allowed as redirect targets.
+ public Uri[] LoginRedirectUrls { get; set; } = Array.Empty();
+
+ ///
+ /// Gets or sets the URLs allowed to use as redirect targets after a successful logout (session termination).
+ ///
+ /// The URLs allowed as redirect targets.
+ /// These are only required if logout is to be used.
+ public Uri[] LogoutRedirectUrls { get; set; } = Array.Empty();
+ }
}
diff --git a/src/Umbraco.Core/Constants-CharArrays.cs b/src/Umbraco.Core/Constants-CharArrays.cs
index 832cac00e6..98a450e9c8 100644
--- a/src/Umbraco.Core/Constants-CharArrays.cs
+++ b/src/Umbraco.Core/Constants-CharArrays.cs
@@ -53,7 +53,7 @@ public static partial class Constants
public static readonly char[] Comma = { ',' };
///
- /// Char array containing only &
+ /// Char array containing only &
///
public static readonly char[] Ampersand = { '&' };
@@ -88,7 +88,7 @@ public static partial class Constants
public static readonly char[] QuestionMark = { '?' };
///
- /// Char array containing ? &
+ /// Char array containing ? &
///
public static readonly char[] QuestionMarkAmpersand = { '?', '&' };
diff --git a/src/Umbraco.Core/Constants-OAuthClaims.cs b/src/Umbraco.Core/Constants-OAuthClaims.cs
new file mode 100644
index 0000000000..19e92b20e6
--- /dev/null
+++ b/src/Umbraco.Core/Constants-OAuthClaims.cs
@@ -0,0 +1,17 @@
+namespace Umbraco.Cms.Core;
+
+public static partial class Constants
+{
+ public static class OAuthClaims
+ {
+ ///
+ /// Key for authenticated member.
+ ///
+ public const string MemberKey = "umbraco-member-key";
+
+ ///
+ /// Roles for authenticated member.
+ ///
+ public const string MemberRoles = "umbraco-member-roles";
+ }
+}
diff --git a/src/Umbraco.Core/Constants-OAuthClientIds.cs b/src/Umbraco.Core/Constants-OAuthClientIds.cs
new file mode 100644
index 0000000000..1938618fca
--- /dev/null
+++ b/src/Umbraco.Core/Constants-OAuthClientIds.cs
@@ -0,0 +1,12 @@
+namespace Umbraco.Cms.Core;
+
+public static partial class Constants
+{
+ public static class OAuthClientIds
+ {
+ ///
+ /// Client ID used for member access.
+ ///
+ public const string Member = "umbraco-member";
+ }
+}
diff --git a/src/Umbraco.Core/DelegateEqualityComparer.cs b/src/Umbraco.Core/DelegateEqualityComparer.cs
index 8a442e8f85..44d12364cb 100644
--- a/src/Umbraco.Core/DelegateEqualityComparer.cs
+++ b/src/Umbraco.Core/DelegateEqualityComparer.cs
@@ -33,8 +33,8 @@ public class DelegateEqualityComparer : IEqualityComparer
///
/// true if the specified objects are equal; otherwise, false.
///
- /// The first object of type to compare.
- /// The second object of type to compare.
+ /// The first object of type to compare.
+ /// The second object of type to compare.
public bool Equals(T? x, T? y) => _equals.Invoke(x, y);
///
diff --git a/src/Umbraco.Core/DeliveryApi/IApiContentQueryProvider.cs b/src/Umbraco.Core/DeliveryApi/IApiContentQueryProvider.cs
index 69d7025d60..1e49ae7ffa 100644
--- a/src/Umbraco.Core/DeliveryApi/IApiContentQueryProvider.cs
+++ b/src/Umbraco.Core/DeliveryApi/IApiContentQueryProvider.cs
@@ -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;
///
public interface IApiContentQueryProvider
{
+ [Obsolete($"Use the {nameof(ExecuteQuery)} method that accepts {nameof(ProtectedAccess)}. Will be removed in V14.")]
+ PagedModel ExecuteQuery(
+ SelectorOption selectorOption,
+ IList filterOptions,
+ IList sortOptions,
+ string culture,
+ bool preview,
+ int skip,
+ int take);
+
///
/// Returns a page of item ids that passed the search criteria.
///
@@ -15,10 +26,19 @@ public interface IApiContentQueryProvider
/// The sorting options of the search criteria.
/// The requested culture.
/// Whether or not to search for preview content.
+ /// Defines the limitations for querying protected content.
/// Number of search results to skip (for pagination).
/// Number of search results to retrieve (for pagination).
/// A paged model containing the resulting IDs and the total number of results that matching the search criteria.
- PagedModel ExecuteQuery(SelectorOption selectorOption, IList filterOptions, IList sortOptions, string culture, bool preview, int skip, int take);
+ PagedModel ExecuteQuery(
+ SelectorOption selectorOption,
+ IList filterOptions,
+ IList sortOptions,
+ string culture,
+ ProtectedAccess protectedAccess,
+ bool preview,
+ int skip,
+ int take) => new();
///
/// 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).
diff --git a/src/Umbraco.Core/DeliveryApi/IApiContentQueryService.cs b/src/Umbraco.Core/DeliveryApi/IApiContentQueryService.cs
index 4a01cd926c..03ff82b9c2 100644
--- a/src/Umbraco.Core/DeliveryApi/IApiContentQueryService.cs
+++ b/src/Umbraco.Core/DeliveryApi/IApiContentQueryService.cs
@@ -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;
///
public interface IApiContentQueryService
{
+ [Obsolete($"Use the {nameof(ExecuteQuery)} method that accepts {nameof(ProtectedAccess)}. Will be removed in V14.")]
+ Attempt, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable filters, IEnumerable sorts, int skip, int take);
+
///
/// Returns an attempt with a collection of item ids that passed the search criteria as a paged model.
///
@@ -16,6 +20,8 @@ public interface IApiContentQueryService
/// Optional sort query parameters values.
/// The amount of items to skip.
/// The amount of items to take.
+ /// Defines the limitations for querying protected content.
/// A paged model of item ids that are returned after applying the search queries in an attempt.
- Attempt, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable filters, IEnumerable sorts, int skip, int take);
+ Attempt, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable filters, IEnumerable sorts, ProtectedAccess protectedAccess, int skip, int take)
+ => default;
}
diff --git a/src/Umbraco.Core/DeliveryApi/IRequestMemberAccessService.cs b/src/Umbraco.Core/DeliveryApi/IRequestMemberAccessService.cs
new file mode 100644
index 0000000000..4d41e97fcb
--- /dev/null
+++ b/src/Umbraco.Core/DeliveryApi/IRequestMemberAccessService.cs
@@ -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 MemberHasAccessToAsync(IPublishedContent content);
+
+ Task MemberAccessAsync();
+}
diff --git a/src/Umbraco.Core/DeliveryApi/NoopApiContentQueryService.cs b/src/Umbraco.Core/DeliveryApi/NoopApiContentQueryService.cs
index d8dda6313c..49cf782d40 100644
--- a/src/Umbraco.Core/DeliveryApi/NoopApiContentQueryService.cs
+++ b/src/Umbraco.Core/DeliveryApi/NoopApiContentQueryService.cs
@@ -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
{
- ///
+ [Obsolete($"Use the {nameof(ExecuteQuery)} method that accepts {nameof(ProtectedAccess)}. Will be removed in V14.")]
public Attempt, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable filters, IEnumerable sorts, int skip, int take)
=> Attempt.SucceedWithStatus(ApiContentQueryOperationStatus.Success, new PagedModel());
+
+ ///
+ public Attempt, ApiContentQueryOperationStatus> ExecuteQuery(string? fetch, IEnumerable filters, IEnumerable sorts, ProtectedAccess protectedAccess, int skip, int take)
+ => Attempt.SucceedWithStatus(ApiContentQueryOperationStatus.Success, new PagedModel());
}
diff --git a/src/Umbraco.Core/DeliveryApi/NoopRequestMemberAccessService.cs b/src/Umbraco.Core/DeliveryApi/NoopRequestMemberAccessService.cs
new file mode 100644
index 0000000000..b16a1cf89b
--- /dev/null
+++ b/src/Umbraco.Core/DeliveryApi/NoopRequestMemberAccessService.cs
@@ -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 MemberHasAccessToAsync(IPublishedContent content) => Task.FromResult(PublicAccessStatus.AccessAccepted);
+
+ public Task MemberAccessAsync() => Task.FromResult(ProtectedAccess.None);
+}
diff --git a/src/Umbraco.Core/DependencyInjection/ServiceProviderExtensions.cs b/src/Umbraco.Core/DependencyInjection/ServiceProviderExtensions.cs
index 9c2202e2aa..a12cc21dd6 100644
--- a/src/Umbraco.Core/DependencyInjection/ServiceProviderExtensions.cs
+++ b/src/Umbraco.Core/DependencyInjection/ServiceProviderExtensions.cs
@@ -6,7 +6,7 @@ using Umbraco.Cms.Core.Models.PublishedContent;
namespace Umbraco.Extensions;
///
-/// Provides extension methods to the class.
+/// Provides extension methods to the class.
///
public static class ServiceProviderExtensions
{
@@ -28,7 +28,7 @@ public static class ServiceProviderExtensions
///
/// Creates an instance of a service, with arguments.
///
- /// The
+ /// The .
/// The type of the instance.
/// Named arguments.
/// An instance of the specified type.
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/bs.xml b/src/Umbraco.Core/EmbeddedResources/Lang/bs.xml
index 212840974c..d7093a2ba5 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/bs.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/bs.xml
@@ -158,6 +158,12 @@
Više opcija za objavljivanje
Pošalji
+
+ Mediji je izbrisan
+ Mediji premješten
+ Mediji kopiran
+ Mediji spremljen
+
Pregled za
Sadržaj je izbrisan
@@ -2653,7 +2659,7 @@ Da upravljate svojom web lokacijom, jednostavno otvorite Umbraco backoffice i po
Želite savladati Umbraco? Provedite nekoliko minuta učeći neke najbolje prakse gledajući jedan od ovih videozapisa o korištenju Umbraco-a Umbraco Learning Base Youtube kanal. Ovdje možete pronaći gomilu video materijala koji pokriva mnoge aspekte Umbraco-a.
+ Želite savladati Umbraco? Provedite nekoliko minuta učeći neke najbolje prakse gledajući jedan od ovih videozapisa o korištenju Umbraco-a Umbraco Learning Base Youtube kanal. Ovdje možete pronaći gomilu video materijala koji pokriva mnoge aspekte Umbraco-a.
]]>
Za početak
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml b/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml
index 07d7c6d28f..347f963e70 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/cs.xml
@@ -151,6 +151,12 @@
Potvrdit
Další možnosti publikování
+
+ Média smazán
+ Média přesunut
+ Média zkopírován
+ Média uložen
+
Zobrazení pro
Obsah smazán
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml b/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml
index 50db692223..86520b6d03 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/cy.xml
@@ -35,7 +35,6 @@
Dileu
Ailenwi
Adfer
- Gosod hawliau ar gyfer y dudalen %0%
Dewis ble i copïo
Dewis ble i symud
Yn y strwythyr goeden isod
@@ -58,6 +57,7 @@
Ail-anfon Gwahoddiad
Golygu cynnwys
Dewiswch ble i fewnforio
+ Cuddio opsiynau nad ydynt ar gael
Cynnwys
@@ -93,7 +93,8 @@
Dim hawl.
- Ychwanegu Parth newydd
+ Ychwanegu parth newydd
+ Ychwanegu parth cyfredol
dileu
Nod annilys.
Fformat parth annilys.
@@ -104,7 +105,7 @@
Parth '%0%' wedi dileu
Parth '%0%' wedi neilltuo eisoes
Parth '%0%' wedi diweddaru
- Golygu Parthau Presennol
+ Golygu Parthau Cyfredol
@@ -144,11 +145,9 @@
Achub
Achub a cau
Achub a chyhoeddi
- Achub ac amserlenni
Achub ac anfon am gymeradwyo
Achub gwedd rhestr
Amserlenni
- Rhagolwg
Save and preview
Rhagolwg wedi analluogi gan nad oes templed wedi'i neilltuo
Dewis arddull
@@ -158,13 +157,17 @@
Achub a chynhyrchu modelau
Dadwneud
Ail-wneud
- Rolio yn ôl
Dileu tag
Canslo
Cadarnhau
Mwy opsiynau cyhoeddi
- Submit
- Submit and close
+ Cyflwyno
+
+
+ Cyfrwng wedi'i dileu
+ Cyfrwng wedi'i symud
+ Cyfrwng wedi'i copïo
+ Cyfrwng wedi'i achub
Dangos am
@@ -180,7 +183,6 @@
Cynnwys wedi'i rolio yn ôl
Cynnwys wedi'i anfon i Gyhoeddi
Cynnwys wedi'i anfon i gyhoeddi am y ieithoedd: %0%
- Cynnwys wedi'i anfon i gyfieithu
Trefnu eitemau blant cyflawnwyd gan ddefnyddiwr
%0%
Copïo
@@ -195,7 +197,6 @@
Rolio yn ôl
Anfon i Gyhoeddi
Anfon i Gyhoeddi
- Anfon i Gyfieithu
Tefnu
Arferu
Hanes (pob amrywiad)
@@ -205,8 +206,6 @@
Achub
- Methwyd creu ffolder o dan id rhiant %0%
- Methwyd creu ffolder o dan rhiant efo enw %0%
Mae'r enw'r ffolder methu cynnwys nodau anghyfreithlon.
Methwyd dileu eitem: %0%
@@ -296,23 +295,13 @@
Cynnwys eitemau cynnwys heb eu cyhoeddi.
Mae'r gwerth yma'n gudd. Os ydych chi angen hawl i weld y gwerth yma, cysylltwch â gweinyddwr eich gwefan.
Mae'r gwerth yma'n gudd.
- Pa ieithoedd yr hoffech chi eu cyhoeddi? Mae pob iaith sydd â chynnwys wei cael ei arbed!
Pa ieithoedd yr hoffech chi eu cyhoeddi?
- Pa ieithoedd yr hoffech chi eu arbed?
- Mae pob iaith sydd â chynnwys yn cael ei arbed wrth greu!
Pa ieithoedd hoffech chi anfon am gymeradwyaeth?
Pa ieithoedd yr hoffech chi eu hamserlennu?
Dewiswch yr ieithoedd i'w anghyhoeddi. Bydd anghyhoeddi iaith orfodol yn anghyhoeddi pob iaith.
- Ieithoedd Cyhoeddedig
- Ieithoedd heb ei gyhoeddi
- Ieithoedd heb eu haddasu
- Nid yw'r ieithoedd hyn wedi'u creu
Bydd pob amrywiad newydd yn cael ei arbed.
P'un amrywiadau wyt ti eisiau cyhoeddi?
Dewiswch pa amrywiadau wyt ti eisiau arbed.
- Dewiswch pa amrywiadau i anfon am gymeradwyaeth.
- Gosod cyhoeddi rhestredig...
- Dewiswch yr amrywiadau i'w anghyhoeddi. Bydd anghyhoeddi iaith orfodol yn anghyhoeddi pob amrywiad.
Mae'r amrywiadau canlynol yn ofynnol er mwyn i gyhoeddi:
Ni ddim yn barod i Gyhoeddi
Barod i Gyhoeddi?
@@ -341,23 +330,18 @@
Cliciwch i lanlwytho
- Gollyngwch eich ffeiliau yma...
- Dolen i gyfrwng
neu cliciwch yma i ddewis ffeiliau
- Gallwch lusgo ffeiliau yma i lanlwtho.
- Dim ond mathau caniatol o ffeil sydd
Ni ellir lanlwytho'r ffeil yma, nid yw math y ffeil yn wedi'i gymeradwyo
Maint ffeil uchaf
Gwraidd gyfrwng
- Methwyd symud cyfrwng
Ni all y ffolderi rhiant a chyrchfan fod yr un peth
- Methwyd copïo cyfrwng
Methwyd creu ffolder o dan id rhiant %0%
Methwyd ailenwi'r ffolder gyda id %0%
Llusgo a gollwng eich ffeil(iau) i mewn i'r ardal
Ni chaniateir llwytho i fyny yn y lleoliad hwn.
Ni ellir lanlwytho'r ffeil yma, ni chaniateir y math cyfrwng gydag alias '%0%' yma
Ni ellir lanlwytho'r ffeil yma, nid oes ganddi enw ffeil dilys
+ Mae un neu fwy o ddilysiadau diogelwch ffeil wedi methu
Creu aelod newydd
@@ -368,6 +352,7 @@
Mae gan yr aelod gyfrinair yn barod
Nid yw cloi allan wedi'i alluogi ar gyfer yr aelod hwn
Nid yw'r aelod yn y grŵp '%0%'
+ Prawf Dilysu Dau Gam
Wedi methu copïo'r fath cynnwys
@@ -417,7 +402,6 @@
Macro rhan-wedd newydd (heb macro)
Ffeil ddalen arddull newydd
Ffeil ddalen arddull Golygydd Testun Cyfoethog newydd
- Macro rhan-wedd wag newydd
Pori eich gwefan
@@ -466,17 +450,12 @@
Dolen
Angor / llinyn ymholi
Enw
- Gweinyddu enwau gwesteia
Cau'r ffenestr yma
Ydych chi'n sicr eich bod eisiau dileu
%0% yn seiliedig ar %1%]]>
-
Ydych chi'n sicr eich bod eisiau analluogi
-
Wyt ti'n siŵr fod ti eisiau dileu
%0%]]>
- %0%]]>
-
Ydych chi'n sicr?
Ydych chi'n sicr?
Torri
@@ -497,7 +476,6 @@
Dolen fewnol:
Wrth ddefnyddio dolenni leol, defnyddiwch "#" o flaen y ddolen
Agor mewn ffenestr newydd?
- Gosodiadau Macro
Nid yw'r macro yma yn cynnwys unrhyw briodweddau gallwch chi olygu
Gludo
Golygu hawliau ar gyfer
@@ -516,25 +494,15 @@
Bydd storfa'r wefan yn cael ei adnewyddu. Bydd holl gynnwys cyhoeddi yn cael ei ddiweddaru, ac bydd holl gynnwys sydd heb ei gyhoeddi yn dal i fod heb ei gyhoeddi.
Nifer o golofnau
Nifer o resi
-
- Gosodwch id dalfan wrth osod ID ar eich dalfan gallwch chwistrellu cynnwys i mewn i'r templed yma o dempledi blentyn,
- wrth gyfeirio at yr ID yma gan ddefnyddio elfen <asp:content />.]]>
-
-
- Dewiswch id dalfan o'r rhestr isod. Gallwch ddim ond
- ddewis Id (neu sawl) o feistr y dempled bresennol.]]>
-
Cliciwch ar y llun i weld y maint llawn
Dewis eitem
Gweld Eitem Storfa
- Creu ffolder...
Perthnasu at y gwreiddiol
Cynnwys disgynyddion
Y gymuned fwyaf cyfeillgar
Dolen i dudalen
Agor y ddolen ddogfen mewn ffenestr neu tab newydd
Dolen i gyfrwng
- Dolen i ffeil
Dewis nod cychwyn cynnwys
Dewis cyfrwng
Dewis y math o gyfrwng
@@ -592,7 +560,6 @@
]]>
Enw Diwylliant
- Golygu allwedd yr eitem geiriadur.
Mae'r broses yn cymryd mwy o amser na'r disgwyl, gwiriwch y log Umbraco i weld os mae wedi bod unrhyw wall yn ystod y gweithrediad hwn
Ni ellir ailadeiladu'r mynegai hwn oherwydd nad yw wedi'i aseinio
IIndexPopulator
+
+
+ Cynnwys yn y mynegai
+ Ni ddarganfuwyd unrhyw ganlyniadau
+ Dangos %0% - %1% o %2% canlyniad(au) - Tudalen %3% o %4%
Darparwch eich enw defnyddiwr
@@ -649,25 +621,10 @@
Darparwch enw arall...
Yn generadu enw arall...
Creu eitem
- Creu
Golygu
Enw
- Caniatáu ar y gwraidd
- Dim ond Mathau o Gynnwys gyda hwn wedi ticio all gael eu creu ar lefel wraidd coed Cynnwys a Chyfrwng
- Mathau o nod blentyn caniataol
- Cyfansoddiadau Mathau o Ddogfen
- Creu
- Dileu tab
- Disgrifiad
- Tab newydd
- Tab
- Ciplun bach
- Galluogi gwedd rhestr
- Ffurfweddu'r eitem gynnwysi ddangos rhestr trefnadwy & a chwiladwy o'i phlant, ni fydd y plant yn cael eu dangos yn y goeden
- Gwedd rhestr bresennol
- Y fath o ddata gwedd rhestr gweithredol
Creu gwedd rhestr pwrpasol
Dileu gwedd rhestr pwrpasol
Mae math o gynnwys, math o gyfrwng neu math o aeold gyda'r enw arall yma'n bodoli eisoes
@@ -689,10 +646,6 @@
Taflenni arddull perthnasol
Dangos label
Lled ac uchder
- Holl fathau o briodweddau & data priodwedd
- yn defnyddio'r fath yma o ddata yn cael eu dileu yn barhaol, cadarnhewch eich bod eisiau dileu'r rhain hefyd
- Iawn, dileu
- a holl fathau o briodwedd & data priodwedd sy'n defnyddio'r math o ddata yma
Dewiswch y ffolder i symud
i'r strwythyr goeden isod
wedi symud o dan
@@ -720,25 +673,15 @@
Darparwch yr enw ac yr enw arall ar y math o briodwedd newydd!
Mae yna broblem gyda hawliau darllen/ysgrifennu i ffeil neu ffolder penodol
Gwall yn llwytho sgript Rhan-Wedd (ffeil: %0%)
- Gwall yn llwytho Rheolydd Defnyddiwr '%0%'
- Gwall yn llwytho Rheolydd Pwrpasol (Gwasaneth: %0%, Math: '%1%')
- Gwall yn llwytho sgript MacroEngine (ffeil: %0%)
- "Gwall yn dosbarthu'r ffeil XSLT: %0%
- "Gwall yn darllen y ffeil XSLT: %0%
Darparwch deitl
Dewiswch fath
Rydych ar fîn gwneud y llun yn fwy 'na'r maint gwreiddiol. Ydych chi'n sicr eich bod eisiau parhau?
- Gwall yn y sgript python
- Nid yw'r sgript python wedi'i achub gan ei fod yn cynnwys gwall(au)
Nod gychwynnol wedi'i ddileu, cysylltwch â'ch gweinyddwr
Marciwch gynnwys cyn newid arddull
Dim arddulliau gweithredol ar gael
Symudwch y cyrchwr ar ochr chwith y ddwy gell yr ydych eisiau cyfuno
Ni allwch hollti cell sydd heb ei gyfuno.
Mae gan briodwedd hon gwallau
- Gwall yn y ffynhonnell XSLT
- Nid yw'r XSLTwedi'i achub gan ei fod yn cynnwys gwall(au)
- Mae gwall ffurfwedd gyda'r math o ddata sy'n cael ei ddefnyddio ar gyfer y priodwedd yma, gwiriwch y fath o ddata
Mae methiant anhysbys wedi digwydd
Methiant cydsyniadau optimistaidd, gwrthrych wedi'i addasu
@@ -803,7 +746,6 @@
Eicon
Id
Mewnforio
- Cynnwys is-ffolderi wrth chwilio
Search only this folder Chwilio yn ffolder hwn yn unig
Gwybodaeth
Ymyl mewnol
@@ -825,7 +767,6 @@
Gofynnol
Neges
Symud
- Mwy
Enw
Newydd
Nesaf
@@ -840,7 +781,6 @@
Trefnu wrth
Cyfrinair
Llwybr
- ID Dalfan
Un eiliad os gwelwch yn dda...
Blaenorol
Priodweddau
@@ -902,9 +842,7 @@
Erthyglau
Fideos
Clirio
- Arsefydlu
Avatar am
-
Cyfryngau
Enw Nôd
Darllen fwy
@@ -915,27 +853,13 @@
Pennawd
maes system
Diweddarwyd Diwethaf
+ Newid
+ Gwybodaeth Umbraco
+ Neidio i'r dewislen
+ Neidio i'r cynnwys
- Du
- Gwyrdd
- Melyn
- Oren
Glas
- Llwyd Las
- Llwyd
- Brown
- Glas Golau
- Gwyrddlas
- Gwyrdd Golau
- Leim
- Melyngoch
- Oren Ddwfn
- Coch
- Pinc
- Piws
- Piws Ddwfn
- Dulas
Ychwanegu tab
@@ -960,7 +884,6 @@
Cyffredinol
Golygydd
Toglo caniatáu amrywiadau diwylliant
- Toglo caniatáu segmentiad
Lliw cefndir
@@ -979,7 +902,7 @@
Ffurfwedd gronfa ddata
Trwy glicio ar y botwm nesaf (neu addasu'r umbracoConfigurationStatus yn web.config), rydych chi'n derbyn y drwydded ar gyfer y feddalwedd hon fel y nodir yn y blwch isod. Sylwch fod y dosbarthiad Umbraco hwn yn cynnwys dwy drwydded wahanol, y drwydded MIT ffynhonnell agored ar gyfer y fframwaith a thrwydded radwedd Umbraco sy'n cwmpasu'r UI.
To finish the installation, you'll need to manually edit the
- <strong>/web.config file</strong> a diweddaru'r allwedd AppSetting <strong>UmbracoConfigurationStatus</strong> yn y gwaelod i'r werth <strong>'%0%'</strong>.
+ /web.config file a diweddaru'r allwedd AppSetting UmbracoConfigurationStatus yn y gwaelod i'r werth '%0%'.
Cod dilysu
Rhowch y cod dilysu os gwelwch yn dda
Cod annilys wedi'i nodi
+ Umbraco: Cod Diogelwch
+ Eich cod diogelwch yw: %0%
Dashfwrdd
@@ -1376,36 +1301,16 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
]]>
Bydd hwn yn dileu'r pecyn
- Gollwng i lanlwytho
Cynhwyswch yr holl nodau plentyn
- neu cliciwch yma i ddewis ffeil pecyn
- Lanlwytho pecyn
- Gosod pecyn leol wrth ddewis o'ch peiriant. Dylwch ddim ond osod pecynnau o ffynonellau yr ydych yn adnabod a bod gennych hyder ynddynt
- Lanlwytho pecyn arall
- Canslo a lanlwytho pecyn arall
Trwydded
- Rydw i'n derbyn
- termau defnydd
- Llwybr i'r ffeil
- Llwybr llwyr i'r ffeil (ie: /bin/umbraco.bin)
Wedi'i osod
- Gosod yn lleol
- Gosod pecyn
- Gorffen
Pecynnau wedi'u gosod
Nid oes gennych unrhyw becynnau wedi'u gosod
'Pecynnau' yng nghornel dop, dde eich sgrîn]]>
Nid oes gan y pecyn hwn unrhyw olwg cyfluniad
Nid oes unrhyw becynnau wedi'u creu eto
- Camau Gweithredu Pecyn
- URL y Awdur
Cynnwys y Pecyn
- Ffeiliau y Pecyn
- URL Eicon
- Gosod pecyn
Trwydded
- URL Trwydded
- Priodweddau Pecyn
Chwilio am becynnau
Canlyniadau ar gyfer
Ni allwn ddarganfod unrhyw beth ar gyfer
@@ -1426,7 +1331,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Mae'r pecyn yma yn gydnaws â'r fersiynau canlynol o Umbraco, fel y mae aelodau'r gymued yn adrodd yn ôl. Ni all warantu cydweddoldeb cyflawn ar gyfer fersiynau sydd wedi'u hadrodd o dan 100%
Ffynonellau allanol
Awdur
- Arddangosiad
Dogfennaeth
Meta ddata pecynnau
Enw pecyn
@@ -1435,7 +1339,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Gallwch ddileu hyn yn ddiogel o'r system wrth glicio "dadosod pecyn" isod.]]>
- Dim uwchraddiadau ar gael
Dewisiadau pecyn
Readme pecyn
Ystorfa pecyn
@@ -1448,24 +1351,7 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Rhybudd: bydd unrhyw ddogfennau, cyfrwng ayyb sy'n dibynnu ar yr eitemau yr ydych am ddileu yn torri, a gall arwain at system ansefydlog,
felly dadosodwch gyda gofal. Os oes unrhyw amheuaeth, cysylltwch ag awdur y pecyn.]]>
- Lawrlwytho diweddariad o'r ystorfa
- Uwchraddio pecyn
- Cyfarwyddiadau uwchraddio
- Mae yna uwchraddiad ar gael ar gyfer y pecyn yma. Gallwch lawrlwytho'n uniongyrchol o'r ystorfa pecynnau Umbraco.
Fersiwn pecyn
- Uwchraddio o ferswin
- Hanes ferswin pecyn
- Gweld gwefan pecyn
- Pecyn wedi'i osod eisoes
- Ni all y pecyn yma gael ei osod, mae angen fersiwn Umrbaco o leiaf
- Dadosod...
- Lawrlwytho...
- Mewnforio...
- Gosod...
- Ailgychwyn, arhoswch...
- Wedi cwblhau, bydd eich porwr yn adnewyddu, arhoswch...
- Cliciwch 'Cwblhau' i orffen y gosodiad ac adnewyddu'r dudalen.
- Lanlwytho pecyn...
Wedi gwirio i weithio ar Umbraco Cloud
Cyfarwyddiadau gosod
Wedi'i hyrwyddo
@@ -1483,9 +1369,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Amddiffyniad yn seiliedig grŵp
Os ydych chi am ganiatáu mynediad i bob aelod o grwpiau aelodau penodol
Mae angen i chi greu grŵp aelod cyn y gallwch ddefnyddio dilysiad grŵp
- Amddiffyn ar sail rôl
- Os hoffwch reoli cyrchiad i'r dudalen wrth ddefnyddio dilysu ar sail rôl, gan ddefnyddio grwpiau aelodaeth Umbraco.
- Mae angen i chi greu grŵp aeloadeth cyn i chi allu defnyddio dilysu ar sail rôl
Tudalen Wall
Wedi'i ddefnyddio pan mae defnyddwyr wedi mewngofnodi, ond nid oes ganddynt hawliau
Dewiswch sut i gyfyngu hawliau at y dudalen yma
@@ -1495,10 +1378,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Dewiswch y dudalen sy'n cynnwys y ffurflen mewngofnodi
Dileu Amddiffyniad
Dewiswch y tudalennau sy'n cynnwys ffurflenni mewngofnodi a negeseuon gwall
- Dewiswch y rolau sydd a hawliau i'r dudlaen yma
- Gosodwch yr enw defnyddiwr a chyfrinair ar gyfer y dudalen yma
- Amddiffyniad defnyddiwr unigol
- Os hoffwch osod amddifyniad syml wrth ddefnyddio enw defnyddiwr a chyfrinair sengl
%0%?]]>
%0%]]>
%0%]]>
@@ -1527,7 +1406,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Methodd y dilysiad ar gyfer yr iaith ofynnol '%0%'. Roedd yr iaith wedi cael ei arbed ond nid ei chyhoeddi.
- Cynnwys is-dudalennau heb eu cyhoeddi
Cyhoeddi ar waith - arhoswch...
%0% allan o %1% o dudalennau wedi eu cyhoeddi...
%0% wedi ei gyhoeddi
@@ -1554,12 +1432,10 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Yn sbwriel
Agor mewn Llyfrgell Cyfryngau
Newid Eitem Gyfrwng
- Ailosod tocio cyfrwng
Golygu %0% ar %1%
Gwaredu cread?
Ydych chi'n siŵr eich bod chi am ganslo'r cread?
Rydych chi wedi gwneud newidiadau i'r cynnwys hwn. Ydych chi'n siŵr eich bod chi am eu gwaredu?
- Dileu?
Dileu pob cyfryngau?
Clipfwrdd
Ni chaniateir
@@ -1580,8 +1456,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Ailosod tocio
- Achub tocio
- Ychwanegu tocio newydd
Wedi gwneud
Dadwneud golygion
Diffiniad defnyddiwr
@@ -1595,37 +1469,29 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Rolio yn ôl at
Dewis fersiwn
Gwedd
-
Yn dangos fersiwn %0% i %1% o %2% fersiynau.
Fersiynau
Fersiwn drafft cyfredol
Fersiwn cyhoeddedig cyfredol
+ Wedi creu
+ Fersiwn gyfredol
+ Nid oes unrhyw wahaniaethau rhwng y fersiwn (drafft) gyfredol a'r fersiwn a ddewiswyd
Golygu ffeil sgript
- Gwas
Cynnwys
- Tywyswr
- Datblygwr
Ffurflenni
- Cymorth
- Dewin Ffurfweddu Umbraco
Cyfrwng
Aelodau
- Cylchlythyrau
Pecynnau
Gosodiadau
- Ystadegau
Cyfieithiad
Defnyddwyr
- Dadansoddeg
+ Marchnad
- ewch i
- Pynciau cymorth ar gyfer
- Penodau fideo ar gyfer
Teithiau
Y fideos tiwtorial Umbraco gorau
Ymweld â our.umbraco.com
@@ -1635,21 +1501,18 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Templed diofyn
- Allwedd Geiriadur
Er mwyn mewnforio math o ddogfen, darganfyddwch y ffeil ".udt" ar ecih cyfrifiadur wrth glicio ar y botwn "Pori" a cliciwch "Mewnforio" (byddwch yn cael eich gofyn i gadarnhau ar y sgrîn nesaf)
Teitl Tab Newydd
Math o nod
Math
Taflen arddull
Sgript
- Priodwedd taflen arddull
Tab
Teitl Tab
Tabiau
Math o Gynnwys Meistr wedi'i alluogi
Mae'r Math o Gynnwys yma yn defnyddio
Dim priodweddau wedi'u diffinio ar y tab yma. Cliciwch ar y ddolen "ychwanegu priodwedd newydd" ar y topi greu priodwedd newydd.
- Math o Ddogfen Feistr
Creu templedi cydweddol
Ychwanegu eicon
@@ -1670,7 +1533,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Diffyg hawliau defnyddiwr, ni ellir cwblhau'r gweithred
Wedi canslo
Gweithred wedi'i ganslo gan ymestyniad 3-ydd parti
- Cyhoeddi wedi'i ganslo gan ymestyniad 3-ydd parti
Math o briodwedd yn bodoli eisoes
Math o briodwedd wedi'i greu
Math o ddata: %1%]]>
@@ -1684,23 +1546,19 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Taflen arddull wedi'i achub heb unrhyw wallau
Math o ddata wedi'i achub
Eitem geiriadur wedi'i achub
- Cyhoeddi wedi methu gan nad yw'r dudalen rhiant wedi'i gyhoeddi
Cynnwys wedi'i gyhoeddi
ac yn weladwy ar y wefan
%0% dogfennau wedi'i gyhoeddi ac yn gweledig ar y wefan
%0% gyhoeddi ac yn gweledig ar y wefan
%0% dogfennau wedi'i gyhoeddi am yr ieithoedd %1% ac yn gweledig ar y wefan
- ac yn weladwy ar y wefan tan %0% at %1%
Cynnwys wedi'i achub
Cofiwch gyhoeddi er mwyn i'r newidiadau fod yn weladwy
Mae amserlen ar gyfer cyhoeddi wedi'i diweddaru
%0% wedi arbed
- Bydd newidiadau yn cael ei gymerdwyo ar %0% at %1%
Wedi'i anfon am gymeradwyo
Newidiadau wedi'u hanfon am gymeradwyo
%0% newidiadau wedi'u hanfon am gymeradwyo
Cyfrwng wedi'i achub
- Grŵp aeloadeth wedi'i achub
Cyfrwng wedi'i achub heb unrhyw wallau
Aelod wedi'i achub
Priodwedd taflen arddull wedi'i achub
@@ -1720,19 +1578,10 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Math o Gyfrwng wedi'i achub
Math o Aelod wedi'i achub
Grŵp Aelod wedi'i achub
- Sgript Python heb ei achub
- Ni ellir achub y sgript Python oherwydd gwall
- Sgript Python wedi'i achub
- Dim gwallau yn y sgript Python
Templed heb ei achub
Sicrhewch nad oes gennych 2 dempled gyda'r un enw arall
Templed wedi'i achub
Templed wedi'i achub heb unrhyw wallau!
- XSLT heb ei achub
- XSLT yn cynnwys gwall
- Ni ellir achub y ffeil XSLT, gwiriwch hawliau ffeil
- XSLT wedi'i achub
- Dim gwallau yn yr XSLT
Cynnwys wedi'i ddadgyhoeddi
amrywiad cynnwys %0% wedi'i dadgyhoeddi
Roedd yr iaith orfodol '%0%' wedi'i dadgyhoeddi. Mae'r holl ieithoedd ar gyfer yr eitem gynnwys hon bellach wedi'i dadgyhoeddi.
@@ -1741,28 +1590,17 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Rhan-wedd heb ei achub
Bu gwall yn ystod achub y ffeil.
Hawliau wedi'u hachub ar gyfer
- Gwedd sgript wedi'i achub
- Gwedd sgript wedi'i achub heb unrhyw wallau!
- Gwedd sgript heb ei achub
- Bu gwall yn ystod achub y ffeil.
- Bu gwall yn ystod achub y ffeil.
Wedi dileu %0% o rwpiau defnwyddwr
%0% wedi'i ddileu
%0% o ddefnyddwyr wedi'u galluogi
- Bu gwall yn ystod galluogi'r defnyddwyr
Wedi analluogi %0% o ddefnyddwyr
- Bu gwall yn ystod analluogi'r defnyddwyr
%0% yn awr wedi galluogi
- Bu gwall yn ystod galluogi'r defnyddiwr
%0% yn awr wedi analluogi
- Bu gwall yn ystod analluogi'r defnyddiwr
Grwpiau defnyddiwr wedi'u gosod
Wedi dileu %0% o rwpiau defnyddwyr
%0% wedi dileu
Wedi datgloi %0% o ddefnyddwyr
- Bu gwall yn ystod datgloi'r defnyddwyr
%0% yn awr wedi datgloi
- Bu gwall yn ystod datgloi'r defnyddiwr
Allforwyd yr aelod at ffeil
Bu gwall yn ystod allforio'r aelod
Defnyddiwr %0% wedi'i ddileu
@@ -1793,7 +1631,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Methu â chopïo gwybodaeth eich system i'r clipfwrdd
- Yn defnyddio cystrawen CSS e.e: h1, .coch, .glas
Ychwanegu ardull
Golygu ardull
Ardull golygydd testun cyfoethog
@@ -1835,7 +1672,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
mae'n wych ar gyfer ail-ddefnyddio côd neu ar gyfer gwahanu templedi cymhleth i mewn i ffeiliau gwahanol.
Templed Meistr
- Dim templed meistr
Dim meistr
Datganu templed blentyn
@@ -1868,9 +1704,7 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
]]>
Adeiladwr ymholiad
- Adeiladu ymholiad
o eitemau wedi dychwelyd, mewn
- Copi i'r clipfwrdd
Rydw i eisiau
holl gynnwys
cynnwys o'r fath "%0%"
@@ -1900,15 +1734,11 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
esgynnol
disgynnol
Templed
- Nid oes modd golygu cynnwys wrth ddefnyddio modd amser rhedeg <code>Production</code>.
+ Nid oes modd golygu cynnwys wrth ddefnyddio modd amser rhedeg Production.
- Golygydd Testun Gyfoethog
Llun
Macro
- Mewnosod
- Pennawd
- Dyfyniad
Dewis math o gynnwys
Dewis cynllun
Ychwanegu rhes
@@ -1920,7 +1750,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Cliciwch i fewnblannu
Cliciwch i fewnosod llun
Cliciwch i fewnosod macro
- Capsiwn llun...
Ysgrifennwch yma...
Cynlluniau Grid
Cynlluniau yw'r holl ardal weithio gyfan ar gyfer y golygydd grid, fel arfer rydych ddim ond angen un neu ddau gynllun gwahanol
@@ -1939,7 +1768,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Ffurfweddu pa osodiadau gall olygyddion eu newid
Ardduliau
Ffurfweddu pa arddulliau gall olygyddion eu newid
- Bydd gosodiadau dim ond yn newid os mae'r ffurfwedd json yn ddilys
Caniatáu pob golygydd
Caniatáu holl ffurfweddi rhes
Uchafswm o eitemau
@@ -1951,15 +1779,12 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Rhybudd
Rydych chi'n dileu'r ffurfwedd rhes
Bydd dileu enw ffurfwedd rhes yn arwain at golli data ar gyfer unrhyw gynnwys cynfodol sy'n seiliedig ar ffurfwedd hwn.
- <p>Bydd addasu enw cyfluniad rhes yn arwain at golli data ar gyfer unrhyw gynnwys presennol sy'n seiliedig ar y ffurfweddiad hwn.</p> <p><strong>Ni fydd addasu'r label yn unig yn arwain at golli data.</strong></p>
+ Bydd addasu enw cyfluniad rhes yn arwain at golli data ar gyfer unrhyw gynnwys presennol sy'n seiliedig ar y ffurfweddiad hwn.
Ni fydd addasu'r label yn unig yn arwain at golli data.
Rydych chi'n dileu'r gosodiad
Bydd addasu cynllun yn arwain at golli data ar gyfer unrhyw gynnwys presennol sy'n seiliedig ar y ffurfweddiad hwn.
Cyfansoddiadau
- Nid ydych wedi ychwanegu unrhyw dabiau
- Ychwanegu tab newydd
- Ychwanegu tab arall
Grŵp
Ni allwch symud y grŵp %0% i'r tab hwn oherwydd bydd y grŵp yn cael yr un alias â thab: "%1%". Ail-enwi'r grŵp i barhau.
Nid ydych wedi ychwanegu unrhyw grwpiau
@@ -1973,7 +1798,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Dewiswch pa olygoddion templedi sy'n cael defnyddio cynnwys o'r fath yma
Caniatáu fel gwraidd
Caniatáu golygyddion i greu cynnwys o'r fath yma yng ngwraidd y goeden gynnwys
- Iawn - caniatáu cynnwys o'r fath yma yn y gwraidd
Mathau o nod blentyn caniateir
Caniatáu cynnwys o'r mathau benodol i gael eu creu o dan cynnwys o'r fath yma
Dewis nod blentyn
@@ -2002,7 +1826,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
a phob dogfen sy'n defnyddio'r fath yma
a phob eitem gyfrwng sy'n defnyddio'r fath yma
a phob aelod sy'n defnyddio'r fath yma
- sy'n defnyddio'r golygydd yma fydd yn cael eu diweddaru gyda'r gosodiadau newydd
Aeloed yn gallu golygu
Caniatáu i'r gwerth briodwedd yma gael ei olygu gan yr aelod ar eu tudalen broffil
Yn ddata sensitif
@@ -2030,9 +1853,9 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Rydych wedi gwneud newidiadau i'r eiddo hwn. Ydych chi'n siŵr eich bod chi am eu taflu?
Ymddangosiad
Label uwchben (lled-llawn)
- Ydych chi'n siŵr eich bod chi am ddileu'r tab <strong>%0%</strong>?
- Ydych chi'n siŵr eich bod chi am ddileu'r tab grŵp <strong>%0%</strong>?
- Ydych chi'n siŵr eich bod chi am ddileu'r eiddo <strong>%0%</strong>?
+ Ydych chi'n siŵr eich bod chi am ddileu'r tab %0%?
+ Ydych chi'n siŵr eich bod chi am ddileu'r tab grŵp %0%?
+ Ydych chi'n siŵr eich bod chi am ddileu'r eiddo %0%?
Bydd hyn hefyd yn dileu'r holl eitemau o dan y tab hwn.
Bydd hyn hefyd yn dileu'r holl eitemau o dan y grŵp hwn.
Ychwanegu tab
@@ -2047,7 +1870,7 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Cadwch y fersiwn diweddaraf y dydd am ddyddiau
Atal glanhau
Galluogi glanhau
- <strong>NODYN!</strong> Mae glanhau fersiynau cynnwys hanesyddol wedi'u hanalluogi'n fyd-eang. Ni fydd y gosodiadau hyn yn dod i rym cyn iddo gael ei alluogi.
+ NODYN! Mae glanhau fersiynau cynnwys hanesyddol wedi'u hanalluogi'n fyd-eang. Ni fydd y gosodiadau hyn yn dod i rym cyn iddo gael ei alluogi.
Mae newid math o ddata gyda gwerthoedd storio wedi'i analluogi. I ganiatáu hyn gallwch newid y gosodiad Umbraco:CMS:DataTypes:CanBeChanged yn appssettings.json.
@@ -2063,9 +1886,9 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Iaith cwympo yn ôl
dim
Cod ISO
- <strong>%0%</strong> yn cael ei rannu ar draws ieithoedd a segmentau.
- <strong>%0%</strong> yn cael ei rannu ar draws pob iaith.
- <strong>%0%</strong> yn cael ei rannu ar draws pob segment.
+ %0% yn cael ei rannu ar draws ieithoedd a segmentau.
+ %0% yn cael ei rannu ar draws pob iaith.
+ %0% yn cael ei rannu ar draws pob segment.
Wedi'i rannu: Ieithoedd
Wedi'i rannu: Segments
@@ -2085,8 +1908,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Methwyd generadu modelau, gweler yr eithriadau yn y log Umbraco
- Ychwanegu maes rolio yn ôl
- Maes rolio yn ôl
Ychwanegu gwerth diofyn
Gwerth diofyn
Maes rolio yn ôl
@@ -2095,26 +1916,21 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Amgodiad
Dewis maes
Trawsnewid torriadau llinellau
- Iawn, trawsnewid torriadau llinellau
Cyfnewid torriadau llinellau gyda tag html 'br'
Meysydd bersonol
Dyddiad yn unig
- Fformat ac amgodiad
Fformatio ar ffurf dyddiad
- Fformatio'r gwerth ar ffurf dyddiad, neu dyddiad gyda amser, yn ôl y diwylliant gweithredol
Amgodi HTML
Bydd yn cyfnewid nodau arbennig gyda'u nodau HTML cyfatebol.
Bydd yn cael ei fewnosod ar ôl y gwerth maes
Bydd yn cael ei fewnosod cyn y gwerth maes
Llythrennau bach
- Newid allbwn
Dim
Sampl allbwn
Mewnosod ar ôl maes
Mewnosod cyn maes
Ailadroddus
Iawn, gwnewch yn ailadroddus
- Gwahanwr
Meysydd Safonol
Llythrennau bras
Amgodi URL
@@ -2124,17 +1940,7 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Dyddiad ac amser
- Tasgau wedi'u neilltuo i chi
-
- wedi'u neilltuo i chi. Er mwyn gweld gwedd fanwl gan gynnwys sylwadau, cliciwch ar "Manylion" neu enw'r dudalen.
- Gallwch hefyd lawrlwytho'r dudalen ar ffurf XML yn uniongyrchol gan glicio'r ddolen "Lawrlwytho Xml".
- Er mwyn cau tasg cyfieithu, ewch at y wedd fanylion a cliciwch ar y botwm "Cau".
- ]]>
-
- cau tasg
Manylion cyfieithiad
- Lawrlwytho pob tasg cyfieithu ar ffurf XML
- Lawrlwytho XML
Lawrlwytho XML DTD
Meysydd
Cynnwys is-dudalennau
@@ -2155,20 +1961,9 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Hwyl fawr oddi wrth y robot Umbraco
]]>
- [%0%] Tasg cyfieithu ar gyfer %1%
Dim defnyddwyr cyfieithu wedi'u darganfod. Creuwch ddefnyddiwr cyfieithu cyn i chi gychwyn anfon cynnwys am gyfieithiadau
- Tasgau wedi'u creu gennych chi
-
- wedi'u creu gennych chi. Er mwyn gweld gwedd fanwl sy'n cynnwys sylwadau,
- cliciwch ar "Manylion" neu enw'r dudalen. Gallwch hefyd lawrlwytho'r dudalen ar ffurf XML yn uniongyrchol gan glicio ar y ddolen "Lawrlwytho Xml".
- Er mwyn cau tasgau cyfieithu, ewch at y wedd fanylion a cliciwch y botwm "Cau".
- ]]>
-
Mae'r dudalen '%0%' wedi cael ei anfon am gyfieithiad
- Dewiswch yr iaith y dylai'r cynnwys gael ei gyfieithu i
Anfon y dudalen '%0%' am gyfieithiad
- Wedi'i neilltuo gan
- Tasg wedi'i hagor
Cyfanswm o eiriau
Cyfieithu i
Cyfieithiad wedi'i gwblhau.
@@ -2204,7 +1999,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Pecynnau
Rhan-weddi
Ffeiliau Rhan-wedd Macro
- Ffeiliau Python
Gosod o ystorfa
Gosod Runway
Modylau Runway
@@ -2212,8 +2006,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Sgriptiau
Taflenni arddull
Templedi
- Ffeiliau XSLT
- Dadansoddeg
Gwyliwr Log
Defnyddwyr
Gosodiadau
@@ -2303,15 +2095,12 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang
Rheoli defnyddwyr
Enw
Hawliau defnyddiwr
- Hawliau grwpiau defnyddiwr
Grŵp defnyddiwr
- Grwpiau defnyddiwr
wedi'i wahodd
Mae gwahoddiad wedi cael ei anfon at y defnyddiwr newydd gyda manylion ar sut i fewngofnodi i Umbraco.
Helo a chroeso i Umbraco! Mewn 1 munud yn unig, byddech chi'n barod i fynd, rydym dim ond angen gosod cyfrinair.
Croeso i Umbraco! Yn anffodus, mae eich gwahoddiad wedi terfynu. Cysylltwch â'ch gweinyddwr a gofynnwch iddynt ail-anfon.
Ysgrifennydd
- Cyfieithydd
Newid
Eich proffil
Eich hanes diweddar
@@ -2414,7 +2203,6 @@ Er mwyn gweinyddu eich gwefan, agorwch swyddfa gefn Umbraco a dechreuwch ychwang