using System; using System.Collections.Generic; using System.Data.Common; using System.Linq; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Configuration; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Install; using Umbraco.Cms.Core.Install.Models; using Umbraco.Cms.Core.Migrations; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Migrations.Upgrade; using Umbraco.Cms.Infrastructure.Persistence; using Umbraco.Cms.Infrastructure.Persistence.Dtos; using Umbraco.Cms.Infrastructure.Scoping; using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Migrations.Install { /// /// Supports building and configuring the database. /// public class DatabaseBuilder { private readonly IUmbracoDatabaseFactory _databaseFactory; private readonly IScopeProvider _scopeProvider; private readonly IScopeAccessor _scopeAccessor; private readonly IRuntimeState _runtimeState; private readonly IKeyValueService _keyValueService; private readonly ILogger _logger; private readonly IDbProviderFactoryCreator _dbProviderFactoryCreator; private readonly IConfigManipulator _configManipulator; private readonly IOptionsMonitor _globalSettings; private readonly IOptionsMonitor _connectionStrings; private readonly IMigrationPlanExecutor _migrationPlanExecutor; private readonly DatabaseSchemaCreatorFactory _databaseSchemaCreatorFactory; private readonly IEnumerable _databaseProviderMetadata; private DatabaseSchemaResult? _databaseSchemaValidationResult; /// /// Initializes a new instance of the class. /// public DatabaseBuilder( IScopeProvider scopeProvider, IScopeAccessor scopeAccessor, IUmbracoDatabaseFactory databaseFactory, IRuntimeState runtimeState, ILoggerFactory loggerFactory, IKeyValueService keyValueService, IDbProviderFactoryCreator dbProviderFactoryCreator, IConfigManipulator configManipulator, IOptionsMonitor globalSettings, IOptionsMonitor connectionStrings, IMigrationPlanExecutor migrationPlanExecutor, DatabaseSchemaCreatorFactory databaseSchemaCreatorFactory, IEnumerable databaseProviderMetadata) { _scopeProvider = scopeProvider; _scopeAccessor = scopeAccessor; _databaseFactory = databaseFactory; _runtimeState = runtimeState; _logger = loggerFactory.CreateLogger(); _keyValueService = keyValueService; _dbProviderFactoryCreator = dbProviderFactoryCreator; _configManipulator = configManipulator; _globalSettings = globalSettings; _connectionStrings = connectionStrings; _migrationPlanExecutor = migrationPlanExecutor; _databaseSchemaCreatorFactory = databaseSchemaCreatorFactory; _databaseProviderMetadata = databaseProviderMetadata; } #region Status /// /// Gets a value indicating whether the database is configured. It does not necessarily /// mean that it is possible to connect, nor that Umbraco is installed, nor up-to-date. /// public bool IsDatabaseConfigured => _databaseFactory.Configured; /// /// Gets a value indicating whether it is possible to connect to the configured database. /// It does not necessarily mean that Umbraco is installed, nor up-to-date. /// public bool CanConnectToDatabase => _databaseFactory.CanConnect; /// /// Verifies whether a it is possible to connect to a database. /// public bool CanConnect(string? connectionString, string providerName) { DbProviderFactory? factory = _dbProviderFactoryCreator.CreateFactory(providerName); return DbConnectionExtensions.IsConnectionAvailable(connectionString, factory); } public bool HasSomeNonDefaultUser() { using (var scope = _scopeProvider.CreateScope()) { // look for the super user with default password var sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql() .SelectCount() .From() .Where(x => x.Id == Constants.Security.SuperUserId && x.Password == "default"); var result = _scopeAccessor.AmbientScope?.Database.ExecuteScalar(sql); var has = result != 1; if (has == false) { // found only 1 user == the default user with default password // however this always exists on uCloud, also need to check if there are other users too result = _scopeAccessor.AmbientScope?.Database.ExecuteScalar("SELECT COUNT(*) FROM umbracoUser"); has = result != 1; } scope.Complete(); return has; } } internal bool IsUmbracoInstalled() { using (var scope = _scopeProvider.CreateScope(autoComplete: true)) { return _scopeAccessor.AmbientScope?.Database.IsUmbracoInstalled() ?? false; } } #endregion #region Configure Connection String public bool ConfigureDatabaseConnection(DatabaseModel databaseSettings, bool isTrialRun) { IDatabaseProviderMetadata? providerMeta; // if the database model is null then we will attempt quick install. if (databaseSettings == null) { providerMeta = _databaseProviderMetadata .OrderBy(x => x.SortOrder) .Where(x => x.SupportsQuickInstall) .FirstOrDefault(x => x.IsAvailable); databaseSettings = new DatabaseModel { DatabaseName = providerMeta?.DefaultDatabaseName!, }; } else { providerMeta = _databaseProviderMetadata .FirstOrDefault(x => x.Id == databaseSettings.DatabaseProviderMetadataId); } if (providerMeta == null) { throw new InstallException("Unable to determine database provider configuration."); } var connectionString = providerMeta.GenerateConnectionString(databaseSettings); var providerName = databaseSettings.ProviderName ?? providerMeta.ProviderName; if (providerMeta.RequiresConnectionTest && !CanConnect(connectionString, providerName!)) { return false; } if (!isTrialRun) { _configManipulator.SaveConnectionString(connectionString!, providerName); Configure(connectionString!, providerName, _globalSettings.CurrentValue.InstallMissingDatabase || providerMeta.ForceCreateDatabase); } return true; } private void Configure(string connectionString, string? providerName, bool installMissingDatabase) { // Update existing connection string var umbracoConnectionString = _connectionStrings.Get(Core.Constants.System.UmbracoConnectionName); umbracoConnectionString.ConnectionString = connectionString; umbracoConnectionString.ProviderName = providerName; _databaseFactory.Configure(umbracoConnectionString); if (installMissingDatabase) { CreateDatabase(); } } #endregion #region Database Schema public void CreateDatabase() => _dbProviderFactoryCreator.CreateDatabase(_databaseFactory.ProviderName!, _databaseFactory.ConnectionString!); /// /// Validates the database schema. /// /// /// This assumes that the database exists and the connection string is /// configured and it is possible to connect to the database. /// public DatabaseSchemaResult? ValidateSchema() { using (var scope = _scopeProvider.CreateScope()) { var result = ValidateSchema(scope); scope.Complete(); return result; } } private DatabaseSchemaResult? ValidateSchema(IScope scope) { if (_databaseFactory.Initialized == false) return new DatabaseSchemaResult(); if (_databaseSchemaValidationResult != null) return _databaseSchemaValidationResult; _databaseSchemaValidationResult = _scopeAccessor.AmbientScope?.Database.ValidateSchema(); scope.Complete(); return _databaseSchemaValidationResult; } /// /// Creates the database schema and inserts initial data. /// /// /// This assumes that the database exists and the connection string is /// configured and it is possible to connect to the database. /// public Result? CreateSchemaAndData() { using (var scope = _scopeProvider.CreateScope()) { var result = CreateSchemaAndData(scope); scope.Complete(); return result; } } private Result? CreateSchemaAndData(IScope scope) { try { var readyForInstall = CheckReadyForInstall(); if (readyForInstall.Success == false) { return readyForInstall.Result; } _logger.LogInformation("Database configuration status: Started"); var database = _scopeAccessor.AmbientScope?.Database; var message = string.Empty; var schemaResult = ValidateSchema(); var hasInstalledVersion = schemaResult?.DetermineHasInstalledVersion() ?? false; //If the determined version is "empty" its a new install - otherwise upgrade the existing if (!hasInstalledVersion) { if (_runtimeState.Level == RuntimeLevel.Run) throw new Exception("Umbraco is already configured!"); var creator = _databaseSchemaCreatorFactory.Create(database); creator.InitializeDatabaseSchema(); message = message + "

Installation completed!

"; //now that everything is done, we need to determine the version of SQL server that is executing _logger.LogInformation("Database configuration status: {DbConfigStatus}", message); return new Result { Message = message, Success = true, Percentage = "100" }; } //we need to do an upgrade so return a new status message and it will need to be done during the next step _logger.LogInformation("Database requires upgrade"); message = "

Upgrading database, this may take some time...

"; return new Result { RequiresUpgrade = true, Message = message, Success = true, Percentage = "30" }; } catch (Exception ex) { return HandleInstallException(ex); } } /// /// Upgrades the database schema and data by running migrations. /// /// /// This assumes that the database exists and the connection string is /// configured and it is possible to connect to the database. /// Runs whichever migrations need to run. /// public Result? UpgradeSchemaAndData(UmbracoPlan plan) { try { var readyForInstall = CheckReadyForInstall(); if (readyForInstall.Success == false) { return readyForInstall.Result; } _logger.LogInformation("Database upgrade started"); // upgrade var upgrader = new Upgrader(plan); upgrader.Execute(_migrationPlanExecutor, _scopeProvider, _keyValueService); var message = "

Upgrade completed!

"; //now that everything is done, we need to determine the version of SQL server that is executing _logger.LogInformation("Database configuration status: {DbConfigStatus}", message); return new Result { Message = message, Success = true, Percentage = "100" }; } catch (Exception ex) { return HandleInstallException(ex); } } private Attempt CheckReadyForInstall() { if (_databaseFactory.CanConnect == false) { return Attempt.Fail(new Result { Message = "Database configuration is invalid. Please check that the entered database exists and" + " that the provided username and password has write access to the database.", Success = false, Percentage = "10" }); } return Attempt.Succeed(); } private Result HandleInstallException(Exception ex) { _logger.LogError(ex, "Database configuration failed"); if (_databaseSchemaValidationResult != null) { _logger.LogInformation("The database schema validation produced the following summary: {DbSchemaSummary}", _databaseSchemaValidationResult.GetSummary()); } return new Result { Message = "The database configuration failed with the following message: " + ex.Message + $"\n Please check log file for additional information (can be found in '{Constants.SystemDirectories.LogFiles}')", Success = false, Percentage = "90" }; } /// /// Represents the result of a database creation or upgrade. /// public class Result { /// /// Gets or sets a value indicating whether an upgrade is required. /// public bool RequiresUpgrade { get; set; } /// /// Gets or sets the message returned by the operation. /// public string? Message { get; set; } /// /// Gets or sets a value indicating whether the operation succeeded. /// public bool Success { get; set; } /// /// Gets or sets an install progress pseudo-percentage. /// public string? Percentage { get; set; } } #endregion } }