Merging ResetSiteAction with UploadRecipeAction and implementing site setup during import.

This commit is contained in:
Sipke Schoorstra
2015-07-15 23:47:24 +01:00
parent 22869b1960
commit 6ba117d016
10 changed files with 311 additions and 88 deletions

View File

@@ -12,8 +12,3 @@ Features:
Description: Imports and exports content item data. Description: Imports and exports content item data.
Category: Content Category: Content
Dependencies: Orchard.jQuery, Orchard.Recipes 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

View File

@@ -48,7 +48,15 @@
<Prefer32Bit>false</Prefer32Bit> <Prefer32Bit>false</Prefer32Bit>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<Reference Include="Autofac, Version=3.5.0.0, Culture=neutral, PublicKeyToken=17863af14b0044da, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\autofac\Autofac.dll</HintPath>
</Reference>
<Reference Include="Microsoft.CSharp" /> <Reference Include="Microsoft.CSharp" />
<Reference Include="NHibernate, Version=4.0.0.4000, Culture=neutral, PublicKeyToken=aa95f207798dfdb4, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\nhibernate\NHibernate.dll</HintPath>
</Reference>
<Reference Include="System" /> <Reference Include="System" />
<Reference Include="System.ComponentModel.DataAnnotations" /> <Reference Include="System.ComponentModel.DataAnnotations" />
<Reference Include="System.Data.DataSetExtensions" /> <Reference Include="System.Data.DataSetExtensions" />
@@ -72,8 +80,10 @@
<Compile Include="Permissions.cs" /> <Compile Include="Permissions.cs" />
<Compile Include="Properties\AssemblyInfo.cs" /> <Compile Include="Properties\AssemblyInfo.cs" />
<Compile Include="Providers\ExportActions\BuildRecipeAction.cs" /> <Compile Include="Providers\ExportActions\BuildRecipeAction.cs" />
<Compile Include="Providers\ImportActions\ResetSiteAction.cs" /> <Compile Include="Services\ISetupService.cs" />
<Compile Include="ViewModels\ResetSiteViewModel.cs" /> <Compile Include="Services\SetupContext.cs" />
<Compile Include="Services\SetupService.cs" />
<Compile Include="ViewModels\UploadRecipeViewModel.cs" />
<Compile Include="Providers\ImportActions\UploadRecipeAction.cs" /> <Compile Include="Providers\ImportActions\UploadRecipeAction.cs" />
<Compile Include="Recipes\Builders\CustomStepsStep.cs" /> <Compile Include="Recipes\Builders\CustomStepsStep.cs" />
<Compile Include="Services\ImportAction.cs" /> <Compile Include="Services\ImportAction.cs" />
@@ -149,9 +159,6 @@
<ItemGroup> <ItemGroup>
<Content Include="Views\EditorTemplates\ExportActions\BuildRecipe.cshtml" /> <Content Include="Views\EditorTemplates\ExportActions\BuildRecipe.cshtml" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<Content Include="Views\EditorTemplates\ImportActions\ResetSite.cshtml" />
</ItemGroup>
<PropertyGroup> <PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion> <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath> <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>

View File

@@ -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("~/");
}
}
}

View File

@@ -1,44 +1,93 @@
using System.IO; using System.IO;
using System.Linq;
using System.Web; using System.Web;
using System.Web.Mvc; using System.Web.Mvc;
using System.Web.Routing; using System.Web.Routing;
using Orchard.ContentManagement; using Orchard.ContentManagement;
using Orchard.Environment.Configuration;
using Orchard.Environment.Features;
using Orchard.ImportExport.Services; using Orchard.ImportExport.Services;
using Orchard.ImportExport.ViewModels;
namespace Orchard.ImportExport.Providers.ImportActions { namespace Orchard.ImportExport.Providers.ImportActions {
public class UploadRecipeAction : ImportAction { public class UploadRecipeAction : ImportAction {
private readonly IOrchardServices _orchardServices; private readonly IOrchardServices _orchardServices;
private readonly IImportExportService _importExportService; 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; _orchardServices = orchardServices;
_importExportService = importExportService; _importExportService = importExportService;
_setupService = setupService;
_shellSettings = shellSettings;
_featureManager = featureManager;
} }
public override string Name { get { return "UploadRecipe"; } } public override string Name { get { return "UploadRecipe"; } }
public HttpPostedFileBase File { get; set; } public HttpPostedFileBase File { get; set; }
public bool ResetSite { get; set; }
public override dynamic BuildEditor(dynamic shapeFactory) { public override dynamic BuildEditor(dynamic shapeFactory) {
return UpdateEditor(shapeFactory, null); return UpdateEditor(shapeFactory, null);
} }
public override dynamic UpdateEditor(dynamic shapeFactory, IUpdateModel updater) { public override dynamic UpdateEditor(dynamic shapeFactory, IUpdateModel updater) {
var viewModel = new UploadRecipeViewModel();
if (updater != null) { if (updater != null) {
if (updater.TryUpdateModel(viewModel, Prefix, null, null)) {
var request = _orchardServices.WorkContext.HttpContext.Request; var request = _orchardServices.WorkContext.HttpContext.Request;
File = request.Files["RecipeFile"]; 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) { public override void Execute(ImportActionContext context) {
if (File == null || File.ContentLength == 0) if (File == null || File.ContentLength == 0)
return; 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 })); 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();
}
} }
} }

View File

@@ -0,0 +1,5 @@
namespace Orchard.ImportExport.Services {
public interface ISetupService : IDependency {
string Setup(SetupContext context);
}
}

View File

@@ -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<string> EnabledFeatures { get; set; }
public string RecipeText { get; set; }
public bool DropExistingTables { get; set; }
}
}

View File

@@ -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<string>()).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<IDataMigrationInterpreter>());
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<ITransactionManager>().RequireNew();
schemaBuilder.CreateTable("Orchard_Framework_DataMigrationRecord", table => table
.Column<int>("Id", column => column.PrimaryKey().Identity())
.Column<string>("DataMigrationClass")
.Column<int>("Version"));
var dataMigrationManager = environment.Resolve<IDataMigrationManager>();
dataMigrationManager.Update("Settings");
foreach (var feature in context.EnabledFeatures) {
dataMigrationManager.Update(feature);
}
var descriptorManager = environment.Resolve<IShellDescriptorManager>();
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<ITransactionManager>().Cancel();
throw;
}
}
_shellSettingsManager.SaveSettings(shellSettings);
return executionId;
}
private string CreateTenantData(SetupContext context, IWorkContextScope environment) {
// Create superuser.
var membershipService = environment.Resolve<IMembershipService>();
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<IAuthenticationService>();
authenticationService.SetAuthenticatedUserForRequest(user);
// Set site name and settings.
var siteService = environment.Resolve<ISiteService>();
var siteSettings = siteService.GetSiteSettings().As<SiteSettingsPart>();
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<ICultureManager>();
cultureManager.AddCulture("en-US");
// Execute recipe
var recipeParser = environment.Resolve<IRecipeParser>();
var recipe = recipeParser.ParseRecipe(context.RecipeText);
var recipeManager = environment.Resolve<IRecipeManager>();
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<ISessionFactoryHolder>();
var schemaBuilder = new SchemaBuilder(environment.Resolve<IDataMigrationInterpreter>());
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);
}
}
}
}
}

View File

@@ -1,6 +1,5 @@
namespace Orchard.ImportExport.ViewModels { namespace Orchard.ImportExport.ViewModels {
public class ResetSiteViewModel { public class UploadRecipeViewModel {
public bool ResetSite { get; set; } public bool ResetSite { get; set; }
public bool DropTables { get; set; }
} }
} }

View File

@@ -1,16 +0,0 @@
@model Orchard.ImportExport.ViewModels.ResetSiteViewModel
@{
Script.Require("ShapesBase");
}
<fieldset>
<div>
@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."))
</div>
<div data-controllerid="@Html.FieldIdFor(m => m.ResetSite)">
@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."))
</div>
</fieldset>

View File

@@ -1,8 +1,18 @@
@model dynamic @model Orchard.ImportExport.ViewModels.UploadRecipeViewModel
<p> <p>
@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" }))) @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" })))
</p> </p>
<fieldset> <fieldset>
<label for="RecipeFile">@T("Recipe File:")</label> <label for="RecipeFile">@T("Recipe File:")</label>
<input type="file" id="RecipeFile" size="64" name="RecipeFile" value="@T("Browse...")" /> <input type="file" id="RecipeFile" size="64" name="RecipeFile" value="@T("Browse...")"/>
</fieldset>
<fieldset>
<div>
@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."))
</div>
<div data-controllerid="@Html.FieldIdFor(m => m.ResetSite)">
<div class="message message-Warning">@T("This will delete your database tables. Please consider creating a backup first.")</div>
</div>
</fieldset> </fieldset>