diff --git a/src/Orchard.Web/Modules/Orchard.ImportExport/Module.txt b/src/Orchard.Web/Modules/Orchard.ImportExport/Module.txt index 283ec6619..3b6960348 100644 --- a/src/Orchard.Web/Modules/Orchard.ImportExport/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.ImportExport/Module.txt @@ -11,9 +11,4 @@ Features: Name: Import Export Description: Imports and exports content item data. Category: Content - Dependencies: Orchard.jQuery, Orchard.Recipes - Orchard.ImportExport.ResetSite - Name: Reset Site Action - Description: Adds a Reset Site action to the Import screen. - Category: Hosting - Dependencies: Orchard.ImportExport, Orchard.MultiTenancy \ No newline at end of file + Dependencies: Orchard.jQuery, Orchard.Recipes \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.ImportExport/Orchard.ImportExport.csproj b/src/Orchard.Web/Modules/Orchard.ImportExport/Orchard.ImportExport.csproj index 029ea8a1d..aee8aa047 100644 --- a/src/Orchard.Web/Modules/Orchard.ImportExport/Orchard.ImportExport.csproj +++ b/src/Orchard.Web/Modules/Orchard.ImportExport/Orchard.ImportExport.csproj @@ -48,7 +48,15 @@ false + + False + ..\..\..\..\lib\autofac\Autofac.dll + + + False + ..\..\..\..\lib\nhibernate\NHibernate.dll + @@ -72,8 +80,10 @@ - - + + + + @@ -149,9 +159,6 @@ - - - 10.0 $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) diff --git a/src/Orchard.Web/Modules/Orchard.ImportExport/Providers/ImportActions/ResetSiteAction.cs b/src/Orchard.Web/Modules/Orchard.ImportExport/Providers/ImportActions/ResetSiteAction.cs deleted file mode 100644 index c8c2b316f..000000000 --- a/src/Orchard.Web/Modules/Orchard.ImportExport/Providers/ImportActions/ResetSiteAction.cs +++ /dev/null @@ -1,51 +0,0 @@ -using System.Web.Mvc; -using Orchard.ContentManagement; -using Orchard.Environment.Configuration; -using Orchard.Environment.Extensions; -using Orchard.ImportExport.Services; -using Orchard.ImportExport.ViewModels; -using Orchard.MultiTenancy.Services; - -namespace Orchard.ImportExport.Providers.ImportActions { - [OrchardFeature("Orchard.ImportExport.ResetSite")] - public class ResetSiteAction : ImportAction { - private readonly ITenantService _tenantService; - private readonly ShellSettings _shellSettings; - - public ResetSiteAction(ITenantService tenantService, ShellSettings shellSettings) { - _tenantService = tenantService; - _shellSettings = shellSettings; - } - - public override string Name { get { return "ResetSite"; } } - - public override int Priority { - get { return 500; } - } - - public override dynamic BuildEditor(dynamic shapeFactory) { - return UpdateEditor(shapeFactory, null); - } - - public bool ResetSite { get; set; } - public bool DropTables { get; set; } - - public override dynamic UpdateEditor(dynamic shapeFactory, IUpdateModel updater) { - var viewModel = new ResetSiteViewModel(); - if (updater != null) { - ResetSite = viewModel.ResetSite; - DropTables = viewModel.DropTables; - } - - return shapeFactory.EditorTemplate(TemplateName: "ImportActions/ResetSite", Model: viewModel, Prefix: Prefix); - } - - public override void Execute(ImportActionContext context) { - _shellSettings.State = TenantState.Disabled; - _tenantService.ResetTenant(_shellSettings, DropTables); - _shellSettings.DataProvider = null; - _tenantService.UpdateTenant(_shellSettings); - context.ActionResult = new RedirectResult("~/"); - } - } -} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.ImportExport/Providers/ImportActions/UploadRecipeAction.cs b/src/Orchard.Web/Modules/Orchard.ImportExport/Providers/ImportActions/UploadRecipeAction.cs index fc39dc483..6d9749607 100644 --- a/src/Orchard.Web/Modules/Orchard.ImportExport/Providers/ImportActions/UploadRecipeAction.cs +++ b/src/Orchard.Web/Modules/Orchard.ImportExport/Providers/ImportActions/UploadRecipeAction.cs @@ -1,44 +1,93 @@ using System.IO; +using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; using Orchard.ContentManagement; +using Orchard.Environment.Configuration; +using Orchard.Environment.Features; using Orchard.ImportExport.Services; +using Orchard.ImportExport.ViewModels; namespace Orchard.ImportExport.Providers.ImportActions { public class UploadRecipeAction : ImportAction { private readonly IOrchardServices _orchardServices; private readonly IImportExportService _importExportService; + private readonly ISetupService _setupService; + private readonly ShellSettings _shellSettings; + private readonly IFeatureManager _featureManager; + + public UploadRecipeAction( + IOrchardServices orchardServices, + IImportExportService importExportService, + ISetupService setupService, + ShellSettings shellSettings, + IFeatureManager featureManager) { - public UploadRecipeAction(IOrchardServices orchardServices, IImportExportService importExportService) { _orchardServices = orchardServices; _importExportService = importExportService; + _setupService = setupService; + _shellSettings = shellSettings; + _featureManager = featureManager; } public override string Name { get { return "UploadRecipe"; } } public HttpPostedFileBase File { get; set; } + public bool ResetSite { get; set; } public override dynamic BuildEditor(dynamic shapeFactory) { return UpdateEditor(shapeFactory, null); } public override dynamic UpdateEditor(dynamic shapeFactory, IUpdateModel updater) { - + var viewModel = new UploadRecipeViewModel(); + if (updater != null) { - var request = _orchardServices.WorkContext.HttpContext.Request; - File = request.Files["RecipeFile"]; + + if (updater.TryUpdateModel(viewModel, Prefix, null, null)) { + var request = _orchardServices.WorkContext.HttpContext.Request; + + File = request.Files["RecipeFile"]; + ResetSite = viewModel.ResetSite; + + if (File == null) + updater.AddModelError("RecipeFile", T("No recipe file selected.")); + } } - return shapeFactory.EditorTemplate(TemplateName: "ImportActions/UploadRecipe", Prefix: Prefix); + return shapeFactory.EditorTemplate(TemplateName: "ImportActions/UploadRecipe", Model: viewModel, Prefix: Prefix); } public override void Execute(ImportActionContext context) { if (File == null || File.ContentLength == 0) return; - var executionId = _importExportService.Import(new StreamReader(File.InputStream).ReadToEnd()); + var executionId = ResetSite ? Setup() : ExecuteRecipe(); context.ActionResult = new RedirectToRouteResult(new RouteValueDictionary(new { action = "ImportResult", controller = "Admin", area = "Orchard.ImportExport", executionId = executionId })); } + + private string Setup() { + var setupContext = new SetupContext { + DropExistingTables = true, + RecipeText = ReadRecipeFile(), + AdminPassword = "password", + AdminUsername = _orchardServices.WorkContext.CurrentSite.SuperUser, + DatabaseConnectionString = _shellSettings.DataConnectionString, + DatabaseProvider = _shellSettings.DataProvider, + DatabaseTablePrefix = _shellSettings.DataTablePrefix, + SiteName = _orchardServices.WorkContext.CurrentSite.SiteName, + EnabledFeatures = _featureManager.GetEnabledFeatures().Select(x => x.Id).ToArray() + }; + return _setupService.Setup(setupContext); + } + + private string ExecuteRecipe() { + return _importExportService.Import(ReadRecipeFile()); + } + + private string ReadRecipeFile() { + return new StreamReader(File.InputStream).ReadToEnd(); + } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.ImportExport/Services/ISetupService.cs b/src/Orchard.Web/Modules/Orchard.ImportExport/Services/ISetupService.cs new file mode 100644 index 000000000..ce57a9829 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.ImportExport/Services/ISetupService.cs @@ -0,0 +1,5 @@ +namespace Orchard.ImportExport.Services { + public interface ISetupService : IDependency { + string Setup(SetupContext context); + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.ImportExport/Services/SetupContext.cs b/src/Orchard.Web/Modules/Orchard.ImportExport/Services/SetupContext.cs new file mode 100644 index 000000000..bb29f6aea --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.ImportExport/Services/SetupContext.cs @@ -0,0 +1,15 @@ +using System.Collections.Generic; + +namespace Orchard.ImportExport.Services { + public class SetupContext { + public string SiteName { get; set; } + public string AdminUsername { get; set; } + public string AdminPassword { get; set; } + public string DatabaseProvider { get; set; } + public string DatabaseConnectionString { get; set; } + public string DatabaseTablePrefix { get; set; } + public IEnumerable EnabledFeatures { get; set; } + public string RecipeText { get; set; } + public bool DropExistingTables { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.ImportExport/Services/SetupService.cs b/src/Orchard.Web/Modules/Orchard.ImportExport/Services/SetupService.cs new file mode 100644 index 000000000..a64403185 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.ImportExport/Services/SetupService.cs @@ -0,0 +1,210 @@ +using System; +using System.Linq; +using System.Security.Cryptography; +using System.Web; +using Orchard.ContentManagement; +using Orchard.Core.Settings.Models; +using Orchard.Data; +using Orchard.Data.Migration; +using Orchard.Data.Migration.Interpreters; +using Orchard.Data.Migration.Schema; +using Orchard.Environment; +using Orchard.Environment.Configuration; +using Orchard.Environment.Descriptor; +using Orchard.Environment.Descriptor.Models; +using Orchard.Environment.ShellBuilders; +using Orchard.Environment.State; +using Orchard.Localization.Services; +using Orchard.Logging; +using Orchard.Recipes.Services; +using Orchard.Security; +using Orchard.Settings; +using Orchard.Utility.Extensions; + +namespace Orchard.ImportExport.Services +{ + public class SetupService : Component, ISetupService { + private readonly ShellSettings _shellSettings; + private readonly IOrchardHost _orchardHost; + private readonly IShellSettingsManager _shellSettingsManager; + private readonly IShellContainerFactory _shellContainerFactory; + private readonly ICompositionStrategy _compositionStrategy; + private readonly IProcessingEngine _processingEngine; + + public SetupService( + ShellSettings shellSettings, + IOrchardHost orchardHost, + IShellSettingsManager shellSettingsManager, + IShellContainerFactory shellContainerFactory, + ICompositionStrategy compositionStrategy, + IProcessingEngine processingEngine) { + + _shellSettings = shellSettings; + _orchardHost = orchardHost; + _shellSettingsManager = shellSettingsManager; + _shellContainerFactory = shellContainerFactory; + _compositionStrategy = compositionStrategy; + _processingEngine = processingEngine; + } + + public string Setup(SetupContext context) { + string executionId; + + Logger.Information("Running setup for tenant '{0}'.", _shellSettings.Name); + + // The vanilla Orchard distibution has the following features enabled. + string[] hardcoded = { + // Framework + "Orchard.Framework", + // Core + "Common", "Containers", "Contents", "Dashboard", "Feeds", "Navigation","Scheduling", "Settings", "Shapes", "Title", + // Modules + "Orchard.Pages", "Orchard.ContentPicker", "Orchard.Themes", "Orchard.Users", "Orchard.Roles", "Orchard.Modules", + "PackagingServices", "Orchard.Packaging", "Gallery", "Orchard.Recipes" + }; + + context.EnabledFeatures = hardcoded.Union(context.EnabledFeatures ?? Enumerable.Empty()).Distinct().ToList(); + + var shellSettings = new ShellSettings(_shellSettings); + + if (String.IsNullOrEmpty(shellSettings.DataProvider)) { + shellSettings.DataProvider = context.DatabaseProvider; + shellSettings.DataConnectionString = context.DatabaseConnectionString; + shellSettings.DataTablePrefix = context.DatabaseTablePrefix; + } + + shellSettings.EncryptionAlgorithm = "AES"; + shellSettings.EncryptionKey = SymmetricAlgorithm.Create(shellSettings.EncryptionAlgorithm).Key.ToHexString(); + shellSettings.HashAlgorithm = "HMACSHA256"; + shellSettings.HashKey = HMAC.Create(shellSettings.HashAlgorithm).Key.ToHexString(); + + var shellDescriptor = new ShellDescriptor { + Features = context.EnabledFeatures.Select(name => new ShellFeature { Name = name }) + }; + + var shellBlueprint = _compositionStrategy.Compose(shellSettings, shellDescriptor); + + // Initialize database explicitly, and store shell descriptor. + using (var bootstrapLifetimeScope = _shellContainerFactory.CreateContainer(shellSettings, shellBlueprint)) { + + using (var environment = bootstrapLifetimeScope.CreateWorkContextScope()) { + + // Check if the database is already created (in case an exception occured in the second phase). + var schemaBuilder = new SchemaBuilder(environment.Resolve()); + var installationPresent = true; + try { + var tablePrefix = String.IsNullOrEmpty(shellSettings.DataTablePrefix) ? "" : shellSettings.DataTablePrefix + "_"; + schemaBuilder.ExecuteSql("SELECT * FROM " + tablePrefix + "Settings_ShellDescriptorRecord"); + } + catch { + installationPresent = false; + } + + if (installationPresent) { + if (context.DropExistingTables) { + DropTenantDatabaseTables(environment); + } + } + + // Make a workaround to avoid the Transaction issue for PostgreSQL. + environment.Resolve().RequireNew(); + + schemaBuilder.CreateTable("Orchard_Framework_DataMigrationRecord", table => table + .Column("Id", column => column.PrimaryKey().Identity()) + .Column("DataMigrationClass") + .Column("Version")); + + var dataMigrationManager = environment.Resolve(); + dataMigrationManager.Update("Settings"); + + foreach (var feature in context.EnabledFeatures) { + dataMigrationManager.Update(feature); + } + + var descriptorManager = environment.Resolve(); + descriptorManager.UpdateShellDescriptor(0, shellDescriptor.Features, shellDescriptor.Parameters); + } + } + + // In effect "pump messages" see PostMessage circa 1980. + while ( _processingEngine.AreTasksPending() ) + _processingEngine.ExecuteNextTask(); + + // Creating a standalone environment. + // In theory this environment can be used to resolve any normal components by interface, and those + // components will exist entirely in isolation - no crossover between the safemode container currently in effect. + + // Must mark state as Running - otherwise standalone environment is created "for setup". + shellSettings.State = TenantState.Running; + using (var environment = _orchardHost.CreateStandaloneEnvironment(shellSettings)) { + try { + executionId = CreateTenantData(context, environment); + } + catch { + environment.Resolve().Cancel(); + throw; + } + } + + _shellSettingsManager.SaveSettings(shellSettings); + + return executionId; + } + + private string CreateTenantData(SetupContext context, IWorkContextScope environment) { + // Create superuser. + var membershipService = environment.Resolve(); + var user = membershipService.CreateUser( + new CreateUserParams( + context.AdminUsername, + context.AdminPassword, + email: String.Empty, + passwordQuestion: String.Empty, + passwordAnswer: String.Empty, + isApproved: true)); + + // Set superuser as current user for request (it will be set as the owner of all content items). + var authenticationService = environment.Resolve(); + authenticationService.SetAuthenticatedUserForRequest(user); + + // Set site name and settings. + var siteService = environment.Resolve(); + var siteSettings = siteService.GetSiteSettings().As(); + siteSettings.SiteSalt = Guid.NewGuid().ToString("N"); + siteSettings.SiteName = context.SiteName; + siteSettings.SuperUser = context.AdminUsername; + siteSettings.SiteCulture = "en-US"; + + // Add default culture. + var cultureManager = environment.Resolve(); + cultureManager.AddCulture("en-US"); + + // Execute recipe + var recipeParser = environment.Resolve(); + var recipe = recipeParser.ParseRecipe(context.RecipeText); + var recipeManager = environment.Resolve(); + var executionId = recipeManager.Execute(recipe); + + // Null check: temporary fix for running setup in command line. + if (HttpContext.Current != null) { + authenticationService.SignIn(user, true); + } + + return executionId; + } + + private void DropTenantDatabaseTables(IWorkContextScope environment) { + var sessionFactoryHolder = environment.Resolve(); + var schemaBuilder = new SchemaBuilder(environment.Resolve()); + var configuration = sessionFactoryHolder.GetConfiguration(); + foreach (var mapping in configuration.ClassMappings) { + try { + schemaBuilder.DropTable(mapping.Table.Name); + } + catch (Exception ex) { + Logger.Warning(ex, "Failed to drop table '{0}'.", mapping.Table.Name); + } + } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.ImportExport/ViewModels/ResetSiteViewModel.cs b/src/Orchard.Web/Modules/Orchard.ImportExport/ViewModels/UploadRecipeViewModel.cs similarity index 54% rename from src/Orchard.Web/Modules/Orchard.ImportExport/ViewModels/ResetSiteViewModel.cs rename to src/Orchard.Web/Modules/Orchard.ImportExport/ViewModels/UploadRecipeViewModel.cs index 498d8e01f..f5046b4fc 100644 --- a/src/Orchard.Web/Modules/Orchard.ImportExport/ViewModels/ResetSiteViewModel.cs +++ b/src/Orchard.Web/Modules/Orchard.ImportExport/ViewModels/UploadRecipeViewModel.cs @@ -1,6 +1,5 @@ namespace Orchard.ImportExport.ViewModels { - public class ResetSiteViewModel { + public class UploadRecipeViewModel { public bool ResetSite { get; set; } - public bool DropTables { get; set; } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.ImportExport/Views/EditorTemplates/ImportActions/ResetSite.cshtml b/src/Orchard.Web/Modules/Orchard.ImportExport/Views/EditorTemplates/ImportActions/ResetSite.cshtml deleted file mode 100644 index 78a891b43..000000000 --- a/src/Orchard.Web/Modules/Orchard.ImportExport/Views/EditorTemplates/ImportActions/ResetSite.cshtml +++ /dev/null @@ -1,16 +0,0 @@ -@model Orchard.ImportExport.ViewModels.ResetSiteViewModel -@{ - Script.Require("ShapesBase"); -} -
-
- @Html.CheckBoxFor(m => m.ResetSite) - @Html.LabelFor(m => m.ResetSite, T("Reset Site").ToString(), new {@class = "forcheckbox"}) - @Html.Hint(T("Check this option to resets your site to its uninitialized state.")) -
-
- @Html.CheckBoxFor(m => m.DropTables) - @Html.LabelFor(m => m.DropTables, T("Drop Tables").ToString(), new { @class = "forcheckbox" }) - @Html.Hint(T("Check this option to also drop all of the database tables of the current tenant.")) -
-
\ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.ImportExport/Views/EditorTemplates/ImportActions/UploadRecipe.cshtml b/src/Orchard.Web/Modules/Orchard.ImportExport/Views/EditorTemplates/ImportActions/UploadRecipe.cshtml index 052ca97ca..37d6e4ea5 100644 --- a/src/Orchard.Web/Modules/Orchard.ImportExport/Views/EditorTemplates/ImportActions/UploadRecipe.cshtml +++ b/src/Orchard.Web/Modules/Orchard.ImportExport/Views/EditorTemplates/ImportActions/UploadRecipe.cshtml @@ -1,8 +1,18 @@ -@model dynamic +@model Orchard.ImportExport.ViewModels.UploadRecipeViewModel

@T("Choose a recipe file to import. Please consider {0} or backing up your data first.", @Html.Link(T("exporting").Text, Url.Action("Export", "Admin", new { area = "Orchard.ImportExport" })))

- + +
+
+
+ @Html.CheckBoxFor(m => m.ResetSite) + @Html.LabelFor(m => m.ResetSite, T("Reset Site").ToString(), new { @class = "forcheckbox" }) + @Html.Hint(T("Check this option to reset your site before executing the uploaded recipe.")) +
+
+
@T("This will delete your database tables. Please consider creating a backup first.")
+
\ No newline at end of file