Skip to content

Commit c7fcb4a

Browse files
committed
Merge remote-tracking branch 'origin/v9/dev' into v9/bugfix/AB13489-no-error-when-exceeding-maxAllowedContentLength
# Conflicts: # src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js
2 parents 45a9a94 + 898f270 commit c7fcb4a

File tree

21 files changed

+1410
-464
lines changed

21 files changed

+1410
-464
lines changed

LICENSE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# The MIT License (MIT) #
22

3-
Copyright (c) 2013-present Umbraco
3+
Copyright (c) 2005-present Umbraco
44

55
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:
66

build/templates/UmbracoProject/.template.config/dotnetcli.host.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@
2828
"ConnectionString":{
2929
"longName": "connection-string",
3030
"shortName": ""
31+
},
32+
"NoNodesViewPath":{
33+
"longName": "no-nodes-view-path",
34+
"shortName": ""
35+
},
36+
"UseHttpsRedirect": {
37+
"longName": "use-https-redirect",
38+
"shortName": ""
3139
}
3240
},
3341
"usageExamples": [

build/templates/UmbracoProject/.template.config/ide.host.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,20 @@
5555
"text": "Optional: Database connection string when using Unattended install"
5656
},
5757
"isVisible": "true"
58+
},
59+
{
60+
"id": "NoNodesViewPath",
61+
"name": {
62+
"text": "Optional: Path to a custom view presented with the Umbraco installation contains no published content"
63+
},
64+
"isVisible": "true"
65+
},
66+
{
67+
"id": "UseHttpsRedirect",
68+
"name": {
69+
"text": "Optional: Adds code to Startup.cs to redirect HTTP to HTTPS and enables the UseHttps setting."
70+
},
71+
"isVisible": "true"
5872
}
5973
]
6074
}

build/templates/UmbracoProject/.template.config/template.json

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,9 +249,56 @@
249249
]
250250
}
251251
},
252+
"NoNodesViewPath":{
253+
"type": "parameter",
254+
"datatype":"text",
255+
"description": "Path to a custom view presented with the Umbraco installation contains no published content",
256+
"defaultValue": ""
257+
},
258+
"NoNodesViewPathReplaced":{
259+
"type": "generated",
260+
"generator": "regex",
261+
"dataType": "string",
262+
"replaces": "NO_NODES_VIEW_PATH_FROM_TEMPLATE",
263+
"parameters": {
264+
"source": "NoNodesViewPath",
265+
"steps": [
266+
{
267+
"regex": "\\\\",
268+
"replacement": "\\\\"
269+
},
270+
{
271+
"regex": "\\\"",
272+
"replacement": "\\\""
273+
},
274+
{
275+
"regex": "\\\n",
276+
"replacement": "\\\n"
277+
},
278+
{
279+
"regex": "\\\t",
280+
"replacement": "\\\t"
281+
}
282+
]
283+
}
284+
},
285+
"HasConnectionString":{
286+
"type": "computed",
287+
"value": "(ConnectionString != \"\")"
288+
},
289+
"HasNoNodesViewPath":{
290+
"type": "computed",
291+
"value": "(NoNodesViewPath != \"\")"
292+
},
252293
"UsingUnattenedInstall":{
253294
"type": "computed",
254295
"value": "(FriendlyName != \"\" && Email != \"\" && Password != \"\" && ConnectionString != \"\")"
296+
},
297+
"UseHttpsRedirect":{
298+
"type": "parameter",
299+
"datatype":"bool",
300+
"defaultValue": "false",
301+
"description": "Adds code to Startup.cs to redirect HTTP to HTTPS and enables the UseHttps setting (Default: false)"
255302
}
256303
}
257304
}

build/templates/UmbracoProject/appsettings.Development.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,11 @@
1717
}
1818
]
1919
},
20+
//#if (HasConnectionString)
2021
"ConnectionStrings": {
2122
"umbracoDbDSN": "CONNECTION_FROM_TEMPLATE"
2223
},
24+
//#endif
2325
"Umbraco": {
2426
"CMS": {
2527
"Content": {

build/templates/UmbracoProject/appsettings.json

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@
1515
},
1616
"Umbraco": {
1717
"CMS": {
18+
//#if (HasNoNodesViewPath || UseHttpsRedirect)
19+
"Global": {
20+
//#if (!HasNoNodesViewPath && UseHttpsRedirect)
21+
"UseHttps": true
22+
//#elseif (UseHttpsRedirect)
23+
"UseHttps": true,
24+
//#endif
25+
//#if (HasNoNodesViewPath)
26+
"NoNodesViewPath": "NO_NODES_VIEW_PATH_FROM_TEMPLATE"
27+
//#endif
28+
},
29+
//#endif
1830
"Hosting": {
1931
"Debug": false
2032
}

src/Umbraco.Core/HealthChecks/Checks/Security/HttpsCheck.cs

Lines changed: 42 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) Umbraco.
1+
// Copyright (c) Umbraco.
22
// See LICENSE for more details.
33

44
using System;
@@ -17,7 +17,7 @@
1717
namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
1818
{
1919
/// <summary>
20-
/// Health checks for the recommended production setup regarding https.
20+
/// Health checks for the recommended production setup regarding HTTPS.
2121
/// </summary>
2222
[HealthCheck(
2323
"EB66BB3B-1BCD-4314-9531-9DA2C1D6D9A7",
@@ -26,17 +26,26 @@ namespace Umbraco.Cms.Core.HealthChecks.Checks.Security
2626
Group = "Security")]
2727
public class HttpsCheck : HealthCheck
2828
{
29+
private const int NumberOfDaysForExpiryWarning = 14;
30+
private const string HttpPropertyKeyCertificateDaysToExpiry = "CertificateDaysToExpiry";
31+
2932
private readonly ILocalizedTextService _textService;
3033
private readonly IOptionsMonitor<GlobalSettings> _globalSettings;
3134
private readonly IHostingEnvironment _hostingEnvironment;
3235

3336
private static HttpClient s_httpClient;
34-
private static HttpClientHandler s_httpClientHandler;
35-
private static int s_certificateDaysToExpiry;
37+
38+
private static HttpClient HttpClient => s_httpClient ??= new HttpClient(new HttpClientHandler()
39+
{
40+
ServerCertificateCustomValidationCallback = ServerCertificateCustomValidation
41+
});
3642

3743
/// <summary>
38-
/// Initializes a new instance of the <see cref="HttpsCheck"/> class.
44+
/// Initializes a new instance of the <see cref="HttpsCheck" /> class.
3945
/// </summary>
46+
/// <param name="textService">The text service.</param>
47+
/// <param name="globalSettings">The global settings.</param>
48+
/// <param name="hostingEnvironment">The hosting environment.</param>
4049
public HttpsCheck(
4150
ILocalizedTextService textService,
4251
IOptionsMonitor<GlobalSettings> globalSettings,
@@ -47,33 +56,22 @@ public HttpsCheck(
4756
_hostingEnvironment = hostingEnvironment;
4857
}
4958

50-
private static HttpClient HttpClient => s_httpClient ??= new HttpClient(HttpClientHandler);
51-
52-
private static HttpClientHandler HttpClientHandler => s_httpClientHandler ??= new HttpClientHandler()
53-
{
54-
ServerCertificateCustomValidationCallback = ServerCertificateCustomValidation
55-
};
56-
57-
/// <summary>
58-
/// Get the status for this health check
59-
/// </summary>
59+
/// <inheritdoc />
6060
public override async Task<IEnumerable<HealthCheckStatus>> GetStatus() =>
6161
await Task.WhenAll(
6262
CheckIfCurrentSchemeIsHttps(),
6363
CheckHttpsConfigurationSetting(),
6464
CheckForValidCertificate());
6565

66-
/// <summary>
67-
/// Executes the action and returns it's status
68-
/// </summary>
66+
/// <inheritdoc />
6967
public override HealthCheckStatus ExecuteAction(HealthCheckAction action)
7068
=> throw new InvalidOperationException("HttpsCheck action requested is either not executable or does not exist");
7169

7270
private static bool ServerCertificateCustomValidation(HttpRequestMessage requestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors sslErrors)
7371
{
74-
if (!(certificate is null) && s_certificateDaysToExpiry == default)
72+
if (certificate is not null)
7573
{
76-
s_certificateDaysToExpiry = (int)Math.Floor((certificate.NotAfter - DateTime.Now).TotalDays);
74+
requestMessage.Properties[HttpPropertyKeyCertificateDaysToExpiry] = (int)Math.Floor((certificate.NotAfter - DateTime.Now).TotalDays);
7775
}
7876

7977
return sslErrors == SslPolicyErrors.None;
@@ -84,30 +82,37 @@ private async Task<HealthCheckStatus> CheckForValidCertificate()
8482
string message;
8583
StatusResultType result;
8684

87-
// Attempt to access the site over HTTPS to see if it HTTPS is supported
88-
// and a valid certificate has been configured
89-
var url = _hostingEnvironment.ApplicationMainUrl.ToString().Replace("http:", "https:");
85+
// Attempt to access the site over HTTPS to see if it HTTPS is supported and a valid certificate has been configured
86+
var urlBuilder = new UriBuilder(_hostingEnvironment.ApplicationMainUrl)
87+
{
88+
Scheme = Uri.UriSchemeHttps
89+
};
90+
var url = urlBuilder.Uri;
9091

9192
var request = new HttpRequestMessage(HttpMethod.Head, url);
9293

9394
try
9495
{
9596
using HttpResponseMessage response = await HttpClient.SendAsync(request);
97+
9698
if (response.StatusCode == HttpStatusCode.OK)
9799
{
98-
// Got a valid response, check now for if certificate expiring within 14 days
99-
// Hat-tip: https://stackoverflow.com/a/15343898/489433
100-
const int numberOfDaysForExpiryWarning = 14;
100+
// Got a valid response, check now if the certificate is expiring within the specified amount of days
101+
int daysToExpiry = 0;
102+
if (request.Properties.TryGetValue(HttpPropertyKeyCertificateDaysToExpiry, out var certificateDaysToExpiry))
103+
{
104+
daysToExpiry = (int)certificateDaysToExpiry;
105+
}
101106

102-
if (s_certificateDaysToExpiry <= 0)
107+
if (daysToExpiry <= 0)
103108
{
104109
result = StatusResultType.Error;
105110
message = _textService.Localize("healthcheck","httpsCheckExpiredCertificate");
106111
}
107-
else if (s_certificateDaysToExpiry < numberOfDaysForExpiryWarning)
112+
else if (daysToExpiry < NumberOfDaysForExpiryWarning)
108113
{
109114
result = StatusResultType.Warning;
110-
message = _textService.Localize("healthcheck","httpsCheckExpiringCertificate", new[] { s_certificateDaysToExpiry.ToString() });
115+
message = _textService.Localize("healthcheck","httpsCheckExpiringCertificate", new[] { daysToExpiry.ToString() });
111116
}
112117
else
113118
{
@@ -118,21 +123,20 @@ private async Task<HealthCheckStatus> CheckForValidCertificate()
118123
else
119124
{
120125
result = StatusResultType.Error;
121-
message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url, response.ReasonPhrase });
126+
message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url.AbsoluteUri, response.ReasonPhrase });
122127
}
123128
}
124129
catch (Exception ex)
125130
{
126-
var exception = ex as WebException;
127-
if (exception != null)
131+
if (ex is WebException exception)
128132
{
129133
message = exception.Status == WebExceptionStatus.TrustFailure
130-
? _textService.Localize("healthcheck","httpsCheckInvalidCertificate", new[] { exception.Message })
131-
: _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url, exception.Message });
134+
? _textService.Localize("healthcheck", "httpsCheckInvalidCertificate", new[] { exception.Message })
135+
: _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, exception.Message });
132136
}
133137
else
134138
{
135-
message = _textService.Localize("healthcheck","healthCheckInvalidUrl", new[] { url, ex.Message });
139+
message = _textService.Localize("healthcheck", "healthCheckInvalidUrl", new[] { url.AbsoluteUri, ex.Message });
136140
}
137141

138142
result = StatusResultType.Error;
@@ -150,7 +154,7 @@ private async Task<HealthCheckStatus> CheckForValidCertificate()
150154
private Task<HealthCheckStatus> CheckIfCurrentSchemeIsHttps()
151155
{
152156
Uri uri = _hostingEnvironment.ApplicationMainUrl;
153-
var success = uri.Scheme == "https";
157+
var success = uri.Scheme == Uri.UriSchemeHttps;
154158

155159
return Task.FromResult(new HealthCheckStatus(_textService.Localize("healthcheck","httpsCheckIsCurrentSchemeHttps", new[] { success ? string.Empty : "not" }))
156160
{
@@ -166,16 +170,14 @@ private Task<HealthCheckStatus> CheckHttpsConfigurationSetting()
166170

167171
string resultMessage;
168172
StatusResultType resultType;
169-
if (uri.Scheme != "https")
173+
if (uri.Scheme != Uri.UriSchemeHttps)
170174
{
171175
resultMessage = _textService.Localize("healthcheck","httpsCheckConfigurationRectifyNotPossible");
172176
resultType = StatusResultType.Info;
173177
}
174178
else
175179
{
176-
resultMessage = _textService.Localize(
177-
"healthcheck","httpsCheckConfigurationCheckResult",
178-
new[] { httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not" });
180+
resultMessage = _textService.Localize("healthcheck","httpsCheckConfigurationCheckResult", new[] { httpsSettingEnabled.ToString(), httpsSettingEnabled ? string.Empty : "not" });
179181
resultType = httpsSettingEnabled ? StatusResultType.Success : StatusResultType.Error;
180182
}
181183

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/// <reference types="Cypress" />
2+
import {
3+
DocumentTypeBuilder,
4+
ContentBuilder,
5+
AliasHelper
6+
} from 'umbraco-cypress-testhelpers';
7+
8+
context('Url Picker', () => {
9+
10+
beforeEach(() => {
11+
cy.umbracoLogin(Cypress.env('username'), Cypress.env('password'), false);
12+
});
13+
14+
it('Test Url Picker', () => {
15+
16+
const urlPickerDocTypeName = 'Url Picker Test';
17+
const pickerDocTypeAlias = AliasHelper.toAlias(urlPickerDocTypeName);
18+
cy.umbracoEnsureDocumentTypeNameNotExists(urlPickerDocTypeName);
19+
cy.deleteAllContent();
20+
const pickerDocType = new DocumentTypeBuilder()
21+
.withName(urlPickerDocTypeName)
22+
.withAlias(pickerDocTypeAlias)
23+
.withAllowAsRoot(true)
24+
.withDefaultTemplate(pickerDocTypeAlias)
25+
.addGroup()
26+
.withName('ContentPickerGroup')
27+
.addUrlPickerProperty()
28+
.withAlias('picker')
29+
.done()
30+
.done()
31+
.build();
32+
33+
cy.saveDocumentType(pickerDocType);
34+
cy.editTemplate(urlPickerDocTypeName, '@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<UrlPickerTest>' +
35+
'\n@{' +
36+
'\n Layout = null;' +
37+
'\n}' +
38+
'\n@foreach(var link in @Model.Picker)' +
39+
'\n{' +
40+
'\n <a href="@link.Url">@link.Name</a>' +
41+
'\n}');
42+
// Create content with url picker
43+
cy.get('li .umb-tree-root:contains("Content")').should("be.visible").rightclick();
44+
cy.get('[data-element="action-create"]').click();
45+
cy.get('[data-element="action-create-' + pickerDocTypeAlias + '"] > .umb-action-link').click();
46+
// Fill out content
47+
cy.umbracoEditorHeaderName('UrlPickerContent');
48+
cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click();
49+
cy.umbracoSuccessNotification().should('be.visible');
50+
cy.get('.umb-node-preview-add').click();
51+
// Should really try and find a better way to do this, but umbracoTreeItem tries to click the content pane in the background
52+
cy.get('#treePicker li:first', {timeout: 6000}).click();
53+
cy.get('.umb-editor-footer-content__right-side > [button-style="success"] > .umb-button > .btn > .umb-button__content').click();
54+
cy.get('.umb-node-preview__name').should('be.visible');
55+
//Save and publish
56+
cy.umbracoButtonByLabelKey('buttons_saveAndPublish').click();
57+
cy.umbracoSuccessNotification().should('be.visible');
58+
//Waiting to ensure we have saved properly before leaving
59+
cy.reload();
60+
//Assert
61+
cy.get('.umb-notifications__notifications > .alert-error').should('not.exist');
62+
//Editing template with some content
63+
64+
//Testing if the edits match the expected results
65+
const expected = '<a href="/">UrlPickerContent</a>';
66+
cy.umbracoVerifyRenderedViewContent('/', expected, true).should('be.true');
67+
//clean
68+
cy.umbracoEnsureDocumentTypeNameNotExists(urlPickerDocTypeName);
69+
cy.umbracoEnsureTemplateNameNotExists(urlPickerDocTypeName);
70+
71+
});
72+
});

0 commit comments

Comments
 (0)