From ab1554368a7d052c511bb9cd98497ceb0a3593f1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 3 May 2024 12:01:00 -0600 Subject: [PATCH] Fixes #446, #439, #432 --- .../appsettings-schema.json | 1621 +++++++---------- src/Articulate/Articulate.csproj | 2 +- .../Components/ArticulateComposer.cs | 3 + .../ArticulateDynamicRouteAttribute.cs | 13 + .../Controllers/ArticulateRssController.cs | 1 + .../Controllers/ArticulateSearchController.cs | 1 + .../Controllers/ArticulateTagsController.cs | 8 +- .../Controllers/MarkdownEditorController.cs | 1 + .../Controllers/MetaWeblogController.cs | 1 + .../Controllers/OpenSearchController.cs | 1 + src/Articulate/Controllers/RsdController.cs | 1 + .../Controllers/WlwManifestController.cs | 1 + .../ArticulateDynamicRouteSelectorPolicy.cs | 74 + .../ArticulateRouteValueTransformer.cs | 12 +- src/Articulate/Routing/ArticulateRouter.cs | 22 +- 15 files changed, 732 insertions(+), 1030 deletions(-) create mode 100644 src/Articulate/Controllers/ArticulateDynamicRouteAttribute.cs create mode 100644 src/Articulate/Routing/ArticulateDynamicRouteSelectorPolicy.cs diff --git a/src/Articulate.Tests.Website/appsettings-schema.json b/src/Articulate.Tests.Website/appsettings-schema.json index 0a737c97..c31b18ce 100644 --- a/src/Articulate.Tests.Website/appsettings-schema.json +++ b/src/Articulate.Tests.Website/appsettings-schema.json @@ -2,6 +2,7 @@ "$schema": "http://json-schema.org/draft-04/schema#", "definitions": { "webOptimizer": { + "title": "web optimizer", "type": "object", "description": "Settings for WebOptimizer.Core", "properties": { @@ -17,6 +18,7 @@ } }, "cdn": { + "title": "CDN", "type": "object", "description": "Definitions for WebEssentials.AspNetCore.CdnTagHelpers", "properties": { @@ -76,12 +78,12 @@ "ApiKey": { "description": "An elmah.io API key with the Messages | Write permission.", "type": "string", - "pattern": "^[0-9a-f]{32}$" + "pattern": "^([0-9a-f]{32})|(#\\{.*\\}#?)$" }, "LogId": { "description": "The Id of the elmah.io log to store messages in.", "type": "string", - "pattern": "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$" + "pattern": "^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})|(#\\{.*\\}#?)$" }, "Application": { "description": "An application name to put on all error messages.", @@ -101,7 +103,7 @@ "HeartbeatId": { "description": "The Id of the elmah.io heartbeat to notify.", "type": "string", - "pattern": "^[0-9a-f]{32}$" + "pattern": "^([0-9a-f]{32})|(#\\{.*\\}#?)$" } }, "required": [ @@ -122,6 +124,7 @@ ] }, "certificate": { + "title": "certificate", "description": "Certificate configuration.", "type": "object", "properties": { @@ -188,13 +191,16 @@ "default": "NoCertificate" }, "kestrel": { + "title": "kestrel", "type": "object", "description": "ASP.NET Core Kestrel server configuration.", "properties": { "Endpoints": { + "title": "endpoints", "description": "Endpoints that Kestrel listens to for network requests. Each endpoint has a name specified by its JSON property name.", "type": "object", "additionalProperties": { + "title": "endpoint options", "description": "Kestrel endpoint configuration.", "type": "object", "properties": { @@ -216,9 +222,11 @@ "$ref": "#/definitions/clientCertificateMode" }, "Sni": { + "title": "SNI", "description": "Server Name Indication (SNI) configuration. This enables the mapping of client requested host names to certificates and other TLS settings. Wildcard names prefixed with '*.', as well as a top level '*' are supported. Available in .NET 5 and later.", "type": "object", "additionalProperties": { + "title": "SNI options", "description": "Endpoint SNI configuration.", "type": "object", "properties": { @@ -244,6 +252,7 @@ } }, "EndpointDefaults": { + "title": "endpoint defaults", "description": "Default configuration applied to all endpoints. Named endpoint specific configuration overrides defaults.", "type": "object", "properties": { @@ -259,6 +268,7 @@ } }, "Certificates": { + "title": "certificates", "description": "Certificates that Kestrel uses with HTTPS endpoints. Each certificate has a name specified by its JSON property name. The 'Default' certificate is used by HTTPS endpoints that haven't specified a certificate.", "type": "object", "additionalProperties": { @@ -281,6 +291,7 @@ ] }, "logLevel": { + "title": "logging level options", "description": "Log level configurations used when creating logs. Only logs that exceeds its matching log level will be enabled. Each log level configuration has a category specified by its JSON property name. For more information about configuring log levels, see https://docs.microsoft.com/aspnet/core/fundamentals/logging/#configure-logging.", "type": "object", "additionalProperties": { @@ -288,6 +299,7 @@ } }, "logging": { + "title": "logging options", "type": "object", "description": "Configuration for Microsoft.Extensions.Logging.", "properties": { @@ -305,6 +317,7 @@ "default": "simple" }, "FormatterOptions": { + "title": "formatter options", "description": "Log message formatter options. Additional properties are available on the options depending on the configured formatter. The formatter is specified by FormatterName.", "type": "object", "properties": { @@ -367,6 +380,7 @@ } }, "additionalProperties": { + "title": "provider logging settings", "type": "object", "description": "Logging configuration for a provider. The provider name must match the configuration's JSON property property name.", "properties": { @@ -381,6 +395,7 @@ "type": "string" }, "connectionStrings": { + "title": "connection string options", "description": "Connection string configuration. Get connection strings with the IConfiguration.GetConnectionString(string) extension method.", "type": "object", "additionalProperties": { @@ -389,6 +404,7 @@ } }, "NLog": { + "title": "NLog options", "type": "object", "description": "NLog configuration", "default": {}, @@ -463,6 +479,7 @@ "description": "Load NLog extension packages for additional targets and layouts", "default": [], "items": { + "title": "extension", "type": "object", "description": "", "default": {}, @@ -485,6 +502,7 @@ } }, "variables": { + "title": "variables", "type": "object", "description": "Key-value pair of variables", "propertyNames": { @@ -501,6 +519,7 @@ } }, "targetDefaultWrapper": { + "title": "default wrapper", "type": "object", "description": "Wrap all defined targets with this custom target wrapper.", "default": {}, @@ -515,6 +534,7 @@ } }, "targets": { + "title": "targets", "type": "object", "description": "", "default": {}, @@ -536,6 +556,7 @@ } }, { + "title": "rules", "type": "object", "propertyNames": { "pattern": "^[0-9]+$" @@ -551,6 +572,7 @@ } }, "NLogRulesItem": { + "title": "NLog rule item", "type": "object", "description": "Redirect LogEvents from matching Logger objects to specified targets", "default": {}, @@ -667,6 +689,7 @@ "description": "", "default": [], "items": { + "title": "filter", "type": "object", "description": "", "default": {}, @@ -694,6 +717,7 @@ } }, { + "title": "filter", "type": "object", "description": "", "default": {} @@ -714,908 +738,300 @@ } } }, - "umbraco": { - "description": "Configuration of Open Source .NET CMS - Umbraco", - "properties": { - "CMS": { - "type": "object", - "properties": { - "ActiveDirectory": { - "$ref": "#/definitions/umbracoActiveDirectory" - }, - "Content": { - "$ref": "#/definitions/umbracoContent" - }, - "Debug": { - "$ref": "#/definitions/umbracoDebug" - }, - "Examine": { - "properties": { - "LuceneDirectoryFactory": { - "description": "Lucene directory factory type", - "type": "string" - } - } - }, - "ExceptionFilter": { - "properties": { - "Disabled": { - "description": "Indicating whether the exception filter is disabled", - "type": "boolean", - "default": false - } - } - }, - "Global": { - "$ref": "#/definitions/umbracoGlobal" - }, - "HealthChecks": { - "$ref": "#/definitions/umbracoHealthChecks" - }, - "Hosting": { - "$ref": "#/definitions/umbracoHosting" - }, - "Imaging": { - "$ref": "#/definitions/umbracoImaging" - }, - "KeepAlive": { - "$ref": "#/definitions/umbracoKeepAlive" - }, - "Logging": { - "properties": { - "MaxLogAge": { - "description": "Maximum age of a log file - https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings", - "type": "string", - "default": "1.00:00:00" - } - } - }, - "ModelsBuilder": { - "$ref": "#/definitions/umbracoModelsBuilder" - }, - "NuCache": { - "properties": { - "BTreeBlockSize": { - "type": "integer" - } - } - }, - "Plugins": { - "properties": { - "BrowsableFileExtensions": { - "description": "Allowed file extensions (including the period .) that should be accessible from the browser", - "type": [ - "string" - ] - } - } - }, - "RequestHandler": { - "$ref": "#/definitions/umbracoRequestHandler" - }, - "RichTextEditor": { - "$ref": "#/definitions/umbracoRichTextEditor" - }, - "Runtime": { - "properties": { - "MaxQueryStringLength": { - "description": "Value for the maximum query string length", - "type": "integer" - }, - "MaxRequestLength": { - "description": "Value for the maximum request length", - "type": "integer" - } - } - }, - "RuntimeMinification": { - "$ref": "#/definitions/umbracoRuntimeMinification" - }, - "Security": { - "$ref": "#/definitions/umbracoSecurity" - }, - "Tours": { - "properties": { - "EnableTours": { - "description": "Indicating whether back-office tours are enabled", - "type": "boolean", - "default": true - } - } - }, - "TypeFinder": { - "properties": { - "AssembliesAcceptingLoadExceptions": { - "description": "A CSV string of assemblies that accept load exceptions during type finder operations", - "type": "string" - } - } - }, - "WebRouting": { - "$ref": "#/definitions/umbracoWebRouting" - }, - "Unattended": { - "$ref": "#/definitions/umbracoUnattended" - } - } - } - }, - "required": [ - "CMS" - ] - }, - "umbracoActiveDirectory": { - "description": "Configuration of Active Directory for Umbraco CMS", - "properties": { - "Domain": { - "type": "string", - "description": "Active Directory Domain" - } - } - }, - "umbracoContent": { - "properties": { - "AllowedUploadFiles": { - "description": "Collection of file extensions without . that are allowed for upload", - "type": [ - "string" - ] - }, - "DisallowedUploadFiles": { - "description": "Collection of file extensions without . that are disallowed for upload", - "type": [ - "string" - ] - }, - "Error404Collection": { - "type": "array", - "items": { - "$ref": "#/definitions/umbracoContentErrorPage" - } - }, - "Imaging": { - "properties": { - "AutoFillImageProperties": { - "description": "Imaging autofill following media file upload fields", - "properties": { - "Alias": { - "default": "umbracoFile" - }, - "ExtensionFieldAlias": { - "default": "umbracoExtension" - }, - "HeightFieldAlias": { - "default": "umbracoHeight" - }, - "LengthFieldAlias": { - "default": "umbracoBytes" - }, - "WidthFieldAlias": { - "default": "umbracoWidth" - } - } - }, - "ImageFileTypes": { - "description": "Collection of accepted image file extensions", - "type": [ - "string" - ] - } - } - }, - "LoginBackgroundImage": { - "description": "Path to the login screen background image", - "default": "assets/img/login.jpg", - "type": "string" - }, - "LoginLogoImage": { - "description": "Path to the login screen logo image", - "default": "assets/img/application/umbraco_logo_white.svg", - "type": "string" - }, - "MacroErrors": { - "description": "Macro error behaviour", - "enum": [ - "Inline", - "Silent", - "Throw", - "Content" - ] - }, - "Notifications": { - "properties": { - "Email": { - "description": "Email address used for notifications", - "type": "string" - }, - "DisableHtmlEmail": { - "description": "Whether HTML email notifications should be disabled", - "type": "boolean", - "default": false - } - } - }, - "PreviewBadge": { - "description": "Preview badge mark-up", - "type": "string" - }, - "ResolveUrlsFromTextString": { - "description": "URLs should be resolved from text strings", - "type": "boolean", - "default": false - }, - "ShowDeprecatedPropertyEditors": { - "description": "Deprecated property editors should be shown", - "type": "boolean", - "default": false - } - } - }, - "umbracoContentErrorPage": { - "properties": { - "ContentId": { - "description": "An int of the content", - "type": "integer" - }, - "ContentKey": { - "description": "A guid of the content", - "type": "string" - }, - "ContentXPath": { - "description": "An XPath query for the content", - "type": "string" - }, - "Culture": { - "description": "Content culture", - "type": "string" - } - } - }, - "umbracoDebug": { - "properties": { - "LogIncompletedScopes": { - "description": "Indicating whether incompleted scopes should be logged", - "type": "boolean", - "default": false - }, - "DumpOnTimeoutThreadAbort": { - "description": "Indicating whether memory dumps on thread abort should be taken", - "type": "boolean", - "default": false - } - } - }, - "umbracoGlobal": { - "properties": { - "ReservedUrls": { - "description": "CSV string of reserved URLs (must end with a comma)", - "type": "string", - "default": "~/config/splashes/noNodes.aspx,~/.well-known," - }, - "ReservedPaths": { - "description": "CSV string of reserved paths (must end with a comma)", - "type": "string", - "default": "~/app_plugins/,~/install/,~/mini-profiler-resources/,~/umbraco/," - }, - "TimeOut": { - "description": "Duration of timeout https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings", - "type": "string", - "default": "00:20:00" - }, - "DefaultUILanguage": { - "description": "Default UI language of Umbraco backoffice", - "type": "string", - "default": "en-US" - }, - "HideTopLevelNodeFromPath": { - "description": "Indicating whether to hide the top level node from the path", - "type": "boolean", - "default": false - }, - "UseHttps": { - "description": "Indicating whether HTTPS should be used", - "type": "boolean", - "default": false - }, - "VersionCheckPeriod": { - "description": "Check for new version. Period in days", - "type": "integer", - "default": 7 - }, - "UmbracoPath": { - "description": "Umbraco back-office path", - "type": "string", - "default": "~/umbraco" - }, - "IconsPath": { - "description": "Path to Umbraco Icons for backoffice", - "type": "string", - "default": "~/umbraco/assets/icons" - }, - "UmbracoCssPath": { - "description": "Path to store CSS files used for website built with Umbraco", - "type": "string", - "default": "~/css" - }, - "UmbracoMediaPath": { - "description": "Path to store media files", - "type": "string", - "default": "~/media" - }, - "InstallMissingDatabase": { - "description": "Indicating whether to install the database when it is missing", - "type": "boolean", - "default": false - }, - "DisableElectionForSingleServer": { - "description": "Indicating whether to disable the election for a single server", - "type": "boolean", - "default": false - }, - "NoNodesViewPath": { - "description": "Path to view when the website built with Umbraco has no content nodes", - "type": "string", - "default": "~/umbraco/UmbracoWebsite/NoNodes.cshtml" - }, - "DatabaseServerRegistrar": { - "properties": { - "WaitTimeBetweenCalls": { - "description": "The amount of time to wait between calls to the database on the background thread https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings", - "type": "string", - "default": "00:01:00" - }, - "StaleServerTimeout": { - "description": "The time span to wait before considering a server stale, after it has last been accessed https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings", - "type": "string", - "default": "00:02:00" - } - } - }, - "DatabaseServerMessenger": { - "properties": { - "MaxProcessingInstructionCount": { - "description": "The maximum number of instructions that can be processed at startup; otherwise the server cold-boots (rebuilds its caches)", - "type": "integer", - "default": 1000 - }, - "TimeToRetainInstructions": { - "description": "The time to keep instructions in the database. Records older than this number will be pruned https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings", - "type": "string", - "default": "2.00:00:00" - }, - "TimeBetweenSyncOperations": { - "description": "The time to wait between each sync operations https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings", - "type": "string", - "default": "00:00:05" - }, - "TimeBetweenPruneOperations": { - "description": "The time to wait between each prune operations https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings", - "type": "string", - "default": "00:01:00" - } - } - }, - "Smtp": { - "properties": { - "From": { - "description": "Email address to use for messages", - "type": "string" - }, - "Host": { - "description": "SMTP Server hostname", - "type": "string" - }, - "Port": { - "description": "SMTP Server Port Number", - "type": "integer" - }, - "SecureSocketOptions": { - "description": "Secure socket options for SMTP server", - "enum": [ - "None", - "Auto", - "SslOnConnect", - "StartTls", - "StartTlsWhenAvailable" - ], - "default": "Auto" - }, - "PickupDirectoryLocation": { - "description": "SMTP pick-up directory path", - "type": "string" - }, - "DeliveryMethod": { - "description": "SMTP delivery method", - "enum": [ - "Network", - "SpecifiedPickupDirectory", - "PickupDirectoryFromIis" - ], - "default": "Network" - }, - "Username": { - "description": "SMTP server username", - "type": "string" - }, - "Password": { - "description": "SMTP server password", - "type": "string" - } - } - } - } - }, - "umbracoHealthChecks": { - "properties": { - "DisabledChecks": { - "type": "array", - "items": { - "$ref": "#/definitions/umbracoDisabledHealthChecks" - } - }, - "Notification": { - "properties": { - "Enabled": { - "description": "Indicating whether health check notifications are enabled", - "type": "boolean", - "default": false - }, - "FirstRunTime": { - "description": "The first run time of a healthcheck notification in crontab format", - "type": "string" - }, - "Period": { - "description": "The period of the healthcheck notifications are run https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings", - "type": "string", - "default": "1.00:00:00" - }, - "NotificationMethods": { - "description": "A collection of health check notification methods that are set by their alias such as 'email'", - "type": "object", - "additionalProperties": { - "type": "object", - "properties": { - "Enabled": { - "description": "Indicating whether the health check notification method is enabled", - "type": "boolean" - }, - "Verbosity": { - "description": "The health check notifications reporting verbosity", - "enum": [ - "Summary", - "Detailed" - ], - "default": "Summary" - }, - "FailureOnly": { - "description": "Indicating whether the health check notifications should occur on failures only", - "type": "boolean", - "default": false - }, - "Settings": { - "description": "An object of Health Check Notification provider specific settings. For the email notification it uses a setting 'RecipientEmail'", - "type": "object" - } - } - } - }, - "DisabledChecks": { - "type": "array", - "items": { - "$ref": "#/definitions/umbracoDisabledHealthChecks" - } - } - } - } - } - }, - "umbracoDisabledHealthChecks": { - "properties": { - "Id": { - "description": "Guid of healthcheck to disable", - "type": "string" - } - } - }, - "umbracoHosting": { - "properties": { - "ApplicationVirtualPath": { - "type": "string" - }, - "Debug": { - "description": "Indicating whether umbraco is running in [debug mode]", - "type": "boolean", - "default": false - }, - "LocalTempStorageLocation": { - "description": "The location of temporary files", - "default": "Default", - "enum": [ - "Default", - "EnvironmentTemp" - ] - } - } - }, - "umbracoImaging": { - "properties": { - "Cache": { - "properties": { - "BrowserMaxAge": { - "description": "Browser image cache maximum age https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings", - "type": "string", - "default": "7.00:00:00" - }, - "CacheMaxAge": { - "description": "Image cache maximum age https://docs.microsoft.com/en-us/dotnet/standard/base-types/standard-timespan-format-strings", - "type": "string", - "default": "365.00:00:00" - }, - "CachedNameLength": { - "description": "Length of the cached name", - "type": "integer", - "default": 8 - }, - "CacheFolder": { - "description": "Location of media cache folder", - "type": "string", - "default": "..\\umbraco\\mediacache" - } - } - }, - "Resize": { - "properties": { - "MaxWidth": { - "description": "Value for the maximum resize width", - "type": "integer", - "default": 5000 - }, - "MaxHeight": { - "description": "Value for the maximum resize height", - "type": "integer", - "default": 5000 - } - } - } - } - }, - "umbracoKeepAlive": { - "properties": { - "DisableKeepAliveTask": { - "description": "Indicating whether the keep alive task is disabled", - "type": "boolean", - "default": false - }, - "KeepAlivePingUrl": { - "description": "Keep alive ping URL. {umbracoApplicationUrl} is replaced", - "type": "string", - "default": "{umbracoApplicationUrl}/api/keepalive/ping" - } - } - }, - "umbracoRichTextEditor": { - "properties": { - "Commands": { - "description": "Commands to add to the TinyMCE Richtext editor", - "type": "array", - "items": { - "$ref": "#/definitions/umbracoRichTextEditorCommands" - } - }, - "Plugins": { - "description": "An array of TinyMCE Plugins to load such as 'paste', 'table'", - "type": [ - "string" - ] - }, - "CustomConfig": { - "description": "Custom configuration for TinyMCE and its plugins", - "type": "object" - }, - "ValidElements": { - "description": "A CSV string of valid HTML elements in the richtext editor. Ex: iframe[*],button[class|title]", - "type": "string" - }, - "InvalidElements": { - "description": "A CSV string of invalid HTML elements in the richtext editor. Ex: font", - "type": "string" - } - } - }, - "umbracoRichTextEditorCommands": { - "properties": { - "Name": { - "description": "Friendly name of Richtext Editor Command", - "type": "string" - }, - "Alias": { - "description": "Alias of the Richtext Editor Command", - "type": "string" - }, - "Mode": { - "description": "Set how the Richtext Editor Command can be used. Such as when a selection is made", - "enum": [ - "Insert", - "Selection", - "All" - ] - } - } - }, - "umbracoRequestHandler": { - "properties": { - "AddTrailingSlash": { - "description": "Indicating whether to add a trailing slash to URLs", - "type": "boolean", - "default": true - }, - "CharCollection": { - "description": "Character collection for replacements", - "type": "array", - "items": { - "$ref": "#/definitions/umbracoCharCollection" - } - }, - "ConvertUrlsToAscii": { - "description": "Indicating whether to convert URLs to ASCII (valid values: true, try or false)", - "enum": [ - "try", - "true", - "false" - ], - "default": "try" - } - } - }, - "umbracoCharCollection": { - "required": [ - "Char", - "Replacement" - ], - "properties": { - "Char": { - "type": "string", - "default": "รค" - }, - "Replacement": { - "type": "string", - "default": "ae" - } - } - }, - "umbracoRuntimeMinification": { - "properties": { - "UseInMemoryCache": { - "type": "boolean", - "default": false - }, - "CacheBuster": { - "description": "Cache buster type to use", - "enum": [ - "Version", - "AppDomain", - "Timestamp" - ], - "default": "Version" - } - } - }, - "umbracoSecurity": { + "Serilog": { + "type": "object", + "title": "Serilog appSettings", + "description": "Serilog appSettings Configuration", "properties": { - "AllowPasswordReset": { - "description": "Indicating whether to allow user password reset", - "type": "boolean", - "default": true - }, - "AuthCookieDomain": { - "description": "Authorization cookie domain", - "type": "string" - }, - "AuthCookieName": { - "description": "The authorization cookie name", + "$schema": { "type": "string", - "default": "UMB_UCONTEXT" - }, - "HideDisabledUsersInBackOffice": { - "description": "Indicating whether to hide disabled users in the back-office", - "type": "boolean", - "default": false + "title": "Schema", + "description": "Pointer to the schema against which this document should be validated." }, - "KeepUserLoggedIn": { - "description": "Indicating whether to keep the user logged in", - "type": "boolean", - "default": false + "Using": { + "type": "array", + "title": "List of Auto-discovery of configuration assemblies", + "description": "Using section contains list of assemblies in which configuration methods. Can be required depending of the project type: See: https://github.com/serilog/serilog-settings-configuration#using-section-and-auto-discovery-of-configuration-assemblies", + "uniqueItems": true, + "items": { + "$ref": "#/definitions/Serilog/definitions/AssemblyReference" + } }, - "MemberPassword": { - "$ref": "#/definitions/umbracoMemberPassword" + "LevelSwitches": { + "type": "object", + "patternProperties": { + "^(?\\${0,1}[A-Za-z]+[A-Za-z0-9]*)$": { + "$ref": "#/definitions/Serilog/definitions/SerilogLogEventLevel" + } + }, + "additionalProperties": false }, - "UsernameIsEmail": { - "description": "Indicating whether the user's email address is to be considered as their username", - "type": "boolean", - "default": true + "FilterSwitches": { + "type": "object", + "patternProperties": { + "^(?\\${0,1}[A-Za-z]+[A-Za-z0-9]*)$": { + "type": "string" + } + }, + "additionalProperties": false }, - "UserPassword": { - "$ref": "#/definitions/umbracoUserPassword" - } - } - }, - "umbracoMemberPassword": { - "properties": { - "RequiredLength": { - "type": "integer", - "default": 10 + "MinimumLevel": { + "type": [ + "string", + "object" + ], + "title": "Minimum LogLevel Threshold", + "description": "Minimum LogLevel Threshold. (Support dynamic reload if the underlying IConfigurationProvider supports it)", + "oneOf": [ + { + "$ref": "#/definitions/Serilog/definitions/SerilogLogEventLevel" + }, + { + "$ref": "#/definitions/Serilog/definitions/DetailedMinimumLevel" + } + ] }, - "RequireNonLetterOrDigit": { - "type": "boolean", - "default": false + "Properties": { + "type": "object", + "title": "Log events Properties", + "description": "This section defines a static list of key-value pairs that will enrich log events.", + "additionalProperties": { + "type": "string" + } }, - "RequireDigit": { - "type": "boolean", - "default": false + "Enrich": { + "allOf": [ + { + "$ref": "#/definitions/Serilog/definitions/MethodCallReference" + } + ], + "title": "Log events Enriches", + "description": "This section defines Enriches that will be applied to log events." }, - "RequireLowercase": { - "type": "boolean", - "default": false + "Destructure": { + "allOf": [ + { + "$ref": "#/definitions/Serilog/definitions/MethodCallReference" + } + ], + "title": "Log events Destructure", + "description": "This section defines Destructure." }, - "RequireUppercase": { - "type": "boolean", - "default": false + "Filter": { + "allOf": [ + { + "$ref": "#/definitions/Serilog/definitions/MethodCallReference" + } + ], + "title": "Log events filters", + "description": "This section defines filters that will be applied to log events." }, - "MaxFailedAccessAttemptsBeforeLockout": { - "type": "integer", - "default": 5 + "WriteTo": { + "allOf": [ + { + "$ref": "#/definitions/Serilog/definitions/MethodCallReference" + } + ], + "title": "Configuration for log destination", + "description": "This section configures the sinks that log events will be emitted to." }, - "HashAlgorithmType": { - "type": "string", - "default": "HMACSHA256" + "AuditTo": { + "allOf": [ + { + "$ref": "#/definitions/Serilog/definitions/MethodCallReference" + } + ], + "title": "Configuration for log destination for auditing", + "description": "This section configures sinks for auditing, instead of regular (safe) logging. Obs: When auditing is used, exceptions from sinks and any intermediate filters propagate back to the caller." } - } - }, - "umbracoUserPassword": { - "properties": { - "RequiredLength": { - "type": "integer", - "default": 10 - }, - "RequireNonLetterOrDigit": { - "type": "boolean", - "default": false - }, - "RequireDigit": { - "type": "boolean", - "default": false + }, + "patternProperties": { + "^Enrich:((?[a-zA-Z_]\\w*)|(?\\d*))$": { + "allOf": [ + { + "$ref": "#/definitions/Serilog/definitions/MethodCallReferenceItem" + } + ], + "title": "Log events Enriches", + "description": "This section defines Enriches that will be applied to log events." }, - "RequireLowercase": { - "type": "boolean", - "default": false + "^Destructure:((?[a-zA-Z_]\\w*)|(?\\d*))$": { + "allOf": [ + { + "$ref": "#/definitions/Serilog/definitions/MethodCallReferenceItem" + } + ], + "title": "Log events Destructure", + "description": "This section defines Destructure." }, - "RequireUppercase": { - "type": "boolean", - "default": false + "^Filter:((?[a-zA-Z_]\\w*)|(?\\d*))$": { + "allOf": [ + { + "$ref": "#/definitions/Serilog/definitions/MethodCallReferenceItem" + } + ], + "title": "Log events filters", + "description": "This section defines filters that will be applied to log events." }, - "MaxFailedAccessAttemptsBeforeLockout": { - "type": "integer", - "default": 5 + "^WriteTo:((?[a-zA-Z_]\\w*)|(?\\d*))$": { + "allOf": [ + { + "$ref": "#/definitions/Serilog/definitions/MethodCallReferenceItem" + } + ], + "title": "Configuration for log destination", + "description": "This section configures the sinks that log events will be emitted to." }, - "HashAlgorithmType": { - "type": "string", - "default": "PBKDF2.ASPNETCORE.V3" + "^AuditTo:((?[a-zA-Z_]\\w*)|(?\\d*))$": { + "allOf": [ + { + "$ref": "#/definitions/Serilog/definitions/MethodCallReferenceItem" + } + ], + "title": "Configuration for log destination for auditing", + "description": "This section configures sinks for auditing, instead of regular (safe) logging. Obs: When auditing is used, exceptions from sinks and any intermediate filters propagate back to the caller." } - } - }, - "umbracoWebRouting": { - "properties": { - "TryMatchingEndpointsForAllPages": { - "description": "Indicating whether to check if any routed endpoints match a front-end request before the Umbraco dynamic router tries to map the request to an Umbraco content item", - "type": "boolean", - "default": false - }, - "TrySkipIisCustomErrors": { - "description": "Indicating whether IIS custom errors should be skipped", - "type": "boolean", - "default": false + }, + "additionalProperties": false, + "definitions": { + "SerilogLogEventLevel": { + "type": "string", + "title": "Log level", + "description": "Log level threshold.", + "enum": [ + "Verbose", + "Debug", + "Information", + "Warning", + "Error", + "Fatal" + ] }, - "InternalRedirectPreservesTemplate": { - "description": "Indicating whether an internal redirect should preserve the template", - "type": "boolean", - "default": false + "LoggingLevelSwitch": { + "type": "string", + "title": "LevelSwitches name", + "description": "Log Level Switch string reference.", + "pattern": "^(?\\${0,1}[A-Za-z]+[A-Za-z0-9]*)$" }, - "DisableAlternativeTemplates": { - "description": "Indicating whether the use of alternative templates are disabled", - "type": "boolean", - "default": false + "SerilogLogLevelThreshold": { + "type": "string", + "title": "Log Level or LevelSwitches name", + "description": "A Serilog Log Level or a reference to a Log Level Switch name on `LevelSwitches` configuration.", + "anyOf": [ + { + "$ref": "#/definitions/Serilog/definitions/SerilogLogEventLevel" + }, + { + "$ref": "#/definitions/Serilog/definitions/LoggingLevelSwitch" + } + ] }, - "ValidateAlternativeTemplates": { - "description": "Indicating whether the use of alternative templates should be validated", - "type": "boolean", - "default": false + "DetailedMinimumLevel": { + "type": "object", + "title": "Detailed Log level.", + "description": "Detailed Log level threshold object. Allowing set log levels be overridden per logging source.", + "properties": { + "Default": { + "$ref": "#/definitions/Serilog/definitions/SerilogLogLevelThreshold" + }, + "ControlledBy": { + "$ref": "#/definitions/Serilog/definitions/LoggingLevelSwitch" + }, + "Override": { + "type": "object", + "title": "Logging Source Log level object.", + "description": "Set the Log level threshold or LevelSwitcher reference per Logging Source.", + "additionalProperties": { + "$ref": "#/definitions/Serilog/definitions/SerilogLogLevelThreshold" + } + } + }, + "additionalProperties": false }, - "DisableFindContentByIdPath": { - "description": "Indicating whether find content ID by path is disabled", - "type": "boolean", - "default": false + "AssemblyReference": { + "type": "string", + "title": "Assembly Name", + "description": ".NET Assembly Name, without the file extension", + "minLength": 1, + "pattern": "^(?\\S+)$" }, - "DisableRedirectUrlTracking": { - "description": "Indicating whether redirect URL tracking is disabled", - "type": "boolean", - "default": false + "ComplexMethodCallReference": { + "type": "object", + "properties": { + "Name": { + "$ref": "#/definitions/Serilog/definitions/CSharpMethodName" + }, + "Args": { + "type": "object", + "patternProperties": { + "^(?[a-zA-Z_]\\w*)$": {} + }, + "additionalProperties": false + } + }, + "additionalProperties": false, + "required": [ + "Name" + ] }, - "UrlProviderMode": { - "enum": [ - "Default", - "Relative", - "Absolute", - "Auto" + "MethodCallReferenceItem": { + "type": [ + "string", + "object" ], - "default": "Auto" - }, - "UmbracoApplicationUrl": { - "type": "string" - } - } - }, - "umbracoUnattended": { - "properties": { - "InstallUnattended": { - "description": "Indicating whether unattended installs are enabled", - "type": "boolean", - "default": false - }, - "UpgradeUnattended": { - "description": "Indicating whether unattended upgrades are enabled", - "type": "boolean", - "default": false - }, - "UnattendedUserName": { - "description": "Use for creating a user with a name for Unattended Installs", - "type": "string" - }, - "UnattendedUserEmail": { - "description": "Use for creating a user with an email for Unattended Installs", - "type": "string" + "oneOf": [ + { + "$ref": "#/definitions/Serilog/definitions/CSharpMethodName" + }, + { + "$ref": "#/definitions/Serilog/definitions/ComplexMethodCallReference" + } + ] }, - "UnattendedUserPassword": { - "description": "Use for creating a user with a password for Unattended Installs", - "type": "string" - } - } - }, - "umbracoModelsBuilder": { - "properties": { - "ModelsMode": { - "description": "ModelsBuilder generation mode", - "enum": [ - "Nothing", - "InMemoryAuto", - "SourceCodeManual", - "SourceCodeAuto" + "MethodCallReference": { + "type": [ + "array", + "string", + "object" ], - "default": "InMemoryAuto" + "minLength": 1, + "pattern": "^(?[a-zA-Z_]\\w*)$", + "minItems": 1, + "uniqueItems": true, + "items": { + "$ref": "#/definitions/Serilog/definitions/MethodCallReferenceItem" + }, + "additionalProperties": { + "$ref": "#/definitions/Serilog/definitions/MethodCallReferenceItem" + } }, - "ModelsNamespace": { + "CSharpMethodName": { "type": "string", - "description": "Namespace to use when generating strongly typed models", - "default": "Umbraco.Cms.Web.Common.PublishedModels" + "title": "Method Name", + "description": "A name referring to a C# Class method", + "minLength": 1, + "pattern": "^(?[a-zA-Z_]\\w*)$" }, - "ModelsDirectory": { + "CSharpMethodArgumentName": { "type": "string", - "description": "Location to generate ModelsBuilder models", - "default": "~/umbraco/models" - }, - "DebugLevel": { - "type": "integer", - "description": "Indicates the debug level. For internal / development use. Set to greater than zero to enable detailed logging.", - "default": 0 + "title": "Argument Name", + "description": "A name referring to a C# Class method argument", + "minLength": 1, + "pattern": "^(?[a-zA-Z_]\\w*)$" }, - "AcceptUnsafeModelsDirectory": { - "type": "boolean", - "description": "Indicates that the directory indicated in ModelsDirectory is allowed to be outside the website root (e.g. ~/../../some/place). Because that can be a potential security risk, it is not allowed by default.", - "default": false + "EnvironmentVariableName": { + "type": "string", + "title": "Environment Variable Name", + "description": "A name referring to a OS Environment Variable", + "minLength": 1, + "pattern": "^(?[a-zA-Z_]\\w*)$" }, - "FlagOutOfDateModels": { - "type": "boolean", - "description": "Indicates whether out-of-date models (i.e. after a content type or data type has been modified) should be flagged.", - "default": true + "SerilogLevelSwitcherName": { + "type": "string", + "title": "A Level Switcher Name", + "description": "A name referring to a Serilog Settings Configuration Level Switcher", + "minLength": 1, + "pattern": "^(?\\${0,1}[A-Za-z]+[A-Za-z0-9]*)$" } } }, @@ -1686,15 +1102,15 @@ "Examine": { "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsIndexCreatorSettings" }, + "Indexing": { + "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsIndexingSettings" + }, "KeepAlive": { "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsKeepAliveSettings" }, "Logging": { "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsLoggingSettings" }, - "MemberPassword": { - "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsMemberPasswordConfigurationSettings" - }, "NuCache": { "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsNuCacheSettings" }, @@ -1713,9 +1129,6 @@ "TypeFinder": { "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsTypeFinderSettings" }, - "UserPassword": { - "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsUserPasswordConfigurationSettings" - }, "WebRouting": { "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsWebRoutingSettings" }, @@ -1746,11 +1159,14 @@ "HelpPage": { "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsHelpPageSettings" }, - "DefaultDataCreation": { - "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsInstallDefaultDataSettings" + "InstallDefaultData": { + "$ref": "#/definitions/JsonSchemaInstallDefaultData" }, "DataTypes": { "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsDataTypesSettings" + }, + "Marketplace": { + "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsMarketplaceSettings" } } }, @@ -1800,21 +1216,6 @@ } ] }, - "DisallowedUploadFiles": { - "type": "array", - "description": "Gets or sets a value for the collection of file extensions that are disallowed for upload.\n ", - "default": "ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,xamlx", - "items": { - "type": "string" - } - }, - "AllowedUploadFiles": { - "type": "array", - "description": "Gets or sets a value for the collection of file extensions that are allowed for upload.\n ", - "items": { - "type": "string" - } - }, "ShowDeprecatedPropertyEditors": { "type": "boolean", "description": "Gets or sets a value indicating whether deprecated property editors should be shown.\n ", @@ -1852,6 +1253,33 @@ "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsContentVersionCleanupPolicySettings" } ] + }, + "AllowEditInvariantFromNonDefault": { + "type": "boolean", + "description": "Gets or sets a value indicating whether to allow editing invariant properties from a non-default language variation.", + "default": false + }, + "AllowedUploadedFileExtensions": { + "type": "array", + "description": "Gets or sets a value for the collection of file extensions that are allowed for upload.\n ", + "items": { + "type": "string" + } + }, + "DisallowedUploadedFileExtensions": { + "type": "array", + "description": "Gets or sets a value for the collection of file extensions that are disallowed for upload.\n ", + "default": "ashx,aspx,ascx,config,cshtml,vbhtml,asmx,air,axd,xamlx", + "items": { + "type": "string" + } + }, + "AllowedMediaHosts": { + "type": "array", + "description": "Gets or sets the allowed external host for media. If empty only relative paths are allowed.", + "items": { + "type": "string" + } } } }, @@ -2145,11 +1573,6 @@ "format": "int32", "default": 7 }, - "UmbracoPath": { - "type": "string", - "description": "Gets or sets a value for the Umbraco back-office path.\n ", - "default": "~/umbraco" - }, "IconsPath": { "type": "string", "description": "Gets or sets a value for the Umbraco icons path.\n ", @@ -2264,6 +1687,10 @@ "description": "Force url paths to be left to right, even when the culture has right to left text", "default": true, "x-example": "For the following hierarchy\n- Root (/ar)\n - 1 (/ar/1)\n - 2 (/ar/1/2)\n - 3 (/ar/1/2/3)\n - 3 (/ar/1/2/3/4)\nWhen forced\n- https://www.umbraco.com/ar/1/2/3/4\nwhen not\n- https://www.umbraco.com/ar/4/3/2/1" + }, + "ShowMaintenancePageWhenInUpgradeState": { + "type": "boolean", + "default": true } } }, @@ -2690,6 +2117,17 @@ "TempFileSystemDirectoryFactory" ] }, + "UmbracoCmsCoreConfigurationModelsIndexingSettings": { + "type": "object", + "description": "Typed configuration options for index creator settings.\n ", + "properties": { + "ExplicitlyIndexEachNestedProperty": { + "type": "boolean", + "description": "Gets or sets a value for whether each nested property should have it's own indexed value. Requires a rebuild of indexes when changed.", + "default": true + } + } + }, "UmbracoCmsCoreConfigurationModelsKeepAliveSettings": { "type": "object", "description": "Typed configuration options for keep alive settings.\n ", @@ -2718,49 +2156,6 @@ } } }, - "UmbracoCmsCoreConfigurationModelsMemberPasswordConfigurationSettings": { - "type": "object", - "description": "Typed configuration options for member password settings.\n ", - "properties": { - "RequiredLength": { - "type": "integer", - "description": "Gets a value for the minimum required length for the password.\n ", - "format": "int32", - "default": 10 - }, - "RequireNonLetterOrDigit": { - "type": "boolean", - "description": "Gets a value indicating whether at least one non-letter or digit is required for the password.\n ", - "default": false - }, - "RequireDigit": { - "type": "boolean", - "description": "Gets a value indicating whether at least one digit is required for the password.\n ", - "default": false - }, - "RequireLowercase": { - "type": "boolean", - "description": "Gets a value indicating whether at least one lower-case character is required for the password.\n ", - "default": false - }, - "RequireUppercase": { - "type": "boolean", - "description": "Gets a value indicating whether at least one upper-case character is required for the password.\n ", - "default": false - }, - "HashAlgorithmType": { - "type": "string", - "description": "Gets a value for the password hash algorithm type.\n ", - "default": "PBKDF2.ASPNETCORE.V3" - }, - "MaxFailedAccessAttemptsBeforeLockout": { - "type": "integer", - "description": "Gets a value for the maximum failed access attempts before lockout.\n ", - "format": "int32", - "default": 5 - } - } - }, "UmbracoCmsCoreConfigurationModelsNuCacheSettings": { "type": "object", "description": "Typed configuration options for NuCache settings.\n ", @@ -2796,6 +2191,10 @@ }, "UnPublishedContentCompression": { "type": "boolean" + }, + "UsePagedSqlQuery": { + "type": "boolean", + "default": true } } }, @@ -2940,28 +2339,6 @@ "description": "Gets or sets the set of allowed characters for a username\n ", "default": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._@+\\" }, - "UserPassword": { - "description": "Gets or sets a value for the user password settings.\n ", - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsUserPasswordConfigurationSettings" - } - ] - }, - "MemberPassword": { - "description": "Gets or sets a value for the member password settings.\n ", - "oneOf": [ - { - "type": "null" - }, - { - "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsMemberPasswordConfigurationSettings" - } - ] - }, "MemberBypassTwoFactorForExternalLogins": { "type": "boolean", "description": "Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login\nfor members. Thereby rely on the External login and potential 2FA at that provider.\n ", @@ -2971,49 +2348,18 @@ "type": "boolean", "description": "Gets or sets a value indicating whether to bypass the two factor requirement in Umbraco when using external login\nfor users. Thereby rely on the External login and potential 2FA at that provider.\n ", "default": true - } - } - }, - "UmbracoCmsCoreConfigurationModelsUserPasswordConfigurationSettings": { - "type": "object", - "description": "Typed configuration options for user password settings.\n ", - "properties": { - "RequiredLength": { + }, + "MemberDefaultLockoutTimeInMinutes": { "type": "integer", - "description": "Gets a value for the minimum required length for the password.\n ", + "description": "Gets or sets a value for how long (in minutes) a member is locked out when a lockout occurs.\n ", "format": "int32", - "default": 10 - }, - "RequireNonLetterOrDigit": { - "type": "boolean", - "description": "Gets a value indicating whether at least one non-letter or digit is required for the password.\n ", - "default": false - }, - "RequireDigit": { - "type": "boolean", - "description": "Gets a value indicating whether at least one digit is required for the password.\n ", - "default": false - }, - "RequireLowercase": { - "type": "boolean", - "description": "Gets a value indicating whether at least one lower-case character is required for the password.\n ", - "default": false - }, - "RequireUppercase": { - "type": "boolean", - "description": "Gets a value indicating whether at least one upper-case character is required for the password.\n ", - "default": false + "default": 43200 }, - "HashAlgorithmType": { - "type": "string", - "description": "Gets a value for the password hash algorithm type.\n ", - "default": "PBKDF2.ASPNETCORE.V3" - }, - "MaxFailedAccessAttemptsBeforeLockout": { + "UserDefaultLockoutTimeInMinutes": { "type": "integer", - "description": "Gets a value for the maximum failed access attempts before lockout.\n ", + "description": "Gets or sets a value for how long (in minutes) a user is locked out when a lockout occurs.\n ", "format": "int32", - "default": 5 + "default": 43200 } } }, @@ -3049,6 +2395,13 @@ "items": { "type": "string" } + }, + "AdditionalAssemblyExclusionEntries": { + "type": "array", + "description": "Gets or sets a value for the assemblies that will be excluded from scanning.\n ", + "items": { + "type": "string" + } } } }, @@ -3182,21 +2535,21 @@ "properties": { "Commands": { "type": "array", - "description": "HTML RichText Editor TinyMCE Commands\n ", + "description": "HTML RichText Editor TinyMCE Commands.\n ", "items": { "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsRichTextEditorCommand" } }, "Plugins": { "type": "array", - "description": "HTML RichText Editor TinyMCE Plugins\n ", + "description": "HTML RichText Editor TinyMCE Plugins.\n ", "items": { "type": "string" } }, "CustomConfig": { "type": "object", - "description": "HTML RichText Editor TinyMCE Custom Config\n ", + "description": "HTML RichText Editor TinyMCE Custom Config.\n ", "additionalProperties": { "type": "string" } @@ -3207,7 +2560,7 @@ }, "InvalidElements": { "type": "string", - "description": "Invalid HTML elements for RichText Editor\n ", + "description": "Invalid HTML elements for RichText Editor.\n ", "default": "font" } } @@ -3395,6 +2748,24 @@ } } }, + "JsonSchemaInstallDefaultData": { + "type": "object", + "description": "Configurations for the Umbraco CMS InstallDefaultData configuration.\n ", + "properties": { + "Languages": { + "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsInstallDefaultDataSettings" + }, + "DataTypes": { + "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsInstallDefaultDataSettings" + }, + "MediaTypes": { + "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsInstallDefaultDataSettings" + }, + "MemberTypes": { + "$ref": "#/definitions/UmbracoCmsCoreConfigurationModelsInstallDefaultDataSettings" + } + } + }, "UmbracoCmsCoreConfigurationModelsInstallDefaultDataSettings": { "type": "object", "description": "Typed configuration options for installation of default data.\n ", @@ -3460,6 +2831,19 @@ "FalseWithHelpText" ] }, + "UmbracoCmsCoreConfigurationModelsMarketplaceSettings": { + "type": "object", + "description": "Configuration options for the Marketplace.", + "properties": { + "AdditionalParameters": { + "type": "object", + "description": "Gets or sets the additional parameters that are sent to the Marketplace.", + "additionalProperties": { + "type": "string" + } + } + } + }, "JsonSchemaFormsDefinition": { "type": "object", "description": "Configurations for the Umbraco Forms package to Umbraco CMS\n ", @@ -3499,6 +2883,18 @@ }, "DefaultEmailTemplate": { "type": "string" + }, + "RemoveProvidedEmailTemplate": { + "type": "boolean" + }, + "RemoveProvidedFormTemplates": { + "type": "boolean" + }, + "FormElementHtmlIdPrefix": { + "type": "string" + }, + "SettingsCustomization": { + "$ref": "#/definitions/UmbracoFormsCoreConfigurationValidationSettingsCustomization" } } }, @@ -3529,14 +2925,34 @@ "HideFieldValidationLabels": { "type": "boolean" }, + "NextPageButtonLabel": { + "type": "string" + }, + "PreviousPageButtonLabel": { + "type": "string" + }, + "SubmitButtonLabel": { + "type": "string" + }, "MessageOnSubmit": { "type": "string" }, + "MessageOnSubmitIsHtml": { + "type": "boolean" + }, "StoreRecordsLocally": { "type": "boolean" }, "AutocompleteAttribute": { "type": "string" + }, + "DaysToRetainSubmittedRecordsFor": { + "type": "integer", + "format": "int32" + }, + "DaysToRetainApprovedRecordsFor": { + "type": "integer", + "format": "int32" } } }, @@ -3554,6 +2970,49 @@ "MarkOptionalFields" ] }, + "UmbracoFormsCoreConfigurationValidationSettingsCustomization": { + "type": "object", + "properties": { + "DataSourceTypes": { + "$ref": "#/definitions/UmbracoFormsCoreConfigurationProviderSettingsCustomization" + }, + "FieldTypes": { + "$ref": "#/definitions/UmbracoFormsCoreConfigurationProviderSettingsCustomization" + }, + "PrevalueSourceTypes": { + "$ref": "#/definitions/UmbracoFormsCoreConfigurationProviderSettingsCustomization" + }, + "WorkflowTypes": { + "$ref": "#/definitions/UmbracoFormsCoreConfigurationProviderSettingsCustomization" + } + } + }, + "UmbracoFormsCoreConfigurationProviderSettingsCustomization": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": { + "$ref": "#/definitions/UmbracoFormsCoreConfigurationProviderSettingsCustomizationDetail" + } + } + }, + "UmbracoFormsCoreConfigurationProviderSettingsCustomizationDetail": { + "type": "object", + "properties": { + "IsHidden": { + "type": "boolean" + }, + "DefaultValue": { + "type": [ + "null", + "string" + ] + }, + "IsReadOnly": { + "type": "boolean" + } + } + }, "UmbracoFormsCoreConfigurationPackageOptionSettings": { "type": "object", "properties": { @@ -3568,6 +3027,44 @@ }, "AppendQueryStringOnRedirectAfterFormSubmission": { "type": "boolean" + }, + "CultureToUseWhenParsingDatesForBackOffice": { + "type": "string" + }, + "TriggerConditionsCheckOn": { + "type": "string" + }, + "ScheduledRecordDeletion": { + "$ref": "#/definitions/UmbracoFormsCoreConfigurationScheduledRecordDeletionSettings" + }, + "DisableRecordIndexing": { + "type": "boolean" + }, + "EnableFormsApi": { + "type": "boolean" + }, + "EnableRecordingOfIpWithFormSubmission": { + "type": "boolean" + }, + "UseSemanticFieldsetRendering": { + "type": "boolean" + } + } + }, + "UmbracoFormsCoreConfigurationScheduledRecordDeletionSettings": { + "type": "object", + "properties": { + "Enabled": { + "type": "boolean", + "default": false + }, + "FirstRunTime": { + "type": "string" + }, + "Period": { + "type": "string", + "format": "duration", + "default": "1.00:00:00" } } }, @@ -3594,6 +3091,15 @@ }, "DefaultUserAccessToNewForms": { "$ref": "#/definitions/UmbracoFormsCoreConfigurationFormAccess" + }, + "FormsApiKey": { + "type": [ + "null", + "string" + ] + }, + "EnableAntiForgeryTokenForFormsApi": { + "type": "boolean" } } }, @@ -3652,9 +3158,24 @@ }, "PrivateKey": { "type": "string" + }, + "Domain": { + "$ref": "#/definitions/UmbracoFormsCoreConfigurationRecaptchaDomain" } } }, + "UmbracoFormsCoreConfigurationRecaptchaDomain": { + "type": "string", + "description": "", + "x-enumNames": [ + "Google", + "Recaptcha" + ], + "enum": [ + "Google", + "Recaptcha" + ] + }, "JsonSchemaDeployDefinition": { "type": "object", "description": "Configurations for the Umbraco Deploy package to Umbraco CMS\n ", @@ -3719,8 +3240,9 @@ "type": "string", "format": "duration" }, - "IgnoreBrokenDependencies": { - "type": "boolean" + "DiskOperationsTimeout": { + "type": "string", + "format": "duration" }, "IgnoreBrokenDependenciesBehavior": { "$ref": "#/definitions/UmbracoDeployCoreConfigurationDeployConfigurationIgnoreBrokenDependenciesBehavior" @@ -3731,6 +3253,9 @@ "TransferDictionaryAsContent": { "type": "boolean" }, + "IgnoreMissingLanguagesForDictionaryItems": { + "type": "boolean" + }, "AllowMembersDeploymentOperations": { "$ref": "#/definitions/UmbracoDeployCoreConfigurationDeployConfigurationMembersDeploymentOperations" }, @@ -3742,6 +3267,45 @@ }, "ExportMemberGroups": { "type": "boolean" + }, + "AllowDomainsDeploymentOperations": { + "$ref": "#/definitions/UmbracoDeployCoreConfigurationDeployConfigurationDomainsDeploymentOperations" + }, + "ReloadMemoryCacheFollowingDiskReadOperation": { + "type": "boolean" + }, + "PreferLocalDbConnectionString": { + "type": "boolean" + }, + "UseDatabaseTransferQueue": { + "type": "boolean" + }, + "SourceDeployBatchSize": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "PackageBatchSize": { + "type": [ + "integer", + "null" + ], + "format": "int32" + }, + "MediaFileChecksumCalculationMethod": { + "$ref": "#/definitions/UmbracoDeployCoreConfigurationDeployConfigurationMediaFileChecksumCalculationMethod" + }, + "NumberOfSignaturesToUseAllRelationCache": { + "type": "integer", + "format": "int32" + }, + "ContinueOnMediaFilePathTooLongException": { + "type": "boolean" + }, + "SuppressCacheRefresherNotifications": { + "type": "boolean" } } }, @@ -3827,9 +3391,43 @@ "All" ] }, + "UmbracoDeployCoreConfigurationDeployConfigurationDomainsDeploymentOperations": { + "type": "string", + "description": "", + "x-enumFlags": true, + "x-enumNames": [ + "None", + "Culture", + "AbsolutePath", + "Hostname", + "All" + ], + "enum": [ + "None", + "Culture", + "AbsolutePath", + "Hostname", + "All" + ] + }, + "UmbracoDeployCoreConfigurationDeployConfigurationMediaFileChecksumCalculationMethod": { + "type": "string", + "description": "", + "x-enumNames": [ + "PartialFileContents", + "Metadata" + ], + "enum": [ + "PartialFileContents", + "Metadata" + ] + }, "UmbracoDeployCoreConfigurationDeployProjectConfigurationDeployProjectConfig": { "type": "object", "properties": { + "CurrentWorkspaceName": { + "type": "string" + }, "Workspaces": { "type": "array", "items": { @@ -3883,6 +3481,7 @@ } } }, + "id": "https://json.schemastore.org/appsettings.json", "patternProperties": { "^WebOptimizer$": { "$ref": "#/definitions/webOptimizer" @@ -3905,8 +3504,8 @@ "^(nlog|Nlog|NLog)$": { "$ref": "#/definitions/NLog" }, - "^(Umbraco|umbraco)$": { - "$ref": "#/definitions/umbraco" + "^(Serilog|serilog)$": { + "$ref": "#/definitions/Serilog" } }, "properties": { diff --git a/src/Articulate/Articulate.csproj b/src/Articulate/Articulate.csproj index 510ff3c6..dbb8d1d6 100644 --- a/src/Articulate/Articulate.csproj +++ b/src/Articulate/Articulate.csproj @@ -1,6 +1,6 @@ - net6.0;net7.0 + net6.0;net7.0;net8.0 Library ..\ true diff --git a/src/Articulate/Components/ArticulateComposer.cs b/src/Articulate/Components/ArticulateComposer.cs index ef0cf085..6a8e5dc9 100644 --- a/src/Articulate/Components/ArticulateComposer.cs +++ b/src/Articulate/Components/ArticulateComposer.cs @@ -5,7 +5,9 @@ using Articulate.Routing; using Articulate.Services; using Articulate.Syndication; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Umbraco.Cms.Core.Composing; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Notifications; @@ -36,6 +38,7 @@ public override void Compose(IUmbracoBuilder builder) services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton()); builder.UrlProviders().InsertBefore(); diff --git a/src/Articulate/Controllers/ArticulateDynamicRouteAttribute.cs b/src/Articulate/Controllers/ArticulateDynamicRouteAttribute.cs new file mode 100644 index 00000000..0cf83cad --- /dev/null +++ b/src/Articulate/Controllers/ArticulateDynamicRouteAttribute.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Articulate.Controllers +{ + [AttributeUsage(AttributeTargets.Class)] + public sealed class ArticulateDynamicRouteAttribute : Attribute + { + } +} diff --git a/src/Articulate/Controllers/ArticulateRssController.cs b/src/Articulate/Controllers/ArticulateRssController.cs index b2537710..64647298 100644 --- a/src/Articulate/Controllers/ArticulateRssController.cs +++ b/src/Articulate/Controllers/ArticulateRssController.cs @@ -27,6 +27,7 @@ namespace Articulate.Controllers #if NET7_0_OR_GREATER [OutputCache(PolicyName = "Articulate300")] #endif + [ArticulateDynamicRoute] public class ArticulateRssController : RenderController { private readonly IRssFeedGenerator _feedGenerator; diff --git a/src/Articulate/Controllers/ArticulateSearchController.cs b/src/Articulate/Controllers/ArticulateSearchController.cs index ed7db70f..b563b59e 100644 --- a/src/Articulate/Controllers/ArticulateSearchController.cs +++ b/src/Articulate/Controllers/ArticulateSearchController.cs @@ -17,6 +17,7 @@ namespace Articulate.Controllers /// /// Renders search results /// + [ArticulateDynamicRoute] public class ArticulateSearchController : ListControllerBase { private readonly IArticulateSearcher _articulateSearcher; diff --git a/src/Articulate/Controllers/ArticulateTagsController.cs b/src/Articulate/Controllers/ArticulateTagsController.cs index 49d1928e..8f53f6a8 100644 --- a/src/Articulate/Controllers/ArticulateTagsController.cs +++ b/src/Articulate/Controllers/ArticulateTagsController.cs @@ -13,7 +13,9 @@ using Articulate.Services; using Umbraco.Cms.Core.PublishedCache; using System.Collections.Generic; -#if NET7_0_OR_GREATER +using Umbraco.Cms.Web.Common.Attributes; + +#if NET7_0_OR_GREATER using Microsoft.AspNetCore.OutputCaching; #endif @@ -28,7 +30,7 @@ namespace Articulate.Controllers #if NET7_0_OR_GREATER [OutputCache(PolicyName = "Articulate60")] #endif - + [ArticulateDynamicRoute] public class ArticulateTagsController : ListControllerBase { private readonly UmbracoHelper _umbracoHelper; @@ -51,7 +53,7 @@ public ArticulateTagsController( _articulateTagService = articulateTagService; _tagQuery = tagQuery; } - + /// /// Used to render the category listing (virtual node) /// diff --git a/src/Articulate/Controllers/MarkdownEditorController.cs b/src/Articulate/Controllers/MarkdownEditorController.cs index 88f390f1..2b916243 100644 --- a/src/Articulate/Controllers/MarkdownEditorController.cs +++ b/src/Articulate/Controllers/MarkdownEditorController.cs @@ -12,6 +12,7 @@ namespace Articulate.Controllers { + [ArticulateDynamicRoute] public class MarkdownEditorController : RenderController { private readonly UmbracoApiControllerTypeCollection _apiControllers; diff --git a/src/Articulate/Controllers/MetaWeblogController.cs b/src/Articulate/Controllers/MetaWeblogController.cs index 37d53afd..b55308e9 100644 --- a/src/Articulate/Controllers/MetaWeblogController.cs +++ b/src/Articulate/Controllers/MetaWeblogController.cs @@ -23,6 +23,7 @@ namespace Articulate.Controllers /// middleware but that just supports one endpoint, we are basically wrapping that /// with our own multi-tenanted version. /// + [ArticulateDynamicRoute] public class MetaWeblogController : RenderController { private readonly IServiceProvider _serviceProvider; diff --git a/src/Articulate/Controllers/OpenSearchController.cs b/src/Articulate/Controllers/OpenSearchController.cs index 04254834..796a66f2 100644 --- a/src/Articulate/Controllers/OpenSearchController.cs +++ b/src/Articulate/Controllers/OpenSearchController.cs @@ -10,6 +10,7 @@ namespace Articulate.Controllers { + [ArticulateDynamicRoute] public class OpenSearchController : RenderController { private readonly IPublishedValueFallback _publishedValueFallback; diff --git a/src/Articulate/Controllers/RsdController.cs b/src/Articulate/Controllers/RsdController.cs index 992ea700..c72cd454 100644 --- a/src/Articulate/Controllers/RsdController.cs +++ b/src/Articulate/Controllers/RsdController.cs @@ -15,6 +15,7 @@ namespace Articulate.Controllers /// /// Really simple discovery controller /// + [ArticulateDynamicRoute] public class RsdController : RenderController { private readonly UmbracoHelper _umbracoHelper; diff --git a/src/Articulate/Controllers/WlwManifestController.cs b/src/Articulate/Controllers/WlwManifestController.cs index 698b6c7f..13c604e2 100644 --- a/src/Articulate/Controllers/WlwManifestController.cs +++ b/src/Articulate/Controllers/WlwManifestController.cs @@ -8,6 +8,7 @@ namespace Articulate.Controllers { + [ArticulateDynamicRoute] public class WlwManifestController : RenderController { private readonly UmbracoHelper _umbraco; diff --git a/src/Articulate/Routing/ArticulateDynamicRouteSelectorPolicy.cs b/src/Articulate/Routing/ArticulateDynamicRouteSelectorPolicy.cs new file mode 100644 index 00000000..881263f7 --- /dev/null +++ b/src/Articulate/Routing/ArticulateDynamicRouteSelectorPolicy.cs @@ -0,0 +1,74 @@ +#nullable enable + +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Articulate.Controllers; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Matching; +using Umbraco.Cms.Web.Common.Routing; + +namespace Articulate.Routing +{ + /// + /// Used when their is ambiguous route candidates due to multiple dynamic routes being assigned. + /// + /// + /// Ambiguous dynamic routes can occur if Umbraco detects a 404 and assigns a route, but sometimes its not + /// actually a 404 because the articulate router occurs after the Umbraco router which handles 404 eagerly. + /// This causes 2x candidates to be resolved and the first (umbraco) is chosen. + /// If we detect that Articulate actually performed the routing, then we use that candidate instead. + /// TODO: Ideally - Umbraco would dynamically route the 404 in a much later state which could be done, + /// by a dynamic router that has a much larger Order so it occurs later in the pipeline instead of eagerly. + /// + internal class ArticulateDynamicRouteSelectorPolicy : MatcherPolicy, IEndpointSelectorPolicy + { + public override int Order => 100; + + public bool AppliesToEndpoints(IReadOnlyList endpoints) + { + // Don't apply this filter to any endpoint group that is a controller route + // i.e. only dynamic routes. + foreach (Endpoint endpoint in endpoints) + { + ControllerAttribute? controller = endpoint.Metadata.GetMetadata(); + if (controller != null) + { + return false; + } + } + + // then ensure this is only applied if all endpoints are IDynamicEndpointMetadata + return endpoints.All(x => x.Metadata.GetMetadata() != null); + } + + public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) + { + var umbracoRouteValues = httpContext.Features.Get(); + + // If the request has been dynamically routed by articulate to an + // Articulate controller + if (umbracoRouteValues != null + && umbracoRouteValues.ControllerActionDescriptor.EndpointMetadata.Any(x => x is ArticulateDynamicRouteAttribute)) + { + for (var i = 0; i < candidates.Count; i++) + { + // If the candidate is an Articulate dynamic controller, set valid + if (candidates[i].Endpoint.Metadata.GetMetadata() is not null) + { + candidates.SetValidity(i, true); + } + else + { + // else it is invalid + candidates.SetValidity(i, false); + } + } + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Articulate/Routing/ArticulateRouteValueTransformer.cs b/src/Articulate/Routing/ArticulateRouteValueTransformer.cs index 86db7496..1216d66c 100644 --- a/src/Articulate/Routing/ArticulateRouteValueTransformer.cs +++ b/src/Articulate/Routing/ArticulateRouteValueTransformer.cs @@ -1,3 +1,5 @@ +#nullable enable + using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.AspNetCore.Routing; @@ -148,6 +150,8 @@ private async Task WriteRouteValues(IUmbracoContext umbracoContext, HttpContext // Store the route values as a httpcontext feature httpContext.Features.Set(umbracoRouteValues); + umbracoContext.PublishedRequest = publishedRequest; + values[ControllerToken] = dynamicRouteValues.ControllerActionDescriptor.ControllerName; if (string.IsNullOrWhiteSpace(dynamicRouteValues.ControllerActionDescriptor.ActionName) == false) { @@ -175,9 +179,11 @@ private bool ShouldCheck( return false; } - // If route values have already been assigned, then Umbraco has - // matched content, we will not proceed. - if (umbracoRouteValues?.PublishedRequest?.PublishedContent != null) + // If route values have already been assigned, then Umbraco has matched content, we will not proceed. + // A 404 can be matched by Umbraco too which will occur for Articulate dynamic routes, so we need to + // proceed to see if it is actually a 404. + if (umbracoRouteValues?.PublishedRequest?.PublishedContent != null + && umbracoRouteValues?.PublishedRequest?.ResponseStatusCode != 404) { return false; } diff --git a/src/Articulate/Routing/ArticulateRouter.cs b/src/Articulate/Routing/ArticulateRouter.cs index 2d95db3d..89b2e224 100644 --- a/src/Articulate/Routing/ArticulateRouter.cs +++ b/src/Articulate/Routing/ArticulateRouter.cs @@ -20,16 +20,14 @@ public class ArticulateRouter { private static readonly object s_locker = new object(); private static readonly string s_searchControllerName = ControllerExtensions.GetControllerName(); - private static readonly string s_openSearchControllerName = ControllerExtensions.GetControllerName(); - private static readonly string s_rsdControllerName = ControllerExtensions.GetControllerName(); - private static readonly string s_wlwControllerName = ControllerExtensions.GetControllerName(); + private static readonly string s_openSearchControllerName = ControllerExtensions.GetControllerName(); + private static readonly string s_rsdControllerName = ControllerExtensions.GetControllerName(); + private static readonly string s_wlwControllerName = ControllerExtensions.GetControllerName(); private static readonly string s_tagsControllerName = ControllerExtensions.GetControllerName(); private static readonly string s_rssControllerName = ControllerExtensions.GetControllerName(); private static readonly string s_markdownEditorControllerName = ControllerExtensions.GetControllerName(); private static readonly string s_metaWeblogControllerName = ControllerExtensions.GetControllerName(); - - private readonly Dictionary _routeCache = new(); private readonly IControllerActionSearcher _controllerActionSearcher; @@ -44,7 +42,7 @@ public ArticulateRouter(IControllerActionSearcher controllerActionSearcher) public bool TryMatch(PathString path, RouteValueDictionary routeValues, out ArticulateRootNodeCache articulateRootNodeCache) { - foreach(var item in _routeCache) + foreach (var item in _routeCache) { var templateMatcher = new TemplateMatcher(item.Key.RouteTemplate, routeValues); if (templateMatcher.TryMatch(path, routeValues)) @@ -116,17 +114,17 @@ public void MapRoutes(HttpContext httpContext, IUmbracoContext umbracoContext) MapAuthorsRssRoute(httpContext, rootNodePath, articulateRootNode, domains); MapSearchRoute(httpContext, rootNodePath, articulateRootNode, domains); - MapMetaWeblogRoute(httpContext, rootNodePath, articulateRootNode, domains); - MapManifestRoute(httpContext, rootNodePath, articulateRootNode, domains); + MapMetaWeblogRoute(httpContext, rootNodePath, articulateRootNode, domains); + MapManifestRoute(httpContext, rootNodePath, articulateRootNode, domains); MapRsdRoute(httpContext, rootNodePath, articulateRootNode, domains); MapOpenSearchRoute(httpContext, rootNodePath, articulateRootNode, domains); // tags/cats routes are the least specific MapTagsAndCategoriesRoute(httpContext, rootNodePath, articulateRootNode, domains); } - } + } } - } + } /// /// Generically caches a url path for a particular controller @@ -153,7 +151,7 @@ private void MapRoute( _routeCache[art] = dynamicRouteValues; } - dynamicRouteValues.Add(articulateRootNode.Id, DomainsForContent(articulateRootNode,domains)); + dynamicRouteValues.Add(articulateRootNode.Id, DomainsForContent(articulateRootNode, domains)); } private List DomainsForContent(IPublishedContent content, IReadOnlyList domains) @@ -175,7 +173,7 @@ private void MapOpenSearchRoute(HttpContext httpContext, string rootNodePath, IP domains); } - private void MapRsdRoute(HttpContext httpContext, string rootNodePath, IPublishedContent articulateRootNode, List domains) + private void MapRsdRoute(HttpContext httpContext, string rootNodePath, IPublishedContent articulateRootNode, List domains) { RouteTemplate template = TemplateParser.Parse($"{rootNodePath}rsd/{{id}}"); MapRoute(