using System.Data.Common; using Microsoft.AspNetCore.Identity; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Installer; using Umbraco.Cms.Core.Models.Installer; using Umbraco.Cms.Core.Models.Membership; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Web; using Umbraco.Cms.Infrastructure.Migrations.Install; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Extensions; using Constants = Umbraco.Cms.Core.Constants; using HttpResponseMessage = System.Net.Http.HttpResponseMessage; namespace Umbraco.Cms.Infrastructure.Installer.Steps; public class CreateUserStep : StepBase, IInstallStep { private readonly IUserService _userService; private readonly DatabaseBuilder _databaseBuilder; private readonly IHttpClientFactory _httpClientFactory; private readonly SecuritySettings _securitySettings; private readonly IOptionsMonitor _connectionStrings; private readonly ICookieManager _cookieManager; private readonly IBackOfficeUserManager _userManager; private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; private readonly IMetricsConsentService _metricsConsentService; private readonly IJsonSerializer _jsonSerializer; private readonly ILogger _logger; [Obsolete("Please use the constructor that takes all parameters. Scheduled for removal in Umbraco 19.")] public CreateUserStep( IUserService userService, DatabaseBuilder databaseBuilder, IHttpClientFactory httpClientFactory, IOptions securitySettings, IOptionsMonitor connectionStrings, ICookieManager cookieManager, IBackOfficeUserManager userManager, IDbProviderFactoryCreator dbProviderFactoryCreator, IMetricsConsentService metricsConsentService, IJsonSerializer jsonSerializer) : this( userService, databaseBuilder, httpClientFactory, securitySettings, connectionStrings, cookieManager, userManager, dbProviderFactoryCreator, metricsConsentService, jsonSerializer, StaticServiceProvider.Instance.GetRequiredService>()) { } public CreateUserStep( IUserService userService, DatabaseBuilder databaseBuilder, IHttpClientFactory httpClientFactory, IOptions securitySettings, IOptionsMonitor connectionStrings, ICookieManager cookieManager, IBackOfficeUserManager userManager, IDbProviderFactoryCreator dbProviderFactoryCreator, IMetricsConsentService metricsConsentService, IJsonSerializer jsonSerializer, ILogger logger) { _userService = userService; _databaseBuilder = databaseBuilder; _httpClientFactory = httpClientFactory; _securitySettings = securitySettings.Value; _connectionStrings = connectionStrings; _cookieManager = cookieManager; _userManager = userManager; _dbProviderFactoryCreator = dbProviderFactoryCreator; _metricsConsentService = metricsConsentService; _jsonSerializer = jsonSerializer; _logger = logger; } public async Task> ExecuteAsync(InstallData model) { IUser? admin = await _userService.GetAsync(Constants.Security.SuperUserKey); if (admin is null) { return FailWithMessage("Could not find the super user"); } UserInstallData user = model.User; admin.Email = user.Email.Trim(); admin.Name = user.Name.Trim(); admin.Username = user.Email.Trim(); _userService.Save(admin); BackOfficeIdentityUser? membershipUser = await _userManager.FindByIdAsync(Constants.Security.SuperUserIdAsString); if (membershipUser == null) { return FailWithMessage( $"No user found in membership provider with id of {Constants.Security.SuperUserIdAsString}."); } // To change the password here we actually need to reset it since we don't have an old one to use to change var resetToken = await _userManager.GeneratePasswordResetTokenAsync(membershipUser); if (string.IsNullOrWhiteSpace(resetToken)) { return FailWithMessage("Could not reset password: unable to generate internal reset token"); } IdentityResult resetResult = await _userManager.ChangePasswordWithResetAsync(membershipUser.Id, resetToken, user.Password.Trim()); if (!resetResult.Succeeded) { return FailWithMessage("Could not reset password: " + string.Join(", ", resetResult.Errors.ToErrorMessage())); } await _metricsConsentService.SetConsentLevelAsync(model.TelemetryLevel); if (model.User.SubscribeToNewsletter) { const string EmailCollectorUrl = "https://emailcollector.umbraco.io/api/EmailProxy"; var emailModel = new EmailModel { Name = admin.Name, Email = admin.Email, UserGroup = [Constants.Security.AdminGroupAlias], }; HttpClient httpClient = _httpClientFactory.CreateClient(); using var content = new StringContent(_jsonSerializer.Serialize(emailModel), System.Text.Encoding.UTF8, "application/json"); try { // Set a reasonable timeout of 5 seconds for web request to save subscriber. using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); HttpResponseMessage response = await httpClient.PostAsync(EmailCollectorUrl, content, cts.Token); if (response.IsSuccessStatusCode) { _logger.LogInformation("Successfully subscribed the user created on installation to the Umbraco newsletter."); } else { _logger.LogWarning("Failed to subscribe the user created on installation to the Umbraco newsletter. Status code: {StatusCode}", response.StatusCode); } } catch (Exception ex) { // Log and move on if a failure occurs, we don't want to block installation for this. _logger.LogError(ex, "Exception occurred while trying to subscribe the user created on installation to the Umbraco newsletter."); } } return Success(); } /// /// Model used to subscribe to the newsletter. Aligns with EmailModel defined in Umbraco.EmailMarketing. /// private class EmailModel { public required string Name { get; init; } public required string Email { get; init; } public required List UserGroup { get; init; } } /// public Task RequiresExecutionAsync(InstallData model) { InstallState installState = GetInstallState(); if (installState.HasFlag(InstallState.Unknown)) { // In this one case when it's a brand new install and nothing has been configured, make sure the // back office cookie is cleared so there's no old cookies lying around causing problems _cookieManager.ExpireCookie(_securitySettings.AuthCookieName); } var shouldRun = installState.HasFlag(InstallState.Unknown) || !installState.HasFlag(InstallState.HasNonDefaultUser); return Task.FromResult(shouldRun); } private InstallState GetInstallState() { InstallState installState = InstallState.Unknown; if (_databaseBuilder.IsDatabaseConfigured) { installState = (installState | InstallState.HasConnectionString) & ~InstallState.Unknown; } ConnectionStrings? umbracoConnectionString = _connectionStrings.CurrentValue; var isConnectionStringConfigured = umbracoConnectionString.IsConnectionStringConfigured(); if (isConnectionStringConfigured) { installState = (installState | InstallState.ConnectionStringConfigured) & ~InstallState.Unknown; } DbProviderFactory? factory = _dbProviderFactoryCreator.CreateFactory(umbracoConnectionString.ProviderName); var isConnectionAvailable = isConnectionStringConfigured && DbConnectionExtensions.IsConnectionAvailable(umbracoConnectionString.ConnectionString, factory); if (isConnectionAvailable) { installState = (installState | InstallState.CanConnect) & ~InstallState.Unknown; } var isUmbracoInstalled = isConnectionAvailable && _databaseBuilder.IsUmbracoInstalled(); if (isUmbracoInstalled) { installState = (installState | InstallState.UmbracoInstalled) & ~InstallState.Unknown; } var hasSomeNonDefaultUser = isUmbracoInstalled && _databaseBuilder.HasSomeNonDefaultUser(); if (hasSomeNonDefaultUser) { installState = (installState | InstallState.HasNonDefaultUser) & ~InstallState.Unknown; } return installState; } [Flags] private enum InstallState : short { // This is an easy way to avoid 0 enum assignment and not worry about // manual calcs. https://www.codeproject.com/Articles/396851/Ending-the-Great-Debate-on-Enum-Flags Unknown = 1, HasVersion = 1 << 1, HasConnectionString = 1 << 2, ConnectionStringConfigured = 1 << 3, CanConnect = 1 << 4, UmbracoInstalled = 1 << 5, HasNonDefaultUser = 1 << 6 } }