Skip to content

Commit 7131526

Browse files
authored
Merge branch 'main' into v17/bugfix/remove-not-found-static-item
2 parents 3faf1a3 + ca08652 commit 7131526

File tree

8 files changed

+245
-50
lines changed

8 files changed

+245
-50
lines changed

src/Umbraco.Cms.Api.Management/ViewModels/Installer/UserInstallRequestModel.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using System.ComponentModel;
1+
using System.ComponentModel;
22
using System.ComponentModel.DataAnnotations;
33

44
namespace Umbraco.Cms.Api.Management.ViewModels.Installer;
@@ -17,5 +17,5 @@ public class UserInstallRequestModel
1717
[PasswordPropertyText]
1818
public string Password { get; set; } = string.Empty;
1919

20-
public bool SubscribeToNewsletter { get; }
20+
public bool SubscribeToNewsletter { get; set; }
2121
}

src/Umbraco.Core/Services/MetricsConsentService.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,10 @@ public TelemetryLevel GetConsentLevel()
3939
return analyticsLevel;
4040
}
4141

42-
public async Task SetConsentLevelAsync(TelemetryLevel telemetryLevel)
42+
public Task SetConsentLevelAsync(TelemetryLevel telemetryLevel)
4343
{
44-
IUser? currentUser = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser ?? await _userService.GetAsync(Constants.Security.SuperUserKey);
45-
46-
_logger.LogInformation("Telemetry level set to {telemetryLevel} by {username}", telemetryLevel, currentUser?.Username);
44+
_logger.LogInformation("Telemetry level set to {telemetryLevel}", telemetryLevel);
4745
_keyValueService.SetValue(Key, telemetryLevel.ToString());
46+
return Task.CompletedTask;
4847
}
4948
}

src/Umbraco.Infrastructure/Installer/Steps/CreateUserStep.cs

Lines changed: 108 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
1-
using System.Collections.Specialized;
21
using System.Data.Common;
3-
using System.Text;
42
using Microsoft.AspNetCore.Identity;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Microsoft.Extensions.Logging;
55
using Microsoft.Extensions.Options;
66
using Umbraco.Cms.Core;
77
using Umbraco.Cms.Core.Configuration.Models;
8+
using Umbraco.Cms.Core.DependencyInjection;
89
using Umbraco.Cms.Core.Installer;
910
using Umbraco.Cms.Core.Models.Installer;
1011
using Umbraco.Cms.Core.Models.Membership;
@@ -32,7 +33,9 @@ public class CreateUserStep : StepBase, IInstallStep
3233
private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator;
3334
private readonly IMetricsConsentService _metricsConsentService;
3435
private readonly IJsonSerializer _jsonSerializer;
36+
private readonly ILogger<CreateUserStep> _logger;
3537

38+
[Obsolete("Please use the constructor that takes all parameters. Scheduled for removal in Umbraco 19.")]
3639
public CreateUserStep(
3740
IUserService userService,
3841
DatabaseBuilder databaseBuilder,
@@ -44,71 +47,132 @@ public CreateUserStep(
4447
IDbProviderFactoryCreator dbProviderFactoryCreator,
4548
IMetricsConsentService metricsConsentService,
4649
IJsonSerializer jsonSerializer)
50+
: this(
51+
userService,
52+
databaseBuilder,
53+
httpClientFactory,
54+
securitySettings,
55+
connectionStrings,
56+
cookieManager,
57+
userManager,
58+
dbProviderFactoryCreator,
59+
metricsConsentService,
60+
jsonSerializer,
61+
StaticServiceProvider.Instance.GetRequiredService<ILogger<CreateUserStep>>())
4762
{
48-
_userService = userService ?? throw new ArgumentNullException(nameof(userService));
49-
_databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder));
63+
}
64+
65+
public CreateUserStep(
66+
IUserService userService,
67+
DatabaseBuilder databaseBuilder,
68+
IHttpClientFactory httpClientFactory,
69+
IOptions<SecuritySettings> securitySettings,
70+
IOptionsMonitor<ConnectionStrings> connectionStrings,
71+
ICookieManager cookieManager,
72+
IBackOfficeUserManager userManager,
73+
IDbProviderFactoryCreator dbProviderFactoryCreator,
74+
IMetricsConsentService metricsConsentService,
75+
IJsonSerializer jsonSerializer,
76+
ILogger<CreateUserStep> logger)
77+
{
78+
_userService = userService;
79+
_databaseBuilder = databaseBuilder;
5080
_httpClientFactory = httpClientFactory;
51-
_securitySettings = securitySettings.Value ?? throw new ArgumentNullException(nameof(securitySettings));
81+
_securitySettings = securitySettings.Value;
5282
_connectionStrings = connectionStrings;
5383
_cookieManager = cookieManager;
54-
_userManager = userManager ?? throw new ArgumentNullException(nameof(userManager));
55-
_dbProviderFactoryCreator = dbProviderFactoryCreator ?? throw new ArgumentNullException(nameof(dbProviderFactoryCreator));
84+
_userManager = userManager;
85+
_dbProviderFactoryCreator = dbProviderFactoryCreator;
5686
_metricsConsentService = metricsConsentService;
5787
_jsonSerializer = jsonSerializer;
88+
_logger = logger;
5889
}
5990

6091
public async Task<Attempt<InstallationResult>> ExecuteAsync(InstallData model)
6192
{
62-
IUser? admin = _userService.GetAsync(Constants.Security.SuperUserKey).GetAwaiter().GetResult();
63-
if (admin is null)
64-
{
65-
return FailWithMessage("Could not find the super user");
66-
}
93+
IUser? admin = await _userService.GetAsync(Constants.Security.SuperUserKey);
94+
if (admin is null)
95+
{
96+
return FailWithMessage("Could not find the super user");
97+
}
6798

68-
UserInstallData user = model.User;
69-
admin.Email = user.Email.Trim();
70-
admin.Name = user.Name.Trim();
71-
admin.Username = user.Email.Trim();
99+
UserInstallData user = model.User;
100+
admin.Email = user.Email.Trim();
101+
admin.Name = user.Name.Trim();
102+
admin.Username = user.Email.Trim();
72103

73-
_userService.Save(admin);
104+
_userService.Save(admin);
74105

75-
BackOfficeIdentityUser? membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserIdAsString);
76-
if (membershipUser == null)
77-
{
78-
return FailWithMessage(
79-
$"No user found in membership provider with id of {Constants.Security.SuperUserIdAsString}.");
80-
}
106+
BackOfficeIdentityUser? membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserIdAsString);
107+
if (membershipUser == null)
108+
{
109+
return FailWithMessage(
110+
$"No user found in membership provider with id of {Constants.Security.SuperUserIdAsString}.");
111+
}
81112

82-
// To change the password here we actually need to reset it since we don't have an old one to use to change
83-
var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser);
84-
if (string.IsNullOrWhiteSpace(resetToken))
113+
// To change the password here we actually need to reset it since we don't have an old one to use to change
114+
var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser);
115+
if (string.IsNullOrWhiteSpace(resetToken))
116+
{
117+
return FailWithMessage("Could not reset password: unable to generate internal reset token");
118+
}
119+
120+
IdentityResult resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim());
121+
if (!resetResult.Succeeded)
122+
{
123+
return FailWithMessage("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage()));
124+
}
125+
126+
await _metricsConsentService.SetConsentLevelAsync(model.TelemetryLevel);
127+
128+
if (model.User.SubscribeToNewsletter)
129+
{
130+
const string EmailCollectorUrl = "https://emailcollector.umbraco.io/api/EmailProxy";
131+
132+
var emailModel = new EmailModel
133+
{
134+
Name = admin.Name,
135+
Email = admin.Email,
136+
UserGroup = [Constants.Security.AdminGroupAlias],
137+
};
138+
139+
HttpClient httpClient = _httpClientFactory.CreateClient();
140+
using var content = new StringContent(_jsonSerializer.Serialize(emailModel), System.Text.Encoding.UTF8, "application/json");
141+
try
85142
{
86-
return FailWithMessage("Could not reset password: unable to generate internal reset token");
143+
// Set a reasonable timeout of 5 seconds for web request to save subscriber.
144+
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
145+
HttpResponseMessage response = await httpClient.PostAsync(EmailCollectorUrl, content, cts.Token);
146+
if (response.IsSuccessStatusCode)
147+
{
148+
_logger.LogInformation("Successfully subscribed the user created on installation to the Umbraco newsletter.");
149+
}
150+
else
151+
{
152+
_logger.LogWarning("Failed to subscribe the user created on installation to the Umbraco newsletter. Status code: {StatusCode}", response.StatusCode);
153+
}
87154
}
88-
89-
IdentityResult resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim());
90-
if (!resetResult.Succeeded)
155+
catch (Exception ex)
91156
{
92-
return FailWithMessage("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage()));
157+
// Log and move on if a failure occurs, we don't want to block installation for this.
158+
_logger.LogError(ex, "Exception occurred while trying to subscribe the user created on installation to the Umbraco newsletter.");
93159
}
94160

95-
await _metricsConsentService.SetConsentLevelAsync(model.TelemetryLevel);
161+
}
96162

97-
if (model.User.SubscribeToNewsletter)
98-
{
99-
var values = new NameValueCollection { { "name", admin.Name }, { "email", admin.Email } };
100-
var content = new StringContent(_jsonSerializer.Serialize(values), Encoding.UTF8, "application/json");
163+
return Success();
164+
}
101165

102-
HttpClient httpClient = _httpClientFactory.CreateClient();
166+
/// <summary>
167+
/// Model used to subscribe to the newsletter. Aligns with EmailModel defined in Umbraco.EmailMarketing.
168+
/// </summary>
169+
private class EmailModel
170+
{
171+
public required string Name { get; init; }
103172

104-
try
105-
{
106-
HttpResponseMessage response = httpClient.PostAsync("https://shop.umbraco.com/base/Ecom/SubmitEmail/installer.aspx", content).Result;
107-
}
108-
catch { /* fail in silence */ }
109-
}
173+
public required string Email { get; init; }
110174

111-
return Success();
175+
public required List<string> UserGroup { get; init; }
112176
}
113177

114178
/// <inheritdoc/>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
export const manifests = [
2+
{
3+
type: 'propertyAction',
4+
kind: 'default',
5+
alias: 'My.propertyAction.Write',
6+
name: 'Write Property Action ',
7+
forPropertyEditorUis: ["Umb.PropertyEditorUi.TextBox"],
8+
api: () => import('./write-property-action.api.js'),
9+
weight: 200,
10+
meta: {
11+
icon: 'icon-brush',
12+
label: 'Write text',
13+
}
14+
},
15+
{
16+
type: 'propertyAction',
17+
kind: 'default',
18+
alias: 'My.propertyAction.Read',
19+
name: 'Read Property Action ',
20+
forPropertyEditorUis: ["Umb.PropertyEditorUi.TextBox"],
21+
api: () => import('./read-property-action.api.js'),
22+
weight: 200,
23+
meta: {
24+
icon: 'icon-eye',
25+
label: 'Read text',
26+
}
27+
}
28+
]
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { UmbPropertyActionBase } from '@umbraco-cms/backoffice/property-action';
2+
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
3+
import { UMB_NOTIFICATION_CONTEXT } from '@umbraco-cms/backoffice/notification';
4+
export class ReadPropertyAction extends UmbPropertyActionBase {
5+
async execute() {
6+
const propertyContext = await this.getContext(UMB_PROPERTY_CONTEXT);
7+
if (!propertyContext) {
8+
return;
9+
}
10+
const value = propertyContext.getValue();
11+
const notificationContext = await this.getContext(UMB_NOTIFICATION_CONTEXT);
12+
notificationContext?.peek('positive', {
13+
data: {
14+
headline: '',
15+
message: value,
16+
},
17+
});
18+
}
19+
}
20+
export { ReadPropertyAction as api };
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
{
2+
"name": "E2E Test Package",
3+
"version": "1.0.0",
4+
"extensions": [
5+
{
6+
"type": "bundle",
7+
"alias": "My.PropertyAction.Bundle",
8+
"name": "My property action bundle",
9+
"js": "/App_Plugins/my-property-action/my-property-action.manifests.js"
10+
}
11+
]
12+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { UmbPropertyActionBase } from '@umbraco-cms/backoffice/property-action';
2+
import { UMB_PROPERTY_CONTEXT } from '@umbraco-cms/backoffice/property';
3+
export class WritePropertyAction extends UmbPropertyActionBase {
4+
async execute() {
5+
const propertyContext = await this.getContext(UMB_PROPERTY_CONTEXT);
6+
if (!propertyContext) {
7+
return;
8+
}
9+
propertyContext.setValue('Hello world');
10+
11+
}
12+
}
13+
export { WritePropertyAction as api };
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import {ConstantHelper, test} from '@umbraco/playwright-testhelpers';
2+
import { expect } from '@playwright/test';
3+
4+
// Content
5+
const contentName = 'TestContent';
6+
// DocumentType
7+
const documentTypeName = 'TestDocumentTypeForContent';
8+
// GroupName
9+
const groupName = 'Content';
10+
// DataType
11+
const dataTypeName = 'Textstring';
12+
// Property actions name
13+
const writeActionName = 'Write text';
14+
const readActionName = 'Read text';
15+
// Test values
16+
const readTextValue = 'Test text value';
17+
const writeTextValue = 'Hello world';
18+
19+
test.afterEach(async ({umbracoApi}) => {
20+
await umbracoApi.document.ensureNameNotExists(contentName);
21+
await umbracoApi.documentType.ensureNameNotExists(documentTypeName);
22+
});
23+
24+
test('can read value from textstring editor using read property action', async ({umbracoApi, umbracoUi}) => {
25+
// Arrange
26+
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
27+
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id, groupName);
28+
await umbracoApi.document.createDocumentWithTextContent(contentName, documentTypeId, readTextValue, dataTypeName);
29+
await umbracoUi.goToBackOffice();
30+
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
31+
32+
// Act
33+
await umbracoUi.content.goToContentWithName(contentName);
34+
await umbracoUi.content.clickActionsMenuForProperty(groupName, dataTypeName);
35+
await umbracoUi.content.clickPropertyActionWithName(readActionName);
36+
37+
// Assert
38+
await umbracoUi.content.doesSuccessNotificationHaveText(readTextValue);
39+
});
40+
41+
test('can write value to textstring editor using write property action', async ({umbracoApi, umbracoUi}) => {
42+
// Arrange
43+
const dataTypeData = await umbracoApi.dataType.getByName(dataTypeName);
44+
const documentTypeId = await umbracoApi.documentType.createDocumentTypeWithPropertyEditor(documentTypeName, dataTypeName, dataTypeData.id, groupName);
45+
const contentId = await umbracoApi.document.createDocumentWithTextContent(contentName, documentTypeId, '', dataTypeName);
46+
await umbracoUi.goToBackOffice();
47+
await umbracoUi.content.goToSection(ConstantHelper.sections.content);
48+
49+
// Act
50+
await umbracoUi.content.goToContentWithName(contentName);
51+
await umbracoUi.content.clickActionsMenuForProperty(groupName, dataTypeName);
52+
await umbracoUi.content.clickPropertyActionWithName(writeActionName);
53+
await umbracoUi.content.clickSaveButton();
54+
55+
// Assert
56+
await umbracoUi.content.isSuccessStateVisibleForSaveButton();
57+
const updatedContentData = await umbracoApi.document.get(contentId);
58+
expect(updatedContentData.values[0].value).toBe(writeTextValue);
59+
});

0 commit comments

Comments
 (0)