From 79316d6eaa6455f6d248782bdf4e6d113616ad99 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Wed, 8 Sep 2021 13:34:51 +0200 Subject: [PATCH 01/19] Store certificate days to expiry in request message properties and clean-up HttpsCheck --- .../Checks/Security/HttpsCheck.cs | 82 ++++++++++--------- 1 file changed, 42 insertions(+), 40 deletions(-) diff --git a/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs b/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs index 2e3ef9f5c8..02c726c0c7 100644 --- a/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs +++ b/src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs @@ -1,4 +1,4 @@ -// Copyright (c) Umbraco. +// Copyright (c) Umbraco. // See LICENSE for more details. using System; @@ -17,7 +17,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Core.HealthChecks.Checks.Security { /// - /// Health checks for the recommended production setup regarding https. + /// Health checks for the recommended production setup regarding HTTPS. /// [HealthCheck( "EB66BB3B-1BCD-4314-9531-9DA2C1D6D9A7", @@ -26,17 +26,26 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security Group = "Security")] public class HttpsCheck : HealthCheck { + private const int NumberOfDaysForExpiryWarning = 14; + private const string HttpPropertyKeyCertificateDaysToExpiry = "CertificateDaysToExpiry"; + private readonly ILocalizedTextService _textService; private readonly IOptionsMonitor _globalSettings; private readonly IHostingEnvironment _hostingEnvironment; private static HttpClient s_httpClient; - private static HttpClientHandler s_httpClientHandler; - private static int s_certificateDaysToExpiry; + + private static HttpClient HttpClient => s_httpClient ??= new HttpClient(new HttpClientHandler() + { + ServerCertificateCustomValidationCallback = ServerCertificateCustomValidation + }); /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// + /// The text service. + /// The global settings. + /// The hosting environment. public HttpsCheck( ILocalizedTextService textService, IOptionsMonitor globalSettings, @@ -47,33 +56,22 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security _hostingEnvironment = hostingEnvironment; } - private static HttpClient HttpClient => s_httpClient ??= new HttpClient(HttpClientHandler); - - private static HttpClientHandler HttpClientHandler => s_httpClientHandler ??= new HttpClientHandler() - { - ServerCertificateCustomValidationCallback = ServerCertificateCustomValidation - }; - - /// - /// Get the status for this health check - /// + /// public override async Task> GetStatus() => await Task.WhenAll( CheckIfCurrentSchemeIsHttps(), CheckHttpsConfigurationSetting(), CheckForValidCertificate()); - /// - /// Executes the action and returns it's status - /// + /// public override HealthCheckStatus ExecuteAction(HealthCheckAction action) => throw new InvalidOperationException("HttpsCheck action requested is either not executable or does not exist"); private static bool ServerCertificateCustomValidation(HttpRequestMessage requestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslErrors) { - if (!(certificate is null) && s_certificateDaysToExpiry == default) + if (certificate is not null) { - s_certificateDaysToExpiry = (int)Math.Floor((certificate.NotAfter - DateTime.Now).TotalDays); + requestMessage.Properties[HttpPropertyKeyCertificateDaysToExpiry] = (int)Math.Floor((certificate.NotAfter - DateTime.Now).TotalDays); } return sslErrors == SslPolicyErrors.None; @@ -84,30 +82,37 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security string message; StatusResultType result; - // Attempt to access the site over HTTPS to see if it HTTPS is supported - // and a valid certificate has been configured - var url = _hostingEnvironment.ApplicationMainUrl.ToString().Replace("http:", "https:"); + // Attempt to access the site over HTTPS to see if it HTTPS is supported and a valid certificate has been configured + var urlBuilder = new UriBuilder(_hostingEnvironment.ApplicationMainUrl) + { + Scheme = Uri.UriSchemeHttps + }; + var url = urlBuilder.Uri; var request = new HttpRequestMessage(HttpMethod.Head, url); try { using HttpResponseMessage response = await HttpClient.SendAsync(request); + if (response.StatusCode == HttpStatusCode.OK) { - // Got a valid response, check now for if certificate expiring within 14 days - // Hat-tip: https://stackoverflow.com/a/15343898/489433 - const int numberOfDaysForExpiryWarning = 14; + // Got a valid response, check now if the certificate is expiring within the specified amount of days + int daysToExpiry = 0; + if (request.Properties.TryGetValue(HttpPropertyKeyCertificateDaysToExpiry, out var certificateDaysToExpiry)) + { + daysToExpiry = (int)certificateDaysToExpiry; + } - if (s_certificateDaysToExpiry <= 0) + if (daysToExpiry <= 0) { result = StatusResultType.Error; message = _textService.Localize("healthcheck","httpsCheckExpiredCertificate"); } - else if (s_certificateDaysToExpiry < numberOfDaysForExpiryWarning) + else if (daysToExpiry < NumberOfDaysForExpiryWarning) { result = StatusResultType.Warning; - message = _textService.Localize("healthcheck","httpsCheckExpiringCertificate", new[] { s_certificateDaysToExpiry.ToString() }); + message = _textService.Localize("healthcheck","httpsCheckExpiringCertificate", new[] { daysToExpiry.ToString() }); } else { @@ -118,21 +123,20 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security else { result = StatusResultType.Error; - message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url, response.ReasonPhrase }); + message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url.AbsoluteUri, response.ReasonPhrase }); } } catch (Exception ex) { - var exception = ex as WebException; - if (exception != null) + if (ex is WebException exception) { message = exception.Status == WebExceptionStatus.TrustFailure - ? _textService.Localize("healthcheck","httpsCheckInvalidCertificate", new[] { exception.Message }) - : _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url, exception.Message }); + ? _textService.Localize("healthcheck", "httpsCheckInvalidCertificate", new[] { exception.Message }) + : _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, exception.Message }); } else { - message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url, ex.Message }); + message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, ex.Message }); } result = StatusResultType.Error; @@ -150,7 +154,7 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security private Task CheckIfCurrentSchemeIsHttps() { Uri uri = _hostingEnvironment.ApplicationMainUrl; - var success = uri.Scheme == "https"; + var success = uri.Scheme == Uri.UriSchemeHttps; return Task.FromResult(new HealthCheckStatus(_textService.Localize("healthcheck","httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" })) { @@ -166,16 +170,14 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security string resultMessage; StatusResultType resultType; - if (uri.Scheme != "https") + if (uri.Scheme != Uri.UriSchemeHttps) { resultMessage = _textService.Localize("healthcheck","httpsCheckConfigurationRectifyNotPossible"); resultType = StatusResultType.Info; } else { - resultMessage = _textService.Localize( - "healthcheck","httpsCheckConfigurationCheckResult", - new[] { httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not" }); + resultMessage = _textService.Localize("healthcheck","httpsCheckConfigurationCheckResult", new[] { httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not" }); resultType = httpsSettingEnabled ? StatusResultType.Success : StatusResultType.Error; } From 09db4ed4a520e4727a722f9c813cee9282a23c94 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Wed, 15 Sep 2021 09:18:36 +0200 Subject: [PATCH 02/19] Show UI errors for invalid files in media upload (#11061) --- .../upload/umbfiledropzone.directive.js | 30 ++++++++++++------- .../components/upload/umb-file-dropzone.html | 10 +++---- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js index e926ca23d7..9350e04af9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js @@ -65,16 +65,15 @@ angular.module("umbraco.directives") function _filesQueued(files, event) { //Push into the queue Utilities.forEach(files, file => { + if (_filterFile(file) === true) { - if (_filterFile(file) === true) { - - if (file.$error) { - scope.rejected.push(file); - } else { - scope.queue.push(file); - } + if (file.$error) { + scope.rejected.push(file); + } else { + scope.queue.push(file); } - }); + } + }); //when queue is done, kick the uploader if (!scope.working) { @@ -173,6 +172,8 @@ angular.module("umbraco.directives") } } else if (evt.Message) { file.serverErrorMessage = evt.Message; + } else if (evt && typeof evt === 'string') { + file.serverErrorMessage = evt; } // If file not found, server will return a 404 and display this message if (status === 404) { @@ -247,11 +248,18 @@ angular.module("umbraco.directives") return true;// yes, we did open the choose-media dialog, therefor we return true. } - scope.handleFiles = function(files, event) { + scope.handleFiles = function(files, event, invalidFiles) { + const allFiles = [...files, ...invalidFiles]; + + // add unique key for each files to use in ng-repeats + allFiles.forEach(file => { + file.key = String.CreateGuid(); + }); + if (scope.filesQueued) { - scope.filesQueued(files, event); + scope.filesQueued(allFiles, event); } - _filesQueued(files, event); + _filesQueued(allFiles, event); }; } }; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html index e5466d1da0..ff0e8970a4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/upload/umb-file-dropzone.html @@ -6,7 +6,7 @@
@@ -43,7 +43,7 @@
    -
  • +
  • {{ file.name }}
    @@ -68,13 +68,13 @@
  • -
  • +
  • {{queued.name}}
  • -
  • +
  • From 9a3f77ed83927dafdfc52ce9fcc5bdd0d48843fe Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 15 Sep 2021 09:45:15 +0200 Subject: [PATCH 03/19] Added option to set NoNodesViewPath in umbraco template. --- .../.template.config/dotnetcli.host.json | 4 ++ .../.template.config/ide.host.json | 7 ++++ .../.template.config/template.json | 37 +++++++++++++++++++ .../appsettings.Development.json | 3 ++ 4 files changed, 51 insertions(+) diff --git a/build/templates/UmbracoProject/.template.config/dotnetcli.host.json b/build/templates/UmbracoProject/.template.config/dotnetcli.host.json index fb990c8902..d0548110fd 100644 --- a/build/templates/UmbracoProject/.template.config/dotnetcli.host.json +++ b/build/templates/UmbracoProject/.template.config/dotnetcli.host.json @@ -28,6 +28,10 @@ "ConnectionString":{ "longName": "connection-string", "shortName": "" + }, + "NoNodesViewPath":{ + "longName": "no-nodes-view-path", + "shortName": "" } }, "usageExamples": [ diff --git a/build/templates/UmbracoProject/.template.config/ide.host.json b/build/templates/UmbracoProject/.template.config/ide.host.json index af5ff5e24c..8b8d2608cd 100644 --- a/build/templates/UmbracoProject/.template.config/ide.host.json +++ b/build/templates/UmbracoProject/.template.config/ide.host.json @@ -55,6 +55,13 @@ "text": "Optional: Database connection string when using Unattended install" }, "isVisible": "true" + }, + { + "id": "NoNodesViewPath", + "name": { + "text": "Optional: Path to a custom view presented with the Umbraco installation contains no published content" + }, + "isVisible": "true" } ] } diff --git a/build/templates/UmbracoProject/.template.config/template.json b/build/templates/UmbracoProject/.template.config/template.json index 8414c5eeb3..fa92b73969 100644 --- a/build/templates/UmbracoProject/.template.config/template.json +++ b/build/templates/UmbracoProject/.template.config/template.json @@ -249,6 +249,43 @@ ] } }, + "NoNodesViewPath":{ + "type": "parameter", + "datatype":"text", + "description": "Path to a custom view presented with the Umbraco installation contains no published content", + "defaultValue": "", + }, + "NoNodesViewPathReplaced":{ + "type": "generated", + "generator": "regex", + "dataType": "string", + "replaces": "NO_NODES_VIEW_PATH_FROM_TEMPLATE", + "parameters": { + "source": "NoNodesViewPath", + "steps": [ + { + "regex": "\\\\", + "replacement": "\\\\" + }, + { + "regex": "\\\"", + "replacement": "\\\"" + }, + { + "regex": "\\\n", + "replacement": "\\\n" + }, + { + "regex": "\\\t", + "replacement": "\\\t" + } + ] + } + }, + "HasNoNodesViewPath":{ + "type": "computed", + "value": "(NoNodesViewPath != \"\")" + }, "UsingUnattenedInstall":{ "type": "computed", "value": "(FriendlyName != \"\" && Email != \"\" && Password != \"\" && ConnectionString != \"\")" diff --git a/build/templates/UmbracoProject/appsettings.Development.json b/build/templates/UmbracoProject/appsettings.Development.json index 59b6882a6b..e305e095ce 100644 --- a/build/templates/UmbracoProject/appsettings.Development.json +++ b/build/templates/UmbracoProject/appsettings.Development.json @@ -34,6 +34,9 @@ }, //#endif "Global": { + //#if (HasNoNodesViewPath) + "NoNodesViewPath": "NO_NODES_VIEW_PATH_FROM_TEMPLATE", + //#endif "Smtp": { "From": "your@email.here", "Host": "localhost", From ee42a7ebace6787d4e6fb9684b3d76e98017d242 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 15 Sep 2021 09:57:03 +0200 Subject: [PATCH 04/19] Add the correct year of open sourcing Umbraco on the license --- LICENSE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.md b/LICENSE.md index fa83dba963..b8bda9d7a3 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ # The MIT License (MIT) # -Copyright (c) 2013-present Umbraco +Copyright (c) 2005-present Umbraco Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: From d52fd92bbd8416a6f0ce71e0e28a7d1a2b39ab93 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 15 Sep 2021 10:21:32 +0200 Subject: [PATCH 05/19] Fixed JSON syntax error --- build/templates/UmbracoProject/.template.config/template.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build/templates/UmbracoProject/.template.config/template.json b/build/templates/UmbracoProject/.template.config/template.json index fa92b73969..1512ce597b 100644 --- a/build/templates/UmbracoProject/.template.config/template.json +++ b/build/templates/UmbracoProject/.template.config/template.json @@ -253,7 +253,7 @@ "type": "parameter", "datatype":"text", "description": "Path to a custom view presented with the Umbraco installation contains no published content", - "defaultValue": "", + "defaultValue": "" }, "NoNodesViewPathReplaced":{ "type": "generated", From b8a135626d34a7e7154ea7e881e2a15b787c7602 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 15 Sep 2021 11:19:12 +0200 Subject: [PATCH 06/19] Moved NoNodesViewPath destination from template to appSettings.json. --- build/templates/UmbracoProject/appsettings.Development.json | 3 --- build/templates/UmbracoProject/appsettings.json | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/build/templates/UmbracoProject/appsettings.Development.json b/build/templates/UmbracoProject/appsettings.Development.json index e305e095ce..59b6882a6b 100644 --- a/build/templates/UmbracoProject/appsettings.Development.json +++ b/build/templates/UmbracoProject/appsettings.Development.json @@ -34,9 +34,6 @@ }, //#endif "Global": { - //#if (HasNoNodesViewPath) - "NoNodesViewPath": "NO_NODES_VIEW_PATH_FROM_TEMPLATE", - //#endif "Smtp": { "From": "your@email.here", "Host": "localhost", diff --git a/build/templates/UmbracoProject/appsettings.json b/build/templates/UmbracoProject/appsettings.json index 4045341729..915671ce4b 100644 --- a/build/templates/UmbracoProject/appsettings.json +++ b/build/templates/UmbracoProject/appsettings.json @@ -15,6 +15,11 @@ }, "Umbraco": { "CMS": { + //#if (HasNoNodesViewPath) + "Global": { + "NoNodesViewPath": "NO_NODES_VIEW_PATH_FROM_TEMPLATE" + }, + //#endif "Hosting": { "Debug": false } From 465d73061678210b79072ad3207407fdba1ae64f Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 15 Sep 2021 11:55:53 +0100 Subject: [PATCH 07/19] Fixes issue with dotnet new template if you did not specify a connection string it was adding an empty one in appsettings.dev --- build/templates/UmbracoProject/.template.config/template.json | 4 ++++ build/templates/UmbracoProject/appsettings.Development.json | 2 ++ 2 files changed, 6 insertions(+) diff --git a/build/templates/UmbracoProject/.template.config/template.json b/build/templates/UmbracoProject/.template.config/template.json index 1512ce597b..3d4f197164 100644 --- a/build/templates/UmbracoProject/.template.config/template.json +++ b/build/templates/UmbracoProject/.template.config/template.json @@ -282,6 +282,10 @@ ] } }, + "HasConnectionString":{ + "type": "computed", + "value": "(ConnectionString != \"\")" + }, "HasNoNodesViewPath":{ "type": "computed", "value": "(NoNodesViewPath != \"\")" diff --git a/build/templates/UmbracoProject/appsettings.Development.json b/build/templates/UmbracoProject/appsettings.Development.json index 59b6882a6b..dad70ac5d8 100644 --- a/build/templates/UmbracoProject/appsettings.Development.json +++ b/build/templates/UmbracoProject/appsettings.Development.json @@ -17,9 +17,11 @@ } ] }, + //#if (HasConnectionString) "ConnectionStrings": { "umbracoDbDSN": "CONNECTION_FROM_TEMPLATE" }, + //#endif "Umbraco": { "CMS": { "Content": { From 2e14abc744608072903771c46e1dbe693661cd5d Mon Sep 17 00:00:00 2001 From: Zeegaan <70372949+Zeegaan@users.noreply.github.com> Date: Wed, 15 Sep 2021 13:48:32 +0200 Subject: [PATCH 08/19] Implemented tabs test --- .../cypress/integration/Content/urlpicker.ts | 73 +++ .../integration/DataTypes/dataTypes.ts | 97 ++++ .../cypress/integration/Tabs/tabs.ts | 537 ++++++++++++++++++ src/Umbraco.Tests.AcceptanceTest/package.json | 2 +- 4 files changed, 708 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/urlpicker.ts create mode 100644 src/Umbraco.Tests.AcceptanceTest/cypress/integration/DataTypes/dataTypes.ts create mode 100644 src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tabs/tabs.ts diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/urlpicker.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/urlpicker.ts new file mode 100644 index 0000000000..1baab4c948 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/urlpicker.ts @@ -0,0 +1,73 @@ +/// +import { + DocumentTypeBuilder, + ContentBuilder, + AliasHelper +} from 'umbraco-cypress-testhelpers'; + +context('Url Picker', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'), false); + }); + + it('Test Url Picker', () => { + + const urlPickerDocTypeName = 'Url Picker Test'; + const pickerDocTypeAlias = AliasHelper.toAlias(urlPickerDocTypeName); + cy.umbracoEnsureDocumentTypeNameNotExists(urlPickerDocTypeName); + cy.deleteAllContent(); + const pickerDocType = new DocumentTypeBuilder() + .withName(urlPickerDocTypeName) + .withAlias(pickerDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(pickerDocTypeAlias) + .addGroup() + .withName('ContentPickerGroup') + .addUrlPickerProperty() + .withAlias('picker') + .done() + .done() + .build(); + + cy.saveDocumentType(pickerDocType); + cy.editTemplate(urlPickerDocTypeName, '@inherits Umbraco.Web.Mvc.UmbracoViewPage' + + '\n@using ContentModels = Umbraco.Web.PublishedModels;' + + '\n@{' + + '\n Layout = null;' + + '\n}' + + '\n@foreach(var link in @Model.Picker)' + + '\n{' + + '\n @link.Name' + + '\n}'); + // Create content with url picker + cy.get('li .umb-tree-root:contains("Content")').should("be.visible").rightclick(); + cy.get('[data-element="action-create"]').click(); + cy.get('[data-element="action-create-' + pickerDocTypeAlias + '"] > .umb-action-link').click(); + // Fill out content + cy.umbracoEditorHeaderName('UrlPickerContent'); + cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('.umb-node-preview-add').click(); + // Should really try and find a better way to do this, but umbracoTreeItem tries to click the content pane in the background + cy.get('#treePicker li:first', {timeout: 6000}).click(); + cy.get('.umb-editor-footer-content__right-side > [button-style="success"] > .umb-button > .btn > .umb-button__content').click(); + cy.get('.umb-node-preview__name').should('be.visible'); + //Save and publish + cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); + cy.umbracoSuccessNotification().should('be.visible'); + //Waiting to ensure we have saved properly before leaving + cy.reload(); + //Assert + cy.get('.umb-notifications__notifications > .alert-error').should('not.exist'); + //Editing template with some content + + //Testing if the edits match the expected results + const expected = 'UrlPickerContent'; + cy.umbracoVerifyRenderedViewContent('/', expected, true).should('be.true'); + //clean + cy.umbracoEnsureDocumentTypeNameNotExists(urlPickerDocTypeName); + cy.umbracoEnsureTemplateNameNotExists(urlPickerDocTypeName); + + }); +}); \ No newline at end of file diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/DataTypes/dataTypes.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/DataTypes/dataTypes.ts new file mode 100644 index 0000000000..a8c2d8d151 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/DataTypes/dataTypes.ts @@ -0,0 +1,97 @@ +/// +import { + AliasHelper, + ApprovedColorPickerDataTypeBuilder, +} from 'umbraco-cypress-testhelpers'; + +context('DataTypes', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'), false); + }); + + it('Tests Approved Colors', () => { + cy.deleteAllContent(); + const name = 'Approved Colour Test'; + const alias = AliasHelper.toAlias(name); + + cy.umbracoEnsureDocumentTypeNameNotExists(name); + cy.umbracoEnsureDataTypeNameNotExists(name); + + const pickerDataType = new ApprovedColorPickerDataTypeBuilder() + .withName(name) + .withPrevalues(['000000', 'FF0000']) + .build() + + //umbracoMakeDocTypeWithDataTypeAndContent(name, alias, pickerDataType); + cy.umbracoCreateDocTypeWithContent(name, alias, pickerDataType); + + // Act + // Enter content + cy.umbracoRefreshContentTree(); + cy.umbracoTreeItem("content", [name]).click(); + //Pick a colour + cy.get('.btn-000000').click(); + //Save + cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); + cy.umbracoSuccessNotification().should('be.visible'); + //Editing template with some content + cy.editTemplate(name, '@inherits Umbraco.Web.Mvc.UmbracoViewPage' + + '\n@using ContentModels = Umbraco.Web.PublishedModels;' + + '\n@{' + + '\n Layout = null;' + + '\n}' + + '\n

    Lorem ipsum dolor sit amet

    '); + + //Assert + const expected = `

    Lorem ipsum dolor sit amet

    `; + cy.umbracoVerifyRenderedViewContent('/', expected, true).should('be.true'); + + //Pick another colour to verify both work + cy.get('.btn-FF0000').click(); + //Save + cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); + cy.umbracoSuccessNotification().should('be.visible'); + //Assert + const expected2 = '

    Lorem ipsum dolor sit amet

    '; + cy.umbracoVerifyRenderedViewContent('/', expected2, true).should('be.true'); + + //Clean + cy.umbracoEnsureDataTypeNameNotExists(name); + cy.umbracoEnsureDocumentTypeNameNotExists(name); + cy.umbracoEnsureTemplateNameNotExists(name); + }); + + // it('Tests Checkbox List', () => { + // const name = 'CheckBox List'; + // const alias = AliasHelper.toAlias(name); + + // cy.umbracoEnsureDocumentTypeNameNotExists(name); + // cy.umbracoEnsureDataTypeNameNotExists(name); + + // const pickerDataType = new CheckBoxListDataTypeBuilder() + // .withName(name) + // .withPrevalues(['Choice 1', 'Choice 2']) + // .build() + + // cy.umbracoCreateDocTypeWithContent(name, alias, pickerDataType); + // // Act + // // Enter content + // cy.umbracoRefreshContentTree(); + // cy.umbracoTreeItem("content", [name]).click(); + // //Check box 1 + // cy.get(':nth-child(1) > umb-checkbox.ng-isolate-scope > .checkbox > .umb-form-check__symbol > .umb-form-check__state > .umb-form-check__check') + // .click(); + // //Save + // cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); + // cy.umbracoSuccessNotification().should('be.visible'); + + // //Edit template with content + // cy.editTemplate(name, '@inherits Umbraco.Web.Mvc.UmbracoViewPage' + + // '\n@using ContentModels = Umbraco.Web.PublishedModels;' + + // '\n@{' + + // '\n Layout = null;' + + // '\n}' + + // '\n

    @Model.UmbracoTest

    '); + // }); +}); \ No newline at end of file diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tabs/tabs.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tabs/tabs.ts new file mode 100644 index 0000000000..3daf907c99 --- /dev/null +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Tabs/tabs.ts @@ -0,0 +1,537 @@ +/// +import { + DocumentTypeBuilder, + AliasHelper +} from 'umbraco-cypress-testhelpers'; + +const tabsDocTypeName = 'Tabs Test Document'; +const tabsDocTypeAlias = AliasHelper.toAlias(tabsDocTypeName); + +context('Tabs', () => { + + beforeEach(() => { + cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'), false); + }); + + afterEach(() => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName) + }); + + function OpenDocTypeFolder(){ + cy.umbracoSection('settings'); + cy.get('li .umb-tree-root:contains("Settings")').should("be.visible"); + cy.get('.umb-tree-item__inner > .umb-tree-item__arrow').eq(0).click(); + cy.get('.umb-tree-item__inner > .umb-tree-item__label').contains(tabsDocTypeName).click(); + } + + function CreateDocWithTabAndNavigate(){ + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withAlias("urlPicker") + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + } + + it('Create tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + cy.deleteAllContent(); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addGroup() + .withName('Tabs1Group') + .addUrlPickerProperty() + .withAlias('picker') + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + //Create a tab + cy.get('.umb-group-builder__tabs__add-tab').click(); + cy.get('ng-form.ng-invalid > .umb-group-builder__group-title-input').type('Tab 1'); + //Create a 2nd tab manually + cy.get('.umb-group-builder__tabs__add-tab').click(); + cy.get('ng-form.ng-invalid > .umb-group-builder__group-title-input').type('Tab 2'); + //Create a textstring property + cy.get('[aria-hidden="false"] > .umb-box-content > .umb-group-builder__group-add-property').click(); + cy.get('.editor-label').type('property name'); + cy.get('[data-element="editor-add"]').click(); + + //Search for textstring + cy.get('#datatype-search').type('Textstring'); + + // Choose first item + cy.get('[title="Textstring"]').closest("li").click(); + + // Save property + cy.get('.btn-success').last().click(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('[title="tab1"]').should('be.visible'); + cy.get('[title="tab2"]').should('be.visible'); + }); + + it('Delete tabs', () => { + CreateDocWithTabAndNavigate(); + //Check if tab is there, else if it wasnt created, this test would always pass + cy.get('[title="aTab 1"]').should('be.visible'); + //Delete a tab + cy.get('.btn-reset > .icon-trash').click(); + cy.get('.umb-button > .btn').last().click(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.get('[title="aTab 1"]').should('not.exist'); + //Clean + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + }); + + it('Delete property in tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withAlias("urlPicker") + .done() + .addContentPickerProperty() + .withAlias('picker') + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + cy.get('[aria-label="Delete property"]').last().click(); + cy.umbracoButtonByLabelKey('actions_delete').click(); + cy.umbracoButtonByLabelKey('buttons_save').click() + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('[title=urlPicker]').should('be.visible'); + cy.get('[title=picker]').should('not.exist'); + }); + + it('Delete group in tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withAlias("urlPicker") + .done() + .done() + .addGroup() + .withName('Content Picker Group') + .addContentPickerProperty() + .withAlias('picker') + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + //Delete group + cy.get('.umb-group-builder__group-remove > .icon-trash').eq(1).click(); + cy.umbracoButtonByLabelKey('actions_delete').click(); + cy.umbracoButtonByLabelKey('buttons_save').click() + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('[title=picker]').should('be.visible'); + cy.get('[title=urlPicker]').should('not.exist'); + }); + + it('Reorders tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group 1') + .addUrlPickerProperty() + .withLabel('Url picker 1') + .withAlias("urlPicker") + .done() + .done() + .done() + .addTab() + .withName('Tab 2') + .addGroup() + .withName('Tab group 2') + .addUrlPickerProperty() + .withLabel('Url picker 2') + .withAlias("pickerTab 2") + .done() + .done() + .done() + .addTab() + .withName('Tab 3') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withLabel('Url picker 3') + .withAlias('pickerTab3') + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + //Check if there are any tabs + cy.get('ng-form.ng-valid-required > .umb-group-builder__group-title-input').should('be.visible'); + cy.get('[alias="reorder"]').click(); + //Type order in + cy.get('.umb-group-builder__tab-sort-order > .umb-property-editor-tiny').first().type('3'); + cy.get('[alias="reorder"]').click(); + //Assert + cy.get('.umb-group-builder__group-title-input').eq(0).invoke('attr', 'title').should('eq', 'aTab 2') + cy.get('.umb-group-builder__group-title-input').eq(1).invoke('attr', 'title').should('eq', 'aTab 3') + cy.get('.umb-group-builder__group-title-input').eq(2).invoke('attr', 'title').should('eq', 'aTab 1') + }); + + it('Reorders groups in a tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group 1') + .addUrlPickerProperty() + .withLabel('Url picker 1') + .withAlias("urlPicker") + .done() + .done() + .addGroup() + .withName('Tab group 2') + .addUrlPickerProperty() + .withLabel('Url picker 2') + .withAlias('urlPickerTwo') + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + cy.get('[alias="reorder"]').click(); + cy.get('.umb-property-editor-tiny').eq(2).type('1'); + + cy.get('[alias="reorder"]').click(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('.umb-group-builder__group-title-input').eq(2) + .invoke('attr', 'title').should('eq', 'aTab 1/aTab group 2'); + }); + + it('Reorders properties in a tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withLabel('PickerOne') + .withAlias("urlPicker") + .done() + .addUrlPickerProperty() + .withLabel('PickerTwo') + .withAlias('urlPickerTwo') + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + //Reorder + cy.get('[alias="reorder"]').click(); + cy.get('.umb-group-builder__group-sort-value').first().type('2'); + cy.get('[alias="reorder"]').click(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('.umb-locked-field__input').last().invoke('attr', 'title').should('eq', 'urlPicker'); + }); + + it('Tab name cannot be empty', () => { + CreateDocWithTabAndNavigate(); + cy.get('.umb-group-builder__group-title-input').first().clear(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoErrorNotification().should('be.visible'); + }); + + it('Two tabs cannot have the same name', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withAlias("urlPicker") + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + //Create a 2nd tab manually + cy.get('.umb-group-builder__tabs__add-tab').click(); + cy.get('ng-form.ng-invalid > .umb-group-builder__group-title-input').type('Tab 1'); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoErrorNotification().should('be.visible'); + }); + + it('Group name cannot be empty', () => { + CreateDocWithTabAndNavigate(); + cy.get('.clearfix > .-placeholder').click(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoErrorNotification().should('be.visible'); + }); + + it('Group name cannot have the same name', () => { + CreateDocWithTabAndNavigate(); + cy.get('.clearfix > .-placeholder').click(); + cy.get('.umb-group-builder__group-title-input').last().type('Tab group'); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoErrorNotification().should('be.visible'); + }); + + it('Drag a group into another tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withAlias("urlPicker") + .done() + .done() + .done() + .addTab() + .withName('Tab 2') + .addGroup() + .withName('Tab group tab 2') + .addUrlPickerProperty() + .withAlias('urlPickerTabTwo') + .done() + .done() + .addGroup() + .withName('Tab group 2') + .addUrlPickerProperty() + .withAlias('urlPickerTwo') + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + cy.get('[alias="reorder"]').click(); + cy.get('.umb-group-builder__tabs-overflow--right > .caret').click().click(); + cy.get('.umb-group-builder__tab').last().click(); + cy.get('.umb-group-builder__group-title-icon').last().trigger('mousedown', { which: 1 }) + cy.get('.umb-group-builder__tab').eq(1).trigger('mousemove', {which: 1, force: true}); + cy.get('.umb-group-builder__tab').eq(1).should('have.class', 'is-active').trigger('mouseup', {force:true}); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('[title="aTab 1/aTab group 2"]').should('be.visible'); + }); + + it('Drag and drop reorders a tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withAlias("urlPicker") + .done() + .done() + .done() + .addTab() + .withName('Tab 2') + .addGroup() + .withName('Tab group tab 2') + .addUrlPickerProperty() + .withAlias('urlPickerTabTwo') + .done() + .done() + .addGroup() + .withName('Tab group 2') + .addUrlPickerProperty() + .withAlias('urlPickerTwo') + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + cy.get('[alias="reorder"]').click(); + //Scroll right so we can see tab 2 + cy.get('.umb-group-builder__tabs-overflow--right > .caret').click().click(); + cy.get('.umb-group-builder__tab-title-icon').eq(1).trigger('mousedown', { which: 1 }) + cy.get('.umb-group-builder__tab').eq(1).trigger('mousemove', {which: 1, force: true}); + cy.get('.umb-group-builder__tab').eq(1).should('have.class', 'is-active').trigger('mouseup', {force:true}); + cy.get('[alias="reorder"]').click(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('[title="aTab 2"]').should('be.visible'); + }); + + it('Drags and drops a property in a tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withAlias("urlPicker") + .withLabel('UrlPickerOne') + .done() + .done() + .done() + .addTab() + .withName('Tab 2') + .addGroup() + .withName('Tab group tab 2') + .addUrlPickerProperty() + .withAlias('urlPickerTabTwo') + .withLabel('UrlPickerTabTwo') + .done() + .addUrlPickerProperty() + .withAlias('urlPickerTwo') + .withLabel('UrlPickerTwo') + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + cy.get('[alias="reorder"]').click(); + //Scroll so we are sure we see tab 2 + cy.get('.umb-group-builder__tabs-overflow--right > .caret').click().click(); + //Navigate to tab 2 + cy.get('.umb-group-builder__tab').last().click(); + cy.get('.umb-group-builder__property-meta > .flex > .icon').eq(1).trigger('mousedown', {which: 1}) + cy.get('.umb-group-builder__tab').eq(1).trigger('mousemove', {which: 1, force: true}); + cy.get('.umb-group-builder__tab').eq(1).should('have.class', 'is-active'); + cy.get('.umb-group-builder__property') + .first().trigger('mousemove', {which: 1, force: true}).trigger('mouseup', {which: 1, force:true}); + cy.get('[alias="reorder"]').click(); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('[title="urlPickerTabTwo"]').should('be.visible'); + }); + + it('Drags and drops a group and converts to tab', () => { + cy.umbracoEnsureDocumentTypeNameNotExists(tabsDocTypeName); + const tabsDocType = new DocumentTypeBuilder() + .withName(tabsDocTypeName) + .withAlias(tabsDocTypeAlias) + .withAllowAsRoot(true) + .withDefaultTemplate(tabsDocTypeAlias) + .addTab() + .withName('Tab 1') + .addGroup() + .withName('Tab group') + .addUrlPickerProperty() + .withAlias("urlPicker") + .withLabel('UrlPickerOne') + .done() + .done() + .addGroup() + .withName('Tab group 2') + .addUrlPickerProperty() + .withAlias('urlPickerTwo') + .withLabel('UrlPickerTwo') + .done() + .done() + .done() + .addTab() + .withName('Tab 2') + .addGroup() + .withName('Tab group tab 2') + .addUrlPickerProperty() + .withAlias('urlPickerTabTwo') + .withLabel('UrlPickerTabTwo') + .done() + .done() + .done() + .build(); + cy.saveDocumentType(tabsDocType); + OpenDocTypeFolder(); + cy.get('[alias="reorder"]').click(); + cy.get('.umb-group-builder__group-title-icon').eq(1).trigger('mousedown', {which: 1}) + cy.get('.umb-group-builder__convert-dropzone').trigger('mousemove', {which: 1, force: true}); + cy.get('.umb-group-builder__convert-dropzone').trigger('mouseup', {which: 1, force:true}); + cy.umbracoButtonByLabelKey('buttons_save').click(); + //Assert + cy.umbracoSuccessNotification().should('be.visible'); + cy.get('[title="tabGroup"]').should('be.visible'); + }); +}); \ No newline at end of file diff --git a/src/Umbraco.Tests.AcceptanceTest/package.json b/src/Umbraco.Tests.AcceptanceTest/package.json index caf75638e6..05c8c51114 100644 --- a/src/Umbraco.Tests.AcceptanceTest/package.json +++ b/src/Umbraco.Tests.AcceptanceTest/package.json @@ -10,7 +10,7 @@ "cypress": "^6.8.0", "ncp": "^2.0.0", "prompt": "^1.0.0", - "umbraco-cypress-testhelpers": "^1.0.0-beta-53" + "umbraco-cypress-testhelpers": "^1.0.0-beta-57" }, "dependencies": { "typescript": "^3.9.2" From 35cfcfcc0c3bae775ec9e52371c07bd3be30d483 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 15 Sep 2021 14:54:27 +0200 Subject: [PATCH 09/19] Fixed acceptance tests to use v9 templates --- .../cypress/integration/Content/urlpicker.ts | 13 ++++++------- .../cypress/integration/DataTypes/dataTypes.ts | 13 ++++++------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/urlpicker.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/urlpicker.ts index 1baab4c948..c8db6380ac 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/urlpicker.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Content/urlpicker.ts @@ -12,9 +12,9 @@ context('Url Picker', () => { }); it('Test Url Picker', () => { - + const urlPickerDocTypeName = 'Url Picker Test'; - const pickerDocTypeAlias = AliasHelper.toAlias(urlPickerDocTypeName); + const pickerDocTypeAlias = AliasHelper.toAlias(urlPickerDocTypeName); cy.umbracoEnsureDocumentTypeNameNotExists(urlPickerDocTypeName); cy.deleteAllContent(); const pickerDocType = new DocumentTypeBuilder() @@ -31,8 +31,7 @@ context('Url Picker', () => { .build(); cy.saveDocumentType(pickerDocType); - cy.editTemplate(urlPickerDocTypeName, '@inherits Umbraco.Web.Mvc.UmbracoViewPage' + - '\n@using ContentModels = Umbraco.Web.PublishedModels;' + + cy.editTemplate(urlPickerDocTypeName, '@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage' + '\n@{' + '\n Layout = null;' + '\n}' + @@ -61,13 +60,13 @@ context('Url Picker', () => { //Assert cy.get('.umb-notifications__notifications > .alert-error').should('not.exist'); //Editing template with some content - + //Testing if the edits match the expected results const expected = 'UrlPickerContent'; - cy.umbracoVerifyRenderedViewContent('/', expected, true).should('be.true'); + cy.umbracoVerifyRenderedViewContent('/', expected, true).should('be.true'); //clean cy.umbracoEnsureDocumentTypeNameNotExists(urlPickerDocTypeName); cy.umbracoEnsureTemplateNameNotExists(urlPickerDocTypeName); }); -}); \ No newline at end of file +}); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/DataTypes/dataTypes.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/DataTypes/dataTypes.ts index a8c2d8d151..9e1e6cc185 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/DataTypes/dataTypes.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/DataTypes/dataTypes.ts @@ -32,12 +32,11 @@ context('DataTypes', () => { cy.umbracoTreeItem("content", [name]).click(); //Pick a colour cy.get('.btn-000000').click(); - //Save + //Save cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); cy.umbracoSuccessNotification().should('be.visible'); //Editing template with some content - cy.editTemplate(name, '@inherits Umbraco.Web.Mvc.UmbracoViewPage' + - '\n@using ContentModels = Umbraco.Web.PublishedModels;' + + cy.editTemplate(name, '@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage' + '\n@{' + '\n Layout = null;' + '\n}' + @@ -49,7 +48,7 @@ context('DataTypes', () => { //Pick another colour to verify both work cy.get('.btn-FF0000').click(); - //Save + //Save cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); cy.umbracoSuccessNotification().should('be.visible'); //Assert @@ -64,7 +63,7 @@ context('DataTypes', () => { // it('Tests Checkbox List', () => { // const name = 'CheckBox List'; - // const alias = AliasHelper.toAlias(name); + // const alias = AliasHelper.toAlias(name); // cy.umbracoEnsureDocumentTypeNameNotExists(name); // cy.umbracoEnsureDataTypeNameNotExists(name); @@ -85,7 +84,7 @@ context('DataTypes', () => { // //Save // cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click(); // cy.umbracoSuccessNotification().should('be.visible'); - + // //Edit template with content // cy.editTemplate(name, '@inherits Umbraco.Web.Mvc.UmbracoViewPage' + // '\n@using ContentModels = Umbraco.Web.PublishedModels;' + @@ -94,4 +93,4 @@ context('DataTypes', () => { // '\n}' + // '\n

    @Model.UmbracoTest

    '); // }); -}); \ No newline at end of file +}); From e056918e4f95a92d8aae323124f0ea486bc36216 Mon Sep 17 00:00:00 2001 From: BatJan <1932158+BatJan@users.noreply.github.com> Date: Wed, 15 Sep 2021 18:44:04 +0200 Subject: [PATCH 10/19] Ensure the expression is interpolated so the aria-selected attribute has a proper true/false value --- .../src/views/components/tabs/umb-tabs-nav.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html b/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html index b55555339b..df649a9e4a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tabs/umb-tabs-nav.html @@ -1,6 +1,6 @@
    • - @@ -23,7 +23,7 @@ ng-class="{'dropdown-menu--active': tab.active}" ng-click="vm.clickTab($event, tab)" role="tab" - aria-selected="{tab.active}" > + aria-selected="{{tab.active}}" > {{ tab.label }}
      !
      From 138ef428849dca952833fe278995e3974477ee66 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 16 Sep 2021 12:39:56 +0100 Subject: [PATCH 11/19] Adds new dotnet new template arg --use-https-redirect (#11115) --- .../.template.config/dotnetcli.host.json | 4 ++++ .../.template.config/ide.host.json | 7 +++++++ .../.template.config/template.json | 6 ++++++ .../templates/UmbracoProject/appsettings.json | 9 ++++++++- src/Umbraco.Web.UI/Startup.cs | 19 ++++++++++++------- 5 files changed, 37 insertions(+), 8 deletions(-) diff --git a/build/templates/UmbracoProject/.template.config/dotnetcli.host.json b/build/templates/UmbracoProject/.template.config/dotnetcli.host.json index d0548110fd..d357188ed7 100644 --- a/build/templates/UmbracoProject/.template.config/dotnetcli.host.json +++ b/build/templates/UmbracoProject/.template.config/dotnetcli.host.json @@ -32,6 +32,10 @@ "NoNodesViewPath":{ "longName": "no-nodes-view-path", "shortName": "" + }, + "UseHttpsRedirect": { + "longName": "use-https-redirect", + "shortName": "" } }, "usageExamples": [ diff --git a/build/templates/UmbracoProject/.template.config/ide.host.json b/build/templates/UmbracoProject/.template.config/ide.host.json index 8b8d2608cd..1ee7a492aa 100644 --- a/build/templates/UmbracoProject/.template.config/ide.host.json +++ b/build/templates/UmbracoProject/.template.config/ide.host.json @@ -62,6 +62,13 @@ "text": "Optional: Path to a custom view presented with the Umbraco installation contains no published content" }, "isVisible": "true" + }, + { + "id": "UseHttpsRedirect", + "name": { + "text": "Optional: Adds code to Startup.cs to redirect HTTP to HTTPS and enables the UseHttps setting." + }, + "isVisible": "true" } ] } diff --git a/build/templates/UmbracoProject/.template.config/template.json b/build/templates/UmbracoProject/.template.config/template.json index 3d4f197164..3cf7485ab2 100644 --- a/build/templates/UmbracoProject/.template.config/template.json +++ b/build/templates/UmbracoProject/.template.config/template.json @@ -293,6 +293,12 @@ "UsingUnattenedInstall":{ "type": "computed", "value": "(FriendlyName != \"\" && Email != \"\" && Password != \"\" && ConnectionString != \"\")" + }, + "UseHttpsRedirect":{ + "type": "parameter", + "datatype":"bool", + "defaultValue": "false", + "description": "Adds code to Startup.cs to redirect HTTP to HTTPS and enables the UseHttps setting (Default: false)" } } } diff --git a/build/templates/UmbracoProject/appsettings.json b/build/templates/UmbracoProject/appsettings.json index 915671ce4b..feb6b07d95 100644 --- a/build/templates/UmbracoProject/appsettings.json +++ b/build/templates/UmbracoProject/appsettings.json @@ -15,9 +15,16 @@ }, "Umbraco": { "CMS": { - //#if (HasNoNodesViewPath) + //#if (HasNoNodesViewPath || UseHttpsRedirect) "Global": { + //#if (!HasNoNodesViewPath && UseHttpsRedirect) + "UseHttps": true + //#elseif (UseHttpsRedirect) + "UseHttps": true, + //#endif + //#if (HasNoNodesViewPath) "NoNodesViewPath": "NO_NODES_VIEW_PATH_FROM_TEMPLATE" + //#endif }, //#endif "Hosting": { diff --git a/src/Umbraco.Web.UI/Startup.cs b/src/Umbraco.Web.UI/Startup.cs index 0d263c7b6b..71c3dd008c 100644 --- a/src/Umbraco.Web.UI/Startup.cs +++ b/src/Umbraco.Web.UI/Startup.cs @@ -15,10 +15,10 @@ namespace Umbraco.Cms.Web.UI private readonly IConfiguration _config; /// - /// Initializes a new instance of the class. + /// Initializes a new instance of the class. /// - /// The Web Host Environment - /// The Configuration + /// The web hosting environment. + /// The configuration. /// /// Only a few services are possible to be injected here https://github.com/dotnet/aspnetcore/issues/9337 /// @@ -28,11 +28,10 @@ namespace Umbraco.Cms.Web.UI _config = config ?? throw new ArgumentNullException(nameof(config)); } - - /// - /// Configures the services + /// Configures the services. /// + /// The services. /// /// This method gets called by the runtime. Use this method to add services to the container. /// For more information on how to configure your application, visit https://go.microsoft.com/fwlink/?LinkID=398940 @@ -50,8 +49,10 @@ namespace Umbraco.Cms.Web.UI } /// - /// Configures the application + /// Configures the application. /// + /// The application builder. + /// The web hosting environment. public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) @@ -59,6 +60,10 @@ namespace Umbraco.Cms.Web.UI app.UseDeveloperExceptionPage(); } +#if (UseHttpsRedirect) + app.UseHttpsRedirection(); + +#endif app.UseUmbraco() .WithMiddleware(u => { From 1baa4cf653839baa60021dd8824a95eb8e1e5d6b Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 16 Sep 2021 14:07:31 +0200 Subject: [PATCH 12/19] Add property group key to save model --- .../src/common/services/umbdataformatter.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index eeb5a59aef..226fabeae4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -75,7 +75,7 @@ }); saveModel.groups = _.map(realGroups, function (g) { - var saveGroup = _.pick(g, 'inherited', 'id', 'sortOrder', 'name', 'alias', 'type'); + var saveGroup = _.pick(g, 'inherited', 'id', 'sortOrder', 'name', 'key', 'alias', 'type'); var realProperties = _.reject(g.properties, function (p) { //do not include these properties From 4a1820b80d0ff5b86feadb6da9c3b9c7ffb5bd96 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 16 Sep 2021 14:07:31 +0200 Subject: [PATCH 13/19] Add property group key to save model --- .../src/common/services/umbdataformatter.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index e72900cacd..f6b24dd12b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -75,7 +75,7 @@ }); saveModel.groups = _.map(realGroups, function (g) { - var saveGroup = _.pick(g, 'inherited', 'id', 'sortOrder', 'name', 'alias', 'type'); + var saveGroup = _.pick(g, 'inherited', 'id', 'sortOrder', 'name', 'key', 'alias', 'type'); var realProperties = _.reject(g.properties, function (p) { //do not include these properties From c799d9e45c38b4bc1203d1f0a047290d035816e7 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 16 Sep 2021 21:59:52 +0200 Subject: [PATCH 14/19] https://github.com/umbraco/Umbraco-CMS/issues/11123 initializing the IVariationContextAccessor if the UmbracoRequestMiddleware --- .../Middleware/UmbracoRequestMiddleware.cs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs index fe0029bd80..539c1c844f 100644 --- a/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs +++ b/src/Umbraco.Web.Common/Middleware/UmbracoRequestMiddleware.cs @@ -13,10 +13,11 @@ using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; using Umbraco.Cms.Core.Logging; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Scoping; -using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.PublishedCache; @@ -50,6 +51,8 @@ namespace Umbraco.Cms.Web.Common.Middleware private readonly UmbracoRequestPaths _umbracoRequestPaths; private readonly BackOfficeWebAssets _backOfficeWebAssets; private readonly IRuntimeState _runtimeState; + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly IDefaultCultureAccessor _defaultCultureAccessor; private readonly SmidgeOptions _smidgeOptions; private readonly WebProfiler _profiler; @@ -77,7 +80,9 @@ namespace Umbraco.Cms.Web.Common.Middleware UmbracoRequestPaths umbracoRequestPaths, BackOfficeWebAssets backOfficeWebAssets, IOptions smidgeOptions, - IRuntimeState runtimeState) + IRuntimeState runtimeState, + IVariationContextAccessor variationContextAccessor, + IDefaultCultureAccessor defaultCultureAccessor) { _logger = logger; _umbracoContextFactory = umbracoContextFactory; @@ -88,6 +93,8 @@ namespace Umbraco.Cms.Web.Common.Middleware _umbracoRequestPaths = umbracoRequestPaths; _backOfficeWebAssets = backOfficeWebAssets; _runtimeState = runtimeState; + _variationContextAccessor = variationContextAccessor; + _defaultCultureAccessor = defaultCultureAccessor; _smidgeOptions = smidgeOptions.Value; _profiler = profiler as WebProfiler; // Ignore if not a WebProfiler } @@ -110,6 +117,7 @@ namespace Umbraco.Cms.Web.Common.Middleware EnsureContentCacheInitialized(); + _variationContextAccessor.VariationContext ??= new VariationContext(_defaultCultureAccessor.DefaultCulture); UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); Uri currentApplicationUrl = GetApplicationUrlFromCurrentRequest(context.Request); From 494c9f1b42d2eb9103a17374238230d6334596e7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 17 Sep 2021 10:29:24 -0600 Subject: [PATCH 15/19] UmbracoRouteValueTransformer fixes --- .../UmbracoBuilderExtensions.cs | 2 +- .../Routing/UmbracoRouteValueTransformer.cs | 56 +++++++++++++------ 2 files changed, 41 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index 62ec5a9921..744bd13388 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -38,7 +38,7 @@ namespace Umbraco.Extensions builder.Services.AddDataProtection(); builder.Services.AddAntiforgery(); - builder.Services.AddScoped(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index 41fd20a69b..9e0b71b61e 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -92,31 +92,41 @@ namespace Umbraco.Cms.Web.Website.Routing // If we aren't running, then we have nothing to route if (_runtime.Level != RuntimeLevel.Run) { - return values; + return null; } // will be null for any client side requests like JS, etc... - if (!_umbracoContextAccessor.TryGetUmbracoContext(out var umbracoContext)) + if (!_umbracoContextAccessor.TryGetUmbracoContext(out IUmbracoContext umbracoContext)) { - return values; + return null; } if (!_routableDocumentFilter.IsDocumentRequest(httpContext.Request.Path)) { - return values; + return null; + } + + // Don't execute if there are already UmbracoRouteValues assigned. + // This can occur if someone else is dynamically routing and in which case we don't want to overwrite + // the routing work being done there. + UmbracoRouteValues umbracoRouteValues = httpContext.Features.Get(); + if (umbracoRouteValues != null) + { + return null; } // Check if there is no existing content and return the no content controller if (!umbracoContext.Content.HasContent()) { - values[ControllerToken] = ControllerExtensions.GetControllerName(); - values[ActionToken] = nameof(RenderNoContentController.Index); - - return values; + return new RouteValueDictionary + { + [ControllerToken] = ControllerExtensions.GetControllerName(), + [ActionToken] = nameof(RenderNoContentController.Index) + }; } IPublishedRequest publishedRequest = await RouteRequestAsync(httpContext, umbracoContext); - UmbracoRouteValues umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest); + umbracoRouteValues = await _routeValuesFactory.CreateAsync(httpContext, publishedRequest); // Store the route values as a httpcontext feature httpContext.Features.Set(umbracoRouteValues); @@ -125,16 +135,27 @@ namespace Umbraco.Cms.Web.Website.Routing PostedDataProxyInfo postedInfo = GetFormInfo(httpContext, values); if (postedInfo != null) { - return HandlePostedValues(postedInfo, httpContext, values); + return HandlePostedValues(postedInfo, httpContext); } - values[ControllerToken] = umbracoRouteValues.ControllerName; + // See https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.dynamicroutevaluetransformer.transformasync?view=aspnetcore-5.0#Microsoft_AspNetCore_Mvc_Routing_DynamicRouteValueTransformer_TransformAsync_Microsoft_AspNetCore_Http_HttpContext_Microsoft_AspNetCore_Routing_RouteValueDictionary_ + // We should apparenlty not be modified these values. + // So we create new ones. + var newValues = new RouteValueDictionary + { + [ControllerToken] = umbracoRouteValues.ControllerName + }; if (string.IsNullOrWhiteSpace(umbracoRouteValues.ActionName) == false) { - values[ActionToken] = umbracoRouteValues.ActionName; + newValues[ActionToken] = umbracoRouteValues.ActionName; } - return values; + // NOTE: If we are never returning null it means that it is not possible for another + // DynamicRouteValueTransformer to execute to set the route values. This one will + // always win even if it is a 404 because we manage all 404s via Umbraco and 404 + // handlers. + + return newValues; } private async Task RouteRequestAsync(HttpContext httpContext, IUmbracoContext umbracoContext) @@ -196,11 +217,14 @@ namespace Umbraco.Cms.Web.Website.Routing }; } - private RouteValueDictionary HandlePostedValues(PostedDataProxyInfo postedInfo, HttpContext httpContext, RouteValueDictionary values) + private RouteValueDictionary HandlePostedValues(PostedDataProxyInfo postedInfo, HttpContext httpContext) { // set the standard route values/tokens - values[ControllerToken] = postedInfo.ControllerName; - values[ActionToken] = postedInfo.ActionName; + var values = new RouteValueDictionary + { + [ControllerToken] = postedInfo.ControllerName, + [ActionToken] = postedInfo.ActionName + }; ControllerActionDescriptor surfaceControllerDescriptor = _controllerActionSearcher.Find(httpContext, postedInfo.ControllerName, postedInfo.ActionName); From d27dc05f328c20f13fa3ff09e9850f6950e2ebb3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 17 Sep 2021 12:02:04 -0600 Subject: [PATCH 16/19] Return null from UmbracoRouteValueTransformer when there is no matches, use a custom IEndpointSelectorPolicy to deal with 404s. --- .../UmbracoBuilderExtensions.cs | 4 + .../Routing/NotFoundSelectorPolicy.cs | 79 +++++++++++++++++++ .../Routing/UmbracoRouteValueTransformer.cs | 16 ++-- 3 files changed, 94 insertions(+), 5 deletions(-) create mode 100644 src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs diff --git a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs index 744bd13388..72d8ec58ce 100644 --- a/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Website/DependencyInjection/UmbracoBuilderExtensions.cs @@ -1,5 +1,7 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Infrastructure.DependencyInjection; @@ -11,6 +13,7 @@ using Umbraco.Cms.Web.Website.Middleware; using Umbraco.Cms.Web.Website.Models; using Umbraco.Cms.Web.Website.Routing; using Umbraco.Cms.Web.Website.ViewEngines; +using static Microsoft.Extensions.DependencyInjection.ServiceDescriptor; namespace Umbraco.Extensions { @@ -40,6 +43,7 @@ namespace Umbraco.Extensions builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.TryAddEnumerable(Singleton()); builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs b/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs new file mode 100644 index 0000000000..589c424629 --- /dev/null +++ b/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs @@ -0,0 +1,79 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Controllers; +using Microsoft.AspNetCore.Routing; +using Microsoft.AspNetCore.Routing.Matching; +using Umbraco.Cms.Core.Routing; +using Umbraco.Cms.Web.Common.Controllers; +using Umbraco.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.Web.Website.Routing +{ + /// + /// Used to handle 404 routes that haven't been handled by the end user + /// + internal class NotFoundSelectorPolicy : MatcherPolicy, IEndpointSelectorPolicy + { + private readonly Lazy _notFound; + private readonly EndpointDataSource _endpointDataSource; + + public NotFoundSelectorPolicy(EndpointDataSource endpointDataSource) + { + _notFound = new Lazy(GetNotFoundEndpoint); + _endpointDataSource = endpointDataSource; + } + + // return the endpoint for the RenderController.Index action. + private Endpoint GetNotFoundEndpoint() + { + Endpoint e = _endpointDataSource.Endpoints.First(x => + { + // return the endpoint for the RenderController.Index action. + ControllerActionDescriptor descriptor = x.Metadata?.GetMetadata(); + return descriptor.ControllerTypeInfo == typeof(RenderController) + && descriptor.ActionName == nameof(RenderController.Index); + }); + return e; + } + + public override int Order => 0; + + 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) + { + if (candidates.Count == 1 && candidates[0].Values == null) + { + UmbracoRouteValues umbracoRouteValues = httpContext.Features.Get(); + if (umbracoRouteValues?.PublishedRequest != null + && !umbracoRouteValues.PublishedRequest.HasPublishedContent() + && umbracoRouteValues.PublishedRequest.ResponseStatusCode == StatusCodes.Status404NotFound) + { + // not found/404 + httpContext.SetEndpoint(_notFound.Value); + } + } + + return Task.CompletedTask; + } + } +} diff --git a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs index 9e0b71b61e..fc54350097 100644 --- a/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs +++ b/src/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformer.cs @@ -28,6 +28,7 @@ using RouteDirection = Umbraco.Cms.Core.Routing.RouteDirection; namespace Umbraco.Cms.Web.Website.Routing { + /// /// The route value transformer for Umbraco front-end routes /// @@ -138,6 +139,16 @@ namespace Umbraco.Cms.Web.Website.Routing return HandlePostedValues(postedInfo, httpContext); } + if (!umbracoRouteValues?.PublishedRequest?.HasPublishedContent() ?? false) + { + // No content was found, not by any registered 404 handlers and + // not by the IContentLastChanceFinder. In this case we want to return + // our default 404 page but we cannot return route values now because + // it's possible that a developer is handling dynamic routes too. + // Our 404 page will be handled with the NotFoundSelectorPolicy + return null; + } + // See https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.mvc.routing.dynamicroutevaluetransformer.transformasync?view=aspnetcore-5.0#Microsoft_AspNetCore_Mvc_Routing_DynamicRouteValueTransformer_TransformAsync_Microsoft_AspNetCore_Http_HttpContext_Microsoft_AspNetCore_Routing_RouteValueDictionary_ // We should apparenlty not be modified these values. // So we create new ones. @@ -150,11 +161,6 @@ namespace Umbraco.Cms.Web.Website.Routing newValues[ActionToken] = umbracoRouteValues.ActionName; } - // NOTE: If we are never returning null it means that it is not possible for another - // DynamicRouteValueTransformer to execute to set the route values. This one will - // always win even if it is a 404 because we manage all 404s via Umbraco and 404 - // handlers. - return newValues; } From 2ede06485cde6b28d06b66b377f5408bd3aae6ae Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 17 Sep 2021 12:47:41 -0600 Subject: [PATCH 17/19] fix/add tests --- .../UmbracoRouteValueTransformerTests.cs | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs index 47fcbd010c..d00f85fd0a 100644 --- a/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs +++ b/src/Umbraco.Tests.UnitTests/Umbraco.Web.Website/Routing/UmbracoRouteValueTransformerTests.cs @@ -14,6 +14,7 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; using Umbraco.Cms.Core.Routing; using Umbraco.Cms.Core.Services; @@ -92,28 +93,28 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Website.Routing => Mock.Of(x => x.RouteRequestAsync(It.IsAny(), It.IsAny()) == Task.FromResult(request)); [Test] - public async Task Noop_When_Runtime_Level_Not_Run() + public async Task Null_When_Runtime_Level_Not_Run() { UmbracoRouteValueTransformer transformer = GetTransformer( Mock.Of(), Mock.Of()); RouteValueDictionary result = await transformer.TransformAsync(new DefaultHttpContext(), new RouteValueDictionary()); - Assert.AreEqual(0, result.Count); + Assert.IsNull(result); } [Test] - public async Task Noop_When_No_Umbraco_Context() + public async Task Null_When_No_Umbraco_Context() { UmbracoRouteValueTransformer transformer = GetTransformerWithRunState( Mock.Of()); RouteValueDictionary result = await transformer.TransformAsync(new DefaultHttpContext(), new RouteValueDictionary()); - Assert.AreEqual(0, result.Count); + Assert.IsNull(result); } [Test] - public async Task Noop_When_Not_Document_Request() + public async Task Null_When_Not_Document_Request() { var umbracoContext = Mock.Of(); UmbracoRouteValueTransformer transformer = GetTransformerWithRunState( @@ -121,7 +122,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Website.Routing Mock.Of(x => x.IsDocumentRequest(It.IsAny()) == false)); RouteValueDictionary result = await transformer.TransformAsync(new DefaultHttpContext(), new RouteValueDictionary()); - Assert.AreEqual(0, result.Count); + Assert.IsNull(result); } [Test] @@ -173,10 +174,10 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Website.Routing } [Test] - public async Task Assigns_Values_To_RouteValueDictionary() + public async Task Assigns_Values_To_RouteValueDictionary_When_Content() { IUmbracoContext umbracoContext = GetUmbracoContext(true); - IPublishedRequest request = Mock.Of(); + IPublishedRequest request = Mock.Of(x => x.PublishedContent == Mock.Of()); UmbracoRouteValues routeValues = GetRouteValues(request); UmbracoRouteValueTransformer transformer = GetTransformerWithRunState( @@ -190,6 +191,23 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Website.Routing Assert.AreEqual(routeValues.ActionName, result[ActionToken]); } + [Test] + public async Task Returns_Null_RouteValueDictionary_When_No_Content() + { + IUmbracoContext umbracoContext = GetUmbracoContext(true); + IPublishedRequest request = Mock.Of(x => x.PublishedContent == null); + UmbracoRouteValues routeValues = GetRouteValues(request); + + UmbracoRouteValueTransformer transformer = GetTransformerWithRunState( + Mock.Of(x => x.TryGetUmbracoContext(out umbracoContext)), + router: GetRouter(request), + routeValuesFactory: GetRouteValuesFactory(request)); + + RouteValueDictionary result = await transformer.TransformAsync(new DefaultHttpContext(), new RouteValueDictionary()); + + Assert.IsNull(result); + } + private class TestController : RenderController { public TestController(ILogger logger, ICompositeViewEngine compositeViewEngine, IUmbracoContextAccessor umbracoContextAccessor) From 57bbbfa7d3104b46bb48d403147d8b26bc3fe34e Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 17 Sep 2021 13:35:43 -0600 Subject: [PATCH 18/19] better null checks --- src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs b/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs index 589c424629..e9370c94ce 100644 --- a/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs +++ b/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; @@ -34,8 +34,8 @@ namespace Umbraco.Cms.Web.Website.Routing { // return the endpoint for the RenderController.Index action. ControllerActionDescriptor descriptor = x.Metadata?.GetMetadata(); - return descriptor.ControllerTypeInfo == typeof(RenderController) - && descriptor.ActionName == nameof(RenderController.Index); + return descriptor?.ControllerTypeInfo == typeof(RenderController) + && descriptor?.ActionName == nameof(RenderController.Index); }); return e; } From 9306861d76a393801fe42a2f6cb0ed6b4dc81737 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 17 Sep 2021 13:49:12 -0600 Subject: [PATCH 19/19] Better candidate checking. --- .../Routing/NotFoundSelectorPolicy.cs | 22 ++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs b/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs index e9370c94ce..dbc06175fd 100644 --- a/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs +++ b/src/Umbraco.Web.Website/Routing/NotFoundSelectorPolicy.cs @@ -24,7 +24,7 @@ namespace Umbraco.Cms.Web.Website.Routing public NotFoundSelectorPolicy(EndpointDataSource endpointDataSource) { _notFound = new Lazy(GetNotFoundEndpoint); - _endpointDataSource = endpointDataSource; + _endpointDataSource = endpointDataSource; } // return the endpoint for the RenderController.Index action. @@ -46,7 +46,7 @@ namespace Umbraco.Cms.Web.Website.Routing { // Don't apply this filter to any endpoint group that is a controller route // i.e. only dynamic routes. - foreach(Endpoint endpoint in endpoints) + foreach (Endpoint endpoint in endpoints) { ControllerAttribute controller = endpoint.Metadata?.GetMetadata(); if (controller != null) @@ -61,7 +61,7 @@ namespace Umbraco.Cms.Web.Website.Routing public Task ApplyAsync(HttpContext httpContext, CandidateSet candidates) { - if (candidates.Count == 1 && candidates[0].Values == null) + if (AllInvalid(candidates)) { UmbracoRouteValues umbracoRouteValues = httpContext.Features.Get(); if (umbracoRouteValues?.PublishedRequest != null @@ -72,8 +72,20 @@ namespace Umbraco.Cms.Web.Website.Routing httpContext.SetEndpoint(_notFound.Value); } } - - return Task.CompletedTask; + + return Task.CompletedTask; + } + + private static bool AllInvalid(CandidateSet candidates) + { + for (int i = 0; i < candidates.Count; i++) + { + if (candidates.IsValidCandidate(i)) + { + return false; + } + } + return true; } } }