using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Threading.Tasks; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ModelBinding; using Newtonsoft.Json.Linq; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Migrations.Install; using Umbraco.Net; using Umbraco.Web.Common.Attributes; using Umbraco.Web.Common.Exceptions; using Umbraco.Web.Common.Filters; using Umbraco.Web.Common.ModelBinding; using Umbraco.Web.Common.Security; using Umbraco.Web.Install; using Umbraco.Web.Install.Models; namespace Umbraco.Web.Common.Install { using Constants = Umbraco.Core.Constants; [UmbracoApiController] [TypeFilter(typeof(HttpResponseExceptionFilter))] [TypeFilter(typeof(AngularJsonOnlyConfigurationAttribute))] [InstallAuthorize] [Area(Umbraco.Core.Constants.Web.Mvc.InstallArea)] public class InstallApiController : ControllerBase { private readonly DatabaseBuilder _databaseBuilder; private readonly InstallStatusTracker _installStatusTracker; private readonly IUmbracoApplicationLifetime _umbracoApplicationLifetime; private readonly BackOfficeSignInManager _backOfficeSignInManager; private readonly InstallStepCollection _installSteps; private readonly ILogger _logger; private readonly IProfilingLogger _proflog; public InstallApiController(DatabaseBuilder databaseBuilder, IProfilingLogger proflog, InstallHelper installHelper, InstallStepCollection installSteps, InstallStatusTracker installStatusTracker, IUmbracoApplicationLifetime umbracoApplicationLifetime, BackOfficeSignInManager backOfficeSignInManager) { _databaseBuilder = databaseBuilder ?? throw new ArgumentNullException(nameof(databaseBuilder)); _proflog = proflog ?? throw new ArgumentNullException(nameof(proflog)); _installSteps = installSteps; _installStatusTracker = installStatusTracker; _umbracoApplicationLifetime = umbracoApplicationLifetime; _backOfficeSignInManager = backOfficeSignInManager; InstallHelper = installHelper; _logger = _proflog; } internal InstallHelper InstallHelper { get; } public bool PostValidateDatabaseConnection(DatabaseModel model) { var canConnect = _databaseBuilder.CanConnect(model.DatabaseType.ToString(), model.ConnectionString, model.Server, model.DatabaseName, model.Login, model.Password, model.IntegratedAuth); return canConnect; } /// /// Gets the install setup. /// public InstallSetup GetSetup() { var setup = new InstallSetup(); // TODO: Check for user/site token var steps = new List(); var installSteps = _installSteps.GetStepsForCurrentInstallType().ToArray(); //only get the steps that are targeting the current install type steps.AddRange(installSteps); setup.Steps = steps; _installStatusTracker.Initialize(setup.InstallId, installSteps); return setup; } public IEnumerable GetPackages() { var starterKits = InstallHelper.GetStarterKits(); return starterKits; } [HttpPost] public async Task CompleteInstall() { // log the super user in if it's a new install var installType = InstallHelper.GetInstallationType(); if (installType == InstallationType.NewInstall) { var user = await _backOfficeSignInManager.UserManager.FindByIdAsync(Constants.Security.SuperUserId.ToString()); await _backOfficeSignInManager.SignInAsync(user, false); } _umbracoApplicationLifetime.Restart(); return NoContent(); } /// /// Installs. /// public async Task PostPerformInstall(InstallInstructions installModel) { if (installModel == null) throw new ArgumentNullException(nameof(installModel)); var status = InstallStatusTracker.GetStatus().ToArray(); //there won't be any statuses returned if the app pool has restarted so we need to re-read from file. if (status.Any() == false) { status = _installStatusTracker.InitializeFromFile(installModel.InstallId).ToArray(); } //create a new queue of the non-finished ones var queue = new Queue(status.Where(x => x.IsComplete == false)); while (queue.Count > 0) { var item = queue.Dequeue(); var step = _installSteps.GetAllSteps().Single(x => x.Name == item.Name); // if this step has any instructions then extract them var instruction = GetInstruction(installModel, item, step); // if this step doesn't require execution then continue to the next one, this is just a fail-safe check. if (StepRequiresExecution(step, instruction) == false) { // set this as complete and continue _installStatusTracker.SetComplete(installModel.InstallId, item.Name); continue; } try { var setupData = await ExecuteStepAsync(step, instruction); // update the status _installStatusTracker.SetComplete(installModel.InstallId, step.Name, setupData?.SavedStepData); // determine's the next step in the queue and dequeue's any items that don't need to execute var nextStep = IterateSteps(step, queue, installModel.InstallId, installModel); // check if there's a custom view to return for this step if (setupData != null && setupData.View.IsNullOrWhiteSpace() == false) { return new InstallProgressResultModel(false, step.Name, nextStep, setupData.View, setupData.ViewModel); } return new InstallProgressResultModel(false, step.Name, nextStep); } catch (Exception ex) { _logger.Error(ex, "An error occurred during installation step {Step}", step.Name); if (ex is TargetInvocationException && ex.InnerException != null) { ex = ex.InnerException; } var installException = ex as InstallException; if (installException != null) { throw HttpResponseException.CreateValidationErrorResponse(new { view = installException.View, model = installException.ViewModel, message = installException.Message }); } throw HttpResponseException.CreateValidationErrorResponse(new { step = step.Name, view = "error", message = ex.Message }); } } _installStatusTracker.Reset(); return new InstallProgressResultModel(true, "", ""); } private static object GetInstruction(InstallInstructions installModel, InstallTrackingItem item, InstallSetupStep step) { installModel.Instructions.TryGetValue(item.Name, out var instruction); // else null if (instruction is JObject jObject) { instruction = jObject?.ToObject(step.StepType); } return instruction; } /// /// We'll peek ahead and check if it's RequiresExecution is returning true. If it /// is not, we'll dequeue that step and peek ahead again (recurse) /// /// /// /// /// /// private string IterateSteps(InstallSetupStep current, Queue queue, Guid installId, InstallInstructions installModel) { while (queue.Count > 0) { var item = queue.Peek(); // if the current step restarts the app pool then we must simply return the next one in the queue, // we cannot peek ahead as the next step might rely on the app restart and therefore RequiresExecution // will rely on that too. if (current.PerformsAppRestart) return item.Name; var step = _installSteps.GetAllSteps().Single(x => x.Name == item.Name); // if this step has any instructions then extract them var instruction = GetInstruction(installModel, item, step); // if the step requires execution then return its name if (StepRequiresExecution(step, instruction)) return step.Name; // no longer requires execution, could be due to a new config change during installation // dequeue queue.Dequeue(); // complete _installStatusTracker.SetComplete(installId, step.Name); // and continue current = step; } return string.Empty; } // determines whether the step requires execution internal bool StepRequiresExecution(InstallSetupStep step, object instruction) { if (step == null) throw new ArgumentNullException(nameof(step)); var modelAttempt = instruction.TryConvertTo(step.StepType); if (!modelAttempt.Success) { throw new InvalidCastException( $"Cannot cast/convert {step.GetType().FullName} into {step.StepType.FullName}"); } var model = modelAttempt.Result; var genericStepType = typeof(InstallSetupStep<>); Type[] typeArgs = { step.StepType }; var typedStepType = genericStepType.MakeGenericType(typeArgs); try { var method = typedStepType.GetMethods().Single(x => x.Name == "RequiresExecution"); return (bool) method.Invoke(step, new[] { model }); } catch (Exception ex) { _logger.Error(ex, "Checking if step requires execution ({Step}) failed.", step.Name); throw; } } // executes the step internal async Task ExecuteStepAsync(InstallSetupStep step, object instruction) { using (_proflog.TraceDuration($"Executing installation step: '{step.Name}'.", "Step completed")) { var modelAttempt = instruction.TryConvertTo(step.StepType); if (!modelAttempt.Success) { throw new InvalidCastException( $"Cannot cast/convert {step.GetType().FullName} into {step.StepType.FullName}"); } var model = modelAttempt.Result; var genericStepType = typeof(InstallSetupStep<>); Type[] typeArgs = { step.StepType }; var typedStepType = genericStepType.MakeGenericType(typeArgs); try { var method = typedStepType.GetMethods().Single(x => x.Name == "ExecuteAsync"); var task = (Task) method.Invoke(step, new[] { model }); return await task; } catch (Exception ex) { _logger.Error(ex, "Installation step {Step} failed.", step.Name); throw; } } } } }