From 7c5640692eaabb6fc8c0b86fc9b51ec8e71d3881 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Mon, 30 Aug 2010 15:06:52 -0700 Subject: [PATCH 01/13] Added Messaging and Email modules --HG-- branch : dev --- .../DataMigrations/MessagingDataMigration.cs | 14 ++ .../Drivers/MessageSettingsPartDriver.cs | 48 ++++++ .../Handlers/MessageSettingsPartHandler.cs | 14 ++ .../Messaging/Models/MessageSettingsPart.cs | 11 ++ .../Models/MessageSettingsPartRecord.cs | 11 ++ src/Orchard.Web/Core/Messaging/Module.txt | 12 ++ .../Services/DefaultMessageManager.cs | 75 ++++++++++ .../MessageSettingsPartViewModel.cs | 10 ++ .../Parts/Messaging.MessageSettings.ascx | 20 +++ .../Core/Messaging/Views/Web.config | 34 +++++ src/Orchard.Web/Core/Orchard.Core.csproj | 10 ++ .../DataMigrations/EmailDataMigration.cs | 22 +++ .../Handlers/SmtpSettingsPartHandler.cs | 15 ++ .../Orchard.Email/Models/SmtpSettingsPart.cs | 47 ++++++ .../Models/SmtpSettingsPartRecord.cs | 48 ++++++ .../Modules/Orchard.Email/Module.txt | 12 ++ .../Orchard.Email/Orchard.Email.csproj | 141 ++++++++++++++++++ .../Orchard.Email/Properties/AssemblyInfo.cs | 35 +++++ .../Services/EmailMessageEventHandler.cs | 30 ++++ .../Services/EmailMessagingChannel.cs | 93 ++++++++++++ .../Services/MissingSettingsBanner.cs | 37 +++++ .../Parts/Smtp.SiteSettings.ascx | 43 ++++++ .../Modules/Orchard.Email/Views/Web.config | 34 +++++ .../Modules/Orchard.Email/Web.config | 22 +++ .../Orchard.Setup/Services/SetupService.cs | 1 + src/Orchard.sln | 7 + .../Messaging/Events/IMessageEventHandler.cs | 9 ++ src/Orchard/Messaging/Models/Message.cs | 11 ++ .../Messaging/Models/MessageContext.cs | 16 ++ .../Messaging/Services/IMessageManager.cs | 21 +++ .../Messaging/Services/IMessagingChannel.cs | 22 +++ src/Orchard/Orchard.Framework.csproj | 5 + 32 files changed, 930 insertions(+) create mode 100644 src/Orchard.Web/Core/Messaging/DataMigrations/MessagingDataMigration.cs create mode 100644 src/Orchard.Web/Core/Messaging/Drivers/MessageSettingsPartDriver.cs create mode 100644 src/Orchard.Web/Core/Messaging/Handlers/MessageSettingsPartHandler.cs create mode 100644 src/Orchard.Web/Core/Messaging/Models/MessageSettingsPart.cs create mode 100644 src/Orchard.Web/Core/Messaging/Models/MessageSettingsPartRecord.cs create mode 100644 src/Orchard.Web/Core/Messaging/Module.txt create mode 100644 src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs create mode 100644 src/Orchard.Web/Core/Messaging/ViewModels/MessageSettingsPartViewModel.cs create mode 100644 src/Orchard.Web/Core/Messaging/Views/EditorTemplates/Parts/Messaging.MessageSettings.ascx create mode 100644 src/Orchard.Web/Core/Messaging/Views/Web.config create mode 100644 src/Orchard.Web/Modules/Orchard.Email/DataMigrations/EmailDataMigration.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Handlers/SmtpSettingsPartHandler.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPart.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPartRecord.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Module.txt create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Properties/AssemblyInfo.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessageEventHandler.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessagingChannel.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Services/MissingSettingsBanner.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Views/EditorTemplates/Parts/Smtp.SiteSettings.ascx create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Views/Web.config create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Web.config create mode 100644 src/Orchard/Messaging/Events/IMessageEventHandler.cs create mode 100644 src/Orchard/Messaging/Models/Message.cs create mode 100644 src/Orchard/Messaging/Models/MessageContext.cs create mode 100644 src/Orchard/Messaging/Services/IMessageManager.cs create mode 100644 src/Orchard/Messaging/Services/IMessagingChannel.cs diff --git a/src/Orchard.Web/Core/Messaging/DataMigrations/MessagingDataMigration.cs b/src/Orchard.Web/Core/Messaging/DataMigrations/MessagingDataMigration.cs new file mode 100644 index 000000000..5ffb35497 --- /dev/null +++ b/src/Orchard.Web/Core/Messaging/DataMigrations/MessagingDataMigration.cs @@ -0,0 +1,14 @@ +using Orchard.Data.Migration; + +namespace Orchard.Core.Messaging.DataMigrations { + public class MessagingDataMigration : DataMigrationImpl { + + public int Create() { + SchemaBuilder.CreateTable("MessageSettingsPartRecord", table => table + .ContentPartRecord() + .Column("DefaultChannelService") + ); + return 1; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Messaging/Drivers/MessageSettingsPartDriver.cs b/src/Orchard.Web/Core/Messaging/Drivers/MessageSettingsPartDriver.cs new file mode 100644 index 000000000..5411b1765 --- /dev/null +++ b/src/Orchard.Web/Core/Messaging/Drivers/MessageSettingsPartDriver.cs @@ -0,0 +1,48 @@ +using JetBrains.Annotations; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Drivers; +using Orchard.Core.ContentsLocation.Models; +using Orchard.Core.Messaging.Models; +using Orchard.Core.Messaging.Services; +using Orchard.Core.Messaging.ViewModels; +using Orchard.Localization; +using Orchard.Messaging.Services; + +namespace Orchard.Core.Messaging.Drivers { + [UsedImplicitly] + public class ContentSubscriptionPartDriver : ContentPartDriver { + private readonly IMessageManager _messageQueueManager; + public IOrchardServices Services { get; set; } + + public ContentSubscriptionPartDriver(IOrchardServices services, IMessageManager messageQueueManager) { + _messageQueueManager = messageQueueManager; + Services = services; + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + protected override string Prefix { get { return "MessageSettings"; } } + + protected override DriverResult Editor(MessageSettingsPart part) { + + var model = new ContentSubscriptionPartViewModel { + ChannelServices = _messageQueueManager.GetAvailableChannelServices(), + MessageSettings = part + }; + + return ContentPartTemplate(model, "Parts/Messaging.MessageSettings"); + } + + protected override DriverResult Editor(MessageSettingsPart part, IUpdateModel updater) { + var model = new ContentSubscriptionPartViewModel { + MessageSettings = part + }; + + if (updater.TryUpdateModel(model, Prefix, null, null)) { + } + + return ContentPartTemplate(model, "Parts/Messaging.MessageSettings"); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Messaging/Handlers/MessageSettingsPartHandler.cs b/src/Orchard.Web/Core/Messaging/Handlers/MessageSettingsPartHandler.cs new file mode 100644 index 000000000..86350726c --- /dev/null +++ b/src/Orchard.Web/Core/Messaging/Handlers/MessageSettingsPartHandler.cs @@ -0,0 +1,14 @@ +using JetBrains.Annotations; +using Orchard.Core.Messaging.Models; +using Orchard.Data; +using Orchard.ContentManagement.Handlers; + +namespace Orchard.Core.Messaging.Handlers { + [UsedImplicitly] + public class SmtpSettingsPartHandler : ContentHandler { + public SmtpSettingsPartHandler(IRepository repository) { + Filters.Add(new ActivatingFilter("Site")); + Filters.Add(StorageFilter.For(repository)); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Messaging/Models/MessageSettingsPart.cs b/src/Orchard.Web/Core/Messaging/Models/MessageSettingsPart.cs new file mode 100644 index 000000000..c624bb20d --- /dev/null +++ b/src/Orchard.Web/Core/Messaging/Models/MessageSettingsPart.cs @@ -0,0 +1,11 @@ +using Orchard.ContentManagement; + +namespace Orchard.Core.Messaging.Models { + public class MessageSettingsPart : ContentPart { + public string DefaultChannelService { + get { return Record.DefaultChannelService; } + set { Record.DefaultChannelService = value; } + } + + } +} diff --git a/src/Orchard.Web/Core/Messaging/Models/MessageSettingsPartRecord.cs b/src/Orchard.Web/Core/Messaging/Models/MessageSettingsPartRecord.cs new file mode 100644 index 000000000..7d23a9d03 --- /dev/null +++ b/src/Orchard.Web/Core/Messaging/Models/MessageSettingsPartRecord.cs @@ -0,0 +1,11 @@ +using Orchard.ContentManagement.Records; + +namespace Orchard.Core.Messaging.Models { + public class MessageSettingsPartRecord : ContentPartRecord { + /// + /// Default service used for messages + /// + public virtual string DefaultChannelService { get; set; } + + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Core/Messaging/Module.txt b/src/Orchard.Web/Core/Messaging/Module.txt new file mode 100644 index 000000000..854b2823e --- /dev/null +++ b/src/Orchard.Web/Core/Messaging/Module.txt @@ -0,0 +1,12 @@ +Name: Messaging +antiforgery: enabled +author: The Orchard Team +website: http://orchardproject.net +version: 0.1.0 +orchardversion: 0.6.0 +description: The Messaging module adds messaging functionalities. +features: + Messaging: + Description: Messaging services. + Category: Messaging + Dependencies: Settings diff --git a/src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs b/src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs new file mode 100644 index 000000000..996f99e57 --- /dev/null +++ b/src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs @@ -0,0 +1,75 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using JetBrains.Annotations; +using Orchard.ContentManagement; +using Orchard.Core.Messaging.Models; +using Orchard.Data; +using Orchard.Logging; +using Orchard.Messaging.Events; +using Orchard.Messaging.Models; +using Orchard.Messaging.Services; +using Orchard.Settings; + +namespace Orchard.Core.Messaging.Services { + public class DefaultMessageManager : IMessageManager { + private readonly IMessageEventHandler _messageEventHandler; + private readonly IEnumerable _channels; + private readonly IRepository _messageRepository; + + protected virtual ISite CurrentSite { get; [UsedImplicitly] private set; } + public ILogger Logger { get; set; } + + public DefaultMessageManager( + IMessageEventHandler messageEventHandler, + IEnumerable channels, + IRepository messageRepository) { + _messageEventHandler = messageEventHandler; + _channels = channels; + _messageRepository = messageRepository; + } + + public void Send(Message message) { + if ( !HasChannels() ) + return; + + var messageSettings = CurrentSite.As().Record; + + if ( messageSettings == null || String.IsNullOrWhiteSpace(messageSettings.DefaultChannelService) ) { + return; + } + + Logger.Information("Sending message {0}", message.Type); + try { + + // if the service is not explicit, use the default one, as per settings configuration + if ( String.IsNullOrWhiteSpace(message.Service) ) { + message.Service = messageSettings.DefaultChannelService; + } + + var context = new MessageContext(message); + + _messageEventHandler.Sending(context); + + foreach ( var channel in _channels ) { + channel.SendMessage(context); + } + + _messageEventHandler.Sent(context); + + Logger.Information("Message {0} sent", message.Type); + } + catch ( Exception e ) { + Logger.Error(e, "An error occured while sending the message {0}", message.Type); + } + } + + public bool HasChannels() { + return _channels.Any(); + } + + public IEnumerable GetAvailableChannelServices() { + return _channels.SelectMany(c => c.GetAvailableServices()); + } + } +} diff --git a/src/Orchard.Web/Core/Messaging/ViewModels/MessageSettingsPartViewModel.cs b/src/Orchard.Web/Core/Messaging/ViewModels/MessageSettingsPartViewModel.cs new file mode 100644 index 000000000..da90402e4 --- /dev/null +++ b/src/Orchard.Web/Core/Messaging/ViewModels/MessageSettingsPartViewModel.cs @@ -0,0 +1,10 @@ +using Orchard.Core.Messaging.Models; +using Orchard.Mvc.ViewModels; +using System.Collections.Generic; + +namespace Orchard.Core.Messaging.ViewModels { + public class ContentSubscriptionPartViewModel : BaseViewModel { + public MessageSettingsPart MessageSettings { get; set; } + public IEnumerable ChannelServices { get; set; } + } +} diff --git a/src/Orchard.Web/Core/Messaging/Views/EditorTemplates/Parts/Messaging.MessageSettings.ascx b/src/Orchard.Web/Core/Messaging/Views/EditorTemplates/Parts/Messaging.MessageSettings.ascx new file mode 100644 index 000000000..2d4734df4 --- /dev/null +++ b/src/Orchard.Web/Core/Messaging/Views/EditorTemplates/Parts/Messaging.MessageSettings.ascx @@ -0,0 +1,20 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Import Namespace="Orchard.Core.Messaging.Models"%> +<%@ Import Namespace="Orchard.Core.Messaging.ViewModels"%> +
+ <%: T("Messaging")%> +
+ + <% if ( Model.ChannelServices.Any() ) { %> + +
+
\ No newline at end of file diff --git a/src/Orchard.Web/Core/Messaging/Views/Web.config b/src/Orchard.Web/Core/Messaging/Views/Web.config new file mode 100644 index 000000000..e065d8735 --- /dev/null +++ b/src/Orchard.Web/Core/Messaging/Views/Web.config @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Core/Orchard.Core.csproj b/src/Orchard.Web/Core/Orchard.Core.csproj index ad30d94b0..9929f5132 100644 --- a/src/Orchard.Web/Core/Orchard.Core.csproj +++ b/src/Orchard.Web/Core/Orchard.Core.csproj @@ -90,6 +90,13 @@ + + + + + + + @@ -247,6 +254,8 @@ + + @@ -366,6 +375,7 @@ + diff --git a/src/Orchard.Web/Modules/Orchard.Email/DataMigrations/EmailDataMigration.cs b/src/Orchard.Web/Modules/Orchard.Email/DataMigrations/EmailDataMigration.cs new file mode 100644 index 000000000..e60fb7e6a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/DataMigrations/EmailDataMigration.cs @@ -0,0 +1,22 @@ +using Orchard.Data.Migration; + +namespace Orchard.Email.DataMigrations { + public class EmailDataMigration : DataMigrationImpl { + + public int Create() { + + SchemaBuilder.CreateTable("SmtpSettingsPartRecord", table => table + .ContentPartRecord() + .Column("Address") + .Column("Host") + .Column("Port") + .Column("EnableSsl") + .Column("RequireCredentials") + .Column("UserName") + .Column("Password") + ); + + return 1; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Handlers/SmtpSettingsPartHandler.cs b/src/Orchard.Web/Modules/Orchard.Email/Handlers/SmtpSettingsPartHandler.cs new file mode 100644 index 000000000..b24131f06 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Handlers/SmtpSettingsPartHandler.cs @@ -0,0 +1,15 @@ +using JetBrains.Annotations; +using Orchard.Email.Models; +using Orchard.Data; +using Orchard.ContentManagement.Handlers; + +namespace Orchard.Email.Handlers { + [UsedImplicitly] + public class SmtpSettingsPartHandler : ContentHandler { + public SmtpSettingsPartHandler(IRepository repository) { + Filters.Add(new ActivatingFilter("Site")); + Filters.Add(StorageFilter.For(repository)); + Filters.Add(new TemplateFilterForRecord("SmtpSettings", "Parts/Smtp.SiteSettings")); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPart.cs b/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPart.cs new file mode 100644 index 000000000..057159041 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPart.cs @@ -0,0 +1,47 @@ +using Orchard.ContentManagement; +using System; + +namespace Orchard.Email.Models { + public class SmtpSettingsPart : ContentPart { + public bool IsValid() { + return !String.IsNullOrWhiteSpace(Record.Host) + && Record.Port > 0 + && !String.IsNullOrWhiteSpace(Record.Address); + } + + public string Address { + get { return Record.Address; } + set { Record.Address = value; } + } + + public string Host { + get { return Record.Host; } + set { Record.Host = value; } + } + + public int Port { + get { return Record.Port; } + set { Record.Port = value; } + } + + public bool EnableSsl { + get { return Record.EnableSsl; } + set { Record.EnableSsl = value; } + } + + public bool RequireCredentials { + get { return Record.RequireCredentials; } + set { Record.RequireCredentials = value; } + } + + public string UserName { + get { return Record.UserName; } + set { Record.UserName = value; } + } + + public string Password { + get { return Record.Password; } + set { Record.Password = value; } + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPartRecord.cs b/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPartRecord.cs new file mode 100644 index 000000000..a9d3950e6 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPartRecord.cs @@ -0,0 +1,48 @@ +using System.Net.Mail; +using Orchard.ContentManagement.Records; +using System.ComponentModel.DataAnnotations; + +namespace Orchard.Email.Models { + public class SmtpSettingsPartRecord : ContentPartRecord { + /// + /// From address in the mail message + /// + public virtual string Address { get; set; } + + /// + /// Server name hosting the SMTP service + /// + public virtual string Host { get; set; } + + /// + /// Port number on which SMTP service runs + /// + public virtual int Port { get; set; } + + /// + /// Whether to enable SSL communications with the server + /// + public virtual bool EnableSsl { get; set; } + + /// + /// Whether specific credentials should be used + /// + public virtual bool RequireCredentials { get; set; } + + /// + /// The username to connect to the SMTP server if DefaultCredentials is False + /// + public virtual string UserName { get; set; } + + /// + /// The password to connect to the SMTP server if DefaultCredentials is False + /// + public virtual string Password { get; set; } + + public SmtpSettingsPartRecord() { + Port = 25; + RequireCredentials = false; + EnableSsl = false; + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Module.txt b/src/Orchard.Web/Modules/Orchard.Email/Module.txt new file mode 100644 index 000000000..6c7d3a8f3 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Module.txt @@ -0,0 +1,12 @@ +Name: Email Messaging +antiforgery: enabled +author: The Orchard Team +website: http://orchardproject.net +version: 0.1.0 +orchardversion: 0.6.0 +description: The Email Messaging module adds Email sending functionalities. +features: + Orchard.Email: + Description: Email Messaging services. + Category: Messaging + Dependencies: Messaging diff --git a/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj b/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj new file mode 100644 index 000000000..0c1ba7057 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj @@ -0,0 +1,141 @@ + + + + Debug + AnyCPU + 9.0.30729 + 2.0 + {05660F47-D649-48BD-9DED-DF4E01E7CFF9} + {F85E285D-A4E0-4152-9332-AB1D724D3325};{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} + Library + Properties + Orchard.Email + Orchard.Email + v4.0 + false + + + 3.5 + + + + + true + full + false + bin\ + DEBUG;TRACE + prompt + 4 + AllRules.ruleset + + + pdbonly + true + bin\ + TRACE + prompt + 4 + AllRules.ruleset + + + + + + 3.5 + + + + + + + + False + ..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + {2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6} + Orchard.Framework + + + {9916839C-39FC-4CEB-A5AF-89CA7E87119F} + Orchard.Core + + + {79AED36E-ABD0-4747-93D3-8722B042454B} + Orchard.Users + + + + + + + + + $(ProjectDir)\..\Manifests + + + + + + + + + + + + False + True + 45979 + / + + + False + True + http://orchard.codeplex.com + False + + + + + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Properties/AssemblyInfo.cs b/src/Orchard.Web/Modules/Orchard.Email/Properties/AssemblyInfo.cs new file mode 100644 index 000000000..ea6abdcf2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Properties/AssemblyInfo.cs @@ -0,0 +1,35 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Orchard.Messaging.Email")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Orchard")] +[assembly: AssemblyCopyright("Copyright © CodePlex Foundation 2009")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// The following GUID is for the ID of the typelib if this project is exposed to COM +[assembly: Guid("9c778ece-c759-47fb-95b6-e73c03d9e969")] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Revision and Build Numbers +// by using the '*' as shown below: +[assembly: AssemblyVersion("0.5.0")] +[assembly: AssemblyFileVersion("0.5.0")] diff --git a/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessageEventHandler.cs b/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessageEventHandler.cs new file mode 100644 index 000000000..d4b87486c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessageEventHandler.cs @@ -0,0 +1,30 @@ +using Orchard.Messaging.Events; +using Orchard.Core.Messaging.Models; +using Orchard.ContentManagement; +using Orchard.Users.Models; +using Orchard.Messaging.Models; + +namespace Orchard.Email.Services { + public class EmailMessageEventHandler : IMessageEventHandler { + private readonly IContentManager _contentManager; + + public EmailMessageEventHandler(IContentManager contentManager) { + _contentManager = contentManager; + } + + public void Sending(MessageContext context) { + var contentItem = _contentManager.Get(context.Message.Recipient.Id); + if ( contentItem == null ) + return; + + var recipient = contentItem.As(); + if ( recipient == null ) + return; + + context.Properties.Add(EmailMessagingChannel.EmailAddress, recipient.Email); + } + + public void Sent(MessageContext context) { + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessagingChannel.cs b/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessagingChannel.cs new file mode 100644 index 000000000..fe8bec169 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessagingChannel.cs @@ -0,0 +1,93 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Mail; +using System.Web; +using System.Web.Hosting; +using JetBrains.Annotations; +using Orchard.ContentManagement; +using Orchard.Localization; +using Orchard.Logging; +using Orchard.Core.Messaging.Models; +using Orchard.Core.Messaging.Services; +using Orchard.Email.Models; +using Orchard.Settings; +using Orchard.Messaging.Services; +using Orchard.Messaging.Models; + +namespace Orchard.Email.Services { + public class EmailMessagingChannel : IMessagingChannel { + + public const string EmailService = "Email"; + public const string EmailAddress = "EmailAddress"; + + public EmailMessagingChannel() { + Logger = NullLogger.Instance; + } + + protected virtual ISite CurrentSite { get; [UsedImplicitly] private set; } + public ILogger Logger { get; set; } + public Localizer T { get; set; } + + public void SendMessage(MessageContext context) { + if ( context.Message.Service.ToLower() != EmailService ) + return; + + var smtpSettings = CurrentSite.As(); + + // can't process emails if the Smtp settings have not yet been set + if ( smtpSettings == null || !smtpSettings.IsValid() ) { + return; + } + + var smtpClient = new SmtpClient { UseDefaultCredentials = !smtpSettings.RequireCredentials }; + if ( !smtpClient.UseDefaultCredentials && !String.IsNullOrWhiteSpace(smtpSettings.UserName) ) { + smtpClient.Credentials = new NetworkCredential(smtpSettings.UserName, smtpSettings.Password); + } + + var emailAddress = context.Properties[EmailAddress]; + + if(String.IsNullOrWhiteSpace(emailAddress)) { + Logger.Error("Recipient is missing an email address"); + return; + } + + if ( smtpSettings.Host != null ) + smtpClient.Host = smtpSettings.Host; + + smtpClient.Port = smtpSettings.Port; + smtpClient.EnableSsl = smtpSettings.EnableSsl; + smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network; + + var message = new MailMessage { + From = new MailAddress(smtpSettings.Address), + Subject = context.Message.Subject ?? "", + Body = context.Message.Body ?? "", + IsBodyHtml = context.Message.Body != null && context.Message.Body.Contains("<") && context.Message.Body.Contains(">") + }; + + message.To.Add(emailAddress); + + try { + smtpClient.Send(message); + Logger.Debug("Message sent to {0}: {1}", emailAddress, context.Message.Subject); + } + catch(Exception e) { + Logger.Error(e, "An unexpected error while sending a message to {0}: {1}", emailAddress, context.Message.Subject); + } + } + + public bool IsRecipientValidated(ContentItem contentItem) { + return false; + } + + public void ValidateRecipient(ContentItem contentItem) { + var context = new MessageContext(new Message { Recipient = contentItem.Record, Body = "Please validate your account", Service = "email", Subject = "Validate your account" } ); + SendMessage(context); + } + + public IEnumerable GetAvailableServices() { + return new[] {EmailService}; + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Email/Services/MissingSettingsBanner.cs b/src/Orchard.Web/Modules/Orchard.Email/Services/MissingSettingsBanner.cs new file mode 100644 index 000000000..d35cbe328 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Services/MissingSettingsBanner.cs @@ -0,0 +1,37 @@ +using System; +using System.Collections.Generic; +using JetBrains.Annotations; +using Orchard.ContentManagement; +using Orchard.Core.Messaging.Models; +using Orchard.Localization; +using Orchard.Email.Models; +using Orchard.Settings; +using Orchard.UI.Admin.Notification; +using Orchard.UI.Notify; + +namespace Orchard.Email.Services { + public class MissingSettingsBanner: INotificationProvider { + + public MissingSettingsBanner() { + T = NullLocalizer.Instance; + } + + protected virtual ISite CurrentSite { get; [UsedImplicitly] private set; } + public Localizer T { get; set; } + + public IEnumerable GetNotifications() { + + var smtpSettings = CurrentSite.As(); + + if ( smtpSettings == null || !smtpSettings.IsValid() ) { + yield return new NotifyEntry { Message = T("The SMTP settings needs to be configured." ), Type = NotifyType.Warning}; + } + + var messageSettings = CurrentSite.As().Record; + + if ( messageSettings == null || String.IsNullOrWhiteSpace(messageSettings.DefaultChannelService) ) { + yield return new NotifyEntry { Message = T("The default channel service needs to be configured."), Type = NotifyType.Warning }; + } + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Email/Views/EditorTemplates/Parts/Smtp.SiteSettings.ascx b/src/Orchard.Web/Modules/Orchard.Email/Views/EditorTemplates/Parts/Smtp.SiteSettings.ascx new file mode 100644 index 000000000..d9cdefe8e --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Views/EditorTemplates/Parts/Smtp.SiteSettings.ascx @@ -0,0 +1,43 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Import Namespace="Orchard.Email.Models"%> +<%@ Import Namespace="System.Net.Mail" %> +
+ <%: T("SMTP")%> +
+ + <%: Html.EditorFor(m => m.Address)%> + <%: Html.ValidationMessage("Address", "*")%> +
+
+ + <%: Html.EditorFor(m => m.Host)%> + <%: Html.ValidationMessage("Host", "*")%> +
+
+ + <%: Html.EditorFor(m => m.Port)%> + <%: Html.ValidationMessage("Port", "*")%> +
+
+ <%: Html.EditorFor(m => m.EnableSsl)%> + + <%: Html.ValidationMessage("EnableSsl", "*")%> +
+
+ <%: Html.EditorFor(m => m.RequireCredentials)%> + + <%: Html.ValidationMessage("RequireCredentials", "*")%> +
+
+
+ + <%: Html.EditorFor(m => m.UserName)%> + <%: Html.ValidationMessage("UserName", "*")%> +
+
+ + <%: Html.PasswordFor(m => m.Password)%> + <%: Html.ValidationMessage("Password", "*")%> +
+
+
\ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Views/Web.config b/src/Orchard.Web/Modules/Orchard.Email/Views/Web.config new file mode 100644 index 000000000..e065d8735 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Views/Web.config @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Email/Web.config b/src/Orchard.Web/Modules/Orchard.Email/Web.config new file mode 100644 index 000000000..31fcd0c21 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Web.config @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Setup/Services/SetupService.cs b/src/Orchard.Web/Modules/Orchard.Setup/Services/SetupService.cs index b0d160831..f639cbc0e 100644 --- a/src/Orchard.Web/Modules/Orchard.Setup/Services/SetupService.cs +++ b/src/Orchard.Web/Modules/Orchard.Setup/Services/SetupService.cs @@ -80,6 +80,7 @@ namespace Orchard.Setup.Services { "Routable", "Settings", "XmlRpc", + "Messaging", "Orchard.Users", "Orchard.Roles", "TinyMce", diff --git a/src/Orchard.sln b/src/Orchard.sln index 23e4179eb..251194d5f 100644 --- a/src/Orchard.sln +++ b/src/Orchard.sln @@ -77,6 +77,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Tests", "Tools\Orch EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.ArchiveLater", "Orchard.Web\Modules\Orchard.ArchiveLater\Orchard.ArchiveLater.csproj", "{1C981BB3-26F7-494C-9005-CC27A5144233}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Orchard.Email", "Orchard.Web\Modules\Orchard.Email\Orchard.Email.csproj", "{05660F47-D649-48BD-9DED-DF4E01E7CFF9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -219,6 +221,10 @@ Global {1C981BB3-26F7-494C-9005-CC27A5144233}.Debug|Any CPU.Build.0 = Debug|Any CPU {1C981BB3-26F7-494C-9005-CC27A5144233}.Release|Any CPU.ActiveCfg = Release|Any CPU {1C981BB3-26F7-494C-9005-CC27A5144233}.Release|Any CPU.Build.0 = Release|Any CPU + {05660F47-D649-48BD-9DED-DF4E01E7CFF9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {05660F47-D649-48BD-9DED-DF4E01E7CFF9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {05660F47-D649-48BD-9DED-DF4E01E7CFF9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {05660F47-D649-48BD-9DED-DF4E01E7CFF9}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -243,6 +249,7 @@ Global {D5D447D7-EF8E-43A6-B9A4-3B025DD9F45D} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {DFD137A2-DDB5-4D22-BE0D-FA9AD4C8B059} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {1C981BB3-26F7-494C-9005-CC27A5144233} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} + {05660F47-D649-48BD-9DED-DF4E01E7CFF9} = {E9C9F120-07BA-4DFB-B9C3-3AFB9D44C9D5} {ABC826D4-2FA1-4F2F-87DE-E6095F653810} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} {F112851D-B023-4746-B6B1-8D2E5AD8F7AA} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} {6CB3EB30-F725-45C0-9742-42599BA8E8D2} = {74E681ED-FECC-4034-B9BD-01B0BB1BDECA} diff --git a/src/Orchard/Messaging/Events/IMessageEventHandler.cs b/src/Orchard/Messaging/Events/IMessageEventHandler.cs new file mode 100644 index 000000000..73ab95b6e --- /dev/null +++ b/src/Orchard/Messaging/Events/IMessageEventHandler.cs @@ -0,0 +1,9 @@ +using Orchard.Events; +using Orchard.Messaging.Models; + +namespace Orchard.Messaging.Events { + public interface IMessageEventHandler : IEventHandler { + void Sending(MessageContext context); + void Sent(MessageContext context); + } +} diff --git a/src/Orchard/Messaging/Models/Message.cs b/src/Orchard/Messaging/Models/Message.cs new file mode 100644 index 000000000..32b37c8d5 --- /dev/null +++ b/src/Orchard/Messaging/Models/Message.cs @@ -0,0 +1,11 @@ +using Orchard.ContentManagement.Records; + +namespace Orchard.Messaging.Models { + public class Message { + public string Service { get; set; } + public string Subject { get; set; } + public string Body { get; set; } + public string Type { get; set; } + public ContentItemRecord Recipient { get; set; } + } +} diff --git a/src/Orchard/Messaging/Models/MessageContext.cs b/src/Orchard/Messaging/Models/MessageContext.cs new file mode 100644 index 000000000..e6582f2ba --- /dev/null +++ b/src/Orchard/Messaging/Models/MessageContext.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using System.Net.Mail; + +namespace Orchard.Messaging.Models { + public class MessageContext { + public Dictionary Properties { get; private set; } + public Message Message { get; private set; } + public MailMessage MailMessage { get; private set; } + + public MessageContext(Message message) { + Properties = new Dictionary(); + Message = message; + MailMessage = new MailMessage {Body = message.Body, Subject = message.Subject}; + } + } +} diff --git a/src/Orchard/Messaging/Services/IMessageManager.cs b/src/Orchard/Messaging/Services/IMessageManager.cs new file mode 100644 index 000000000..a71395170 --- /dev/null +++ b/src/Orchard/Messaging/Services/IMessageManager.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Orchard.Messaging.Models; + +namespace Orchard.Messaging.Services { + public interface IMessageManager : IDependency { + /// + /// Sends a message without using the queue + /// + void Send(Message message); + + /// + /// Wether at least one channel is active on the current site + /// + bool HasChannels(); + + /// + /// Provides a list of all the current available channel services + /// + IEnumerable GetAvailableChannelServices(); + } +} diff --git a/src/Orchard/Messaging/Services/IMessagingChannel.cs b/src/Orchard/Messaging/Services/IMessagingChannel.cs new file mode 100644 index 000000000..59db18e7d --- /dev/null +++ b/src/Orchard/Messaging/Services/IMessagingChannel.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using Orchard.ContentManagement; +using Orchard.Messaging.Models; + +namespace Orchard.Messaging.Services { + public interface IMessagingChannel : IDependency { + /// + /// Actually sends the message though this channel + /// + void SendMessage(MessageContext message); + + /// + /// Sends a message to the recipient to validate his account + /// + void ValidateRecipient(ContentItem recipient); + + /// + /// Provides all the handled services, the user can choose from when receving messages + /// + IEnumerable GetAvailableServices(); + } +} diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index a5467f5c7..19a6caed8 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -371,6 +371,11 @@ + + + + + From e81ec58497012d884785312c7d13cb30dc52a33d Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 31 Aug 2010 12:13:55 -0700 Subject: [PATCH 02/13] Added unit tests for big text --HG-- branch : dev --- src/Orchard.Core.Tests/Body/BodyPartTests.cs | 100 ++++++++++++++ .../Orchard.Core.Tests.csproj | 1 + .../Builders/SessionFactoryBuilderTests.cs | 128 ++++-------------- src/Orchard.Tests/Data/ProviderUtilities.cs | 110 +++++++++++++++ src/Orchard.Tests/Data/ProvidersTests.cs | 60 ++++++++ .../Orchard.Framework.Tests.csproj | 3 + src/Orchard.Tests/Records/BigRecord.cs | 9 ++ 7 files changed, 308 insertions(+), 103 deletions(-) create mode 100644 src/Orchard.Core.Tests/Body/BodyPartTests.cs create mode 100644 src/Orchard.Tests/Data/ProviderUtilities.cs create mode 100644 src/Orchard.Tests/Data/ProvidersTests.cs create mode 100644 src/Orchard.Tests/Records/BigRecord.cs diff --git a/src/Orchard.Core.Tests/Body/BodyPartTests.cs b/src/Orchard.Core.Tests/Body/BodyPartTests.cs new file mode 100644 index 000000000..03319a0bf --- /dev/null +++ b/src/Orchard.Core.Tests/Body/BodyPartTests.cs @@ -0,0 +1,100 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Autofac; +using JetBrains.Annotations; +using Moq; +using NUnit.Framework; +using Orchard.ContentManagement; +using Orchard.ContentManagement.Drivers; +using Orchard.ContentManagement.Handlers; +using Orchard.ContentManagement.MetaData; +using Orchard.ContentManagement.Records; +using Orchard.Core.Common.Handlers; +using Orchard.Core.Common.Models; +using Orchard.Data; +using Orchard.Environment; +using Orchard.Security; +using Orchard.Tests.Modules; +using System.Web.Mvc; +using System.Web.Routing; +using Orchard.Tests.Stubs; +using Orchard.UI.Notify; + +namespace Orchard.Core.Tests.Body { + [TestFixture] + public class BodyPartTests : DatabaseEnabledTestsBase { + + public override void Register(ContainerBuilder builder) { + builder.RegisterType().As(); + builder.RegisterType().As(); + builder.RegisterInstance(new Mock().Object); + builder.RegisterInstance(new Mock().Object); + builder.RegisterInstance(new Mock().Object); + builder.RegisterInstance(new Mock().Object); + builder.RegisterType().As(); + + builder.RegisterType().As(); + + builder.RegisterType().As(); + builder.RegisterType().As(); + } + + [Test] + public void BodyCanHandleLongText() { + var contentManager = _container.Resolve(); + + contentManager.Create(ThingDriver.ContentType.Name, t => { + t.As().Record = new BodyPartRecord(); + t.Text = new String('x', 10000); + }); + + var bodies = contentManager.Query().List(); + Assert.That(bodies, Is.Not.Null); + Assert.That(bodies.Any(), Is.True); + Assert.That(bodies.First().Text, Is.EqualTo(new String('x', 10000))); + + } + + protected override IEnumerable DatabaseTypes { + get { + return new[] { + typeof(BodyPartRecord), + typeof(ContentTypeRecord), + typeof(ContentItemRecord), + typeof(ContentItemVersionRecord), + typeof(CommonPartRecord), + typeof(CommonPartVersionRecord), + }; + } + } + + [UsedImplicitly] + public class ThingHandler : ContentHandler { + public ThingHandler() { + Filters.Add(new ActivatingFilter(ThingDriver.ContentType.Name)); + Filters.Add(new ActivatingFilter>(ThingDriver.ContentType.Name)); + Filters.Add(new ActivatingFilter(ThingDriver.ContentType.Name)); + Filters.Add(new ActivatingFilter(ThingDriver.ContentType.Name)); + } + } + + public class Thing : ContentPart { + public int Id { get { return ContentItem.Id; } } + + public string Text { + get { return this.As().Text; } + set { this.As().Text = value; } + } + + } + + public class ThingDriver : ContentItemDriver { + public readonly static ContentType ContentType = new ContentType { + Name = "thing", + DisplayName = "Thing" + }; + } + + } +} \ No newline at end of file diff --git a/src/Orchard.Core.Tests/Orchard.Core.Tests.csproj b/src/Orchard.Core.Tests/Orchard.Core.Tests.csproj index 1c720d5d5..41c019947 100644 --- a/src/Orchard.Core.Tests/Orchard.Core.Tests.csproj +++ b/src/Orchard.Core.Tests/Orchard.Core.Tests.csproj @@ -101,6 +101,7 @@ + diff --git a/src/Orchard.Tests/Data/Builders/SessionFactoryBuilderTests.cs b/src/Orchard.Tests/Data/Builders/SessionFactoryBuilderTests.cs index e8e0f8799..014e0c911 100644 --- a/src/Orchard.Tests/Data/Builders/SessionFactoryBuilderTests.cs +++ b/src/Orchard.Tests/Data/Builders/SessionFactoryBuilderTests.cs @@ -14,122 +14,44 @@ using Orchard.Tests.Records; namespace Orchard.Tests.Data.Builders { [TestFixture] public class SessionFactoryBuilderTests { - private string _tempDataFolder; - - [SetUp] - public void Init() { - var tempFilePath = Path.GetTempFileName(); - File.Delete(tempFilePath); - Directory.CreateDirectory(tempFilePath); - _tempDataFolder = tempFilePath; - } - - [TearDown] - public void Term() { - try { Directory.Delete(_tempDataFolder, true); } - catch (IOException) { } - } - - private static void CreateSqlServerDatabase(string databasePath) { - var databaseName = Path.GetFileNameWithoutExtension(databasePath); - using (var connection = new SqlConnection( - "Data Source=.\\SQLEXPRESS;Initial Catalog=tempdb;Integrated Security=true;User Instance=True;")) { - connection.Open(); - using (var command = connection.CreateCommand()) { - command.CommandText = - "CREATE DATABASE " + databaseName + - " ON PRIMARY (NAME=" + databaseName + - ", FILENAME='" + databasePath.Replace("'", "''") + "')"; - command.ExecuteNonQuery(); - - command.CommandText = - "EXEC sp_detach_db '" + databaseName + "', 'true'"; - command.ExecuteNonQuery(); - } - } - } - - - [Test] public void SqlCeSchemaShouldBeGeneratedAndUsable() { - var recordDescriptors = new[] { - new RecordBlueprint {TableName = "Hello", Type = typeof (FooRecord)} - }; - - var parameters = new SessionFactoryParameters { - Provider = "SqlCe", - DataFolder = _tempDataFolder, - RecordDescriptors = recordDescriptors - }; - - var manager = (IDataServicesProviderFactory) new DataServicesProviderFactory(new[] { - new Meta( - (dataFolder, connectionString) => new SqlCeDataServicesProvider(dataFolder, connectionString), - new Dictionary {{"ProviderName", "SqlCe"}}) - }); - - var configuration = manager - .CreateProvider(parameters) - .BuildConfiguration(parameters); - - configuration.SetProperty("connection.release_mode", "on_close"); - - new SchemaExport(configuration).Execute(false, true, false); - - var sessionFactory = configuration.BuildSessionFactory(); - - var session = sessionFactory.OpenSession(); - var foo = new FooRecord {Name = "hi there", Id = 1}; - session.Save(foo); - session.Flush(); - session.Close(); - - Assert.That(foo, Is.Not.EqualTo(0)); - - sessionFactory.Close(); - - } - - [Test] - public void SqlServerSchemaShouldBeGeneratedAndUsable() { - var databasePath = Path.Combine(_tempDataFolder, "Orchard.mdf"); - CreateSqlServerDatabase(databasePath); - var recordDescriptors = new[] { new RecordBlueprint {TableName = "Hello", Type = typeof (FooRecord)} }; - var manager = (IDataServicesProviderFactory)new DataServicesProviderFactory(new[] { - new Meta( - (dataFolder, connectionString) => new SqlServerDataServicesProvider(dataFolder, connectionString), - new Dictionary {{"ProviderName", "SqlServer"}}) - }); - var parameters = new SessionFactoryParameters { - Provider = "SqlServer", - DataFolder = _tempDataFolder, - ConnectionString = "Data Source=.\\SQLEXPRESS;AttachDbFileName=" + databasePath + ";Integrated Security=True;User Instance=True;", - RecordDescriptors = recordDescriptors, - }; + ProviderUtilities.RunWithSqlCe(recordDescriptors, + sessionFactory => { + var session = sessionFactory.OpenSession(); + var foo = new FooRecord { Name = "hi there" }; + session.Save(foo); + session.Flush(); + session.Close(); - var configuration = manager - .CreateProvider(parameters) - .BuildConfiguration(parameters); + Assert.That(foo, Is.Not.EqualTo(0)); - new SchemaExport(configuration).Execute(false, true, false); + }); - var sessionFactory = configuration.BuildSessionFactory(); + } - var session = sessionFactory.OpenSession(); - var foo = new FooRecord { Name = "hi there" }; - session.Save(foo); - session.Flush(); - session.Close(); + [Test] + public void SqlServerSchemaShouldBeGeneratedAndUsable() { + var recordDescriptors = new[] { + new RecordBlueprint {TableName = "Hello", Type = typeof (FooRecord)} + }; - Assert.That(foo, Is.Not.EqualTo(0)); + ProviderUtilities.RunWithSqlServer(recordDescriptors, + sessionFactory => { + var session = sessionFactory.OpenSession(); + var foo = new FooRecord { Name = "hi there" }; + session.Save(foo); + session.Flush(); + session.Close(); - sessionFactory.Close(); + Assert.That(foo, Is.Not.EqualTo(0)); + + }); } } } \ No newline at end of file diff --git a/src/Orchard.Tests/Data/ProviderUtilities.cs b/src/Orchard.Tests/Data/ProviderUtilities.cs new file mode 100644 index 000000000..cf7b76f17 --- /dev/null +++ b/src/Orchard.Tests/Data/ProviderUtilities.cs @@ -0,0 +1,110 @@ +using System; +using System.Collections.Generic; +using System.Data.SqlClient; +using System.IO; +using Autofac.Features.Metadata; +using NHibernate; +using NHibernate.Tool.hbm2ddl; +using Orchard.Data.Providers; +using Orchard.Environment.ShellBuilders.Models; + +namespace Orchard.Tests.Data { + public class ProviderUtilities { + + public static void RunWithSqlServer(IEnumerable recordDescriptors, Action action) { + var temporaryPath = Path.GetTempFileName(); + if(File.Exists(temporaryPath)) + File.Delete(temporaryPath); + Directory.CreateDirectory(temporaryPath); + var databasePath = Path.Combine(temporaryPath, "Orchard.mdf"); + var databaseName = Path.GetFileNameWithoutExtension(databasePath); + try { + + // create database + using ( var connection = new SqlConnection( + "Data Source=.\\SQLEXPRESS;Initial Catalog=tempdb;Integrated Security=true;User Instance=True;") ) { + connection.Open(); + using ( var command = connection.CreateCommand() ) { + command.CommandText = + "CREATE DATABASE " + databaseName + + " ON PRIMARY (NAME=" + databaseName + + ", FILENAME='" + databasePath.Replace("'", "''") + "')"; + command.ExecuteNonQuery(); + + command.CommandText = + "EXEC sp_detach_db '" + databaseName + "', 'true'"; + command.ExecuteNonQuery(); + } + } + + var manager = (IDataServicesProviderFactory)new DataServicesProviderFactory(new[] { + new Meta( + (dataFolder, connectionString) => new SqlServerDataServicesProvider(dataFolder, connectionString), + new Dictionary {{"ProviderName", "SqlServer"}}) + }); + var parameters = new SessionFactoryParameters { + Provider = "SqlServer", + DataFolder = temporaryPath, + ConnectionString = "Data Source=.\\SQLEXPRESS;AttachDbFileName=" + databasePath + ";Integrated Security=True;User Instance=True;", + RecordDescriptors = recordDescriptors, + }; + + var configuration = manager + .CreateProvider(parameters) + .BuildConfiguration(parameters); + + new SchemaExport(configuration).Execute(false, true, false); + + using ( var sessionFactory = configuration.BuildSessionFactory() ) { + action(sessionFactory); + } + } + finally { + try { + Directory.Delete(temporaryPath, true); + } + catch (IOException) {} + } + } + + public static void RunWithSqlCe(IEnumerable recordDescriptors, Action action) { + var temporaryPath = Path.GetTempFileName(); + if ( File.Exists(temporaryPath) ) + File.Delete(temporaryPath); + Directory.CreateDirectory(temporaryPath); + var databasePath = Path.Combine(temporaryPath, "Orchard.mdf"); + var databaseName = Path.GetFileNameWithoutExtension(databasePath); + var parameters = new SessionFactoryParameters { + Provider = "SqlCe", + DataFolder = temporaryPath, + RecordDescriptors = recordDescriptors + }; + try { + var manager = (IDataServicesProviderFactory)new DataServicesProviderFactory(new[] { + new Meta( + (dataFolder, connectionString) => new SqlCeDataServicesProvider(dataFolder, connectionString), + new Dictionary {{"ProviderName", "SqlCe"}}) + }); + + var configuration = manager + .CreateProvider(parameters) + .BuildConfiguration(parameters); + + configuration.SetProperty("connection.release_mode", "on_close"); + + new SchemaExport(configuration).Execute(false, true, false); + + using ( var sessionFactory = configuration.BuildSessionFactory() ) { + action(sessionFactory); + } + + } + finally { + try { + Directory.Delete(temporaryPath, true); + } + catch (IOException) {} + } + } + } +} diff --git a/src/Orchard.Tests/Data/ProvidersTests.cs b/src/Orchard.Tests/Data/ProvidersTests.cs new file mode 100644 index 000000000..1f4dcaf1c --- /dev/null +++ b/src/Orchard.Tests/Data/ProvidersTests.cs @@ -0,0 +1,60 @@ +using System; +using NUnit.Framework; +using Orchard.Environment.ShellBuilders.Models; +using Orchard.Tests.Records; + +namespace Orchard.Tests.Data { + [TestFixture] + public class ProvidersTests { + + [Test] + public void SqlCeShouldHandleBigFields() { + + var recordDescriptors = new[] { + new RecordBlueprint {TableName = "Big", Type = typeof (BigRecord)} + }; + + ProviderUtilities.RunWithSqlCe(recordDescriptors, + sessionFactory => { + var session = sessionFactory.OpenSession(); + var foo = new BigRecord { Body = new String('x', 10000) }; + session.Save(foo); + session.Flush(); + session.Close(); + + session = sessionFactory.OpenSession(); + foo = session.Get(foo.Id); + session.Close(); + + Assert.That(foo, Is.Not.Null); + Assert.That(foo.Body, Is.EqualTo(new String('x', 10000))); + }); + } + + + [Test] + public void SqlServerShouldHandleBigFields() { + + var recordDescriptors = new[] { + new RecordBlueprint {TableName = "Big", Type = typeof (BigRecord)} + }; + + ProviderUtilities.RunWithSqlServer(recordDescriptors, + sessionFactory => { + var session = sessionFactory.OpenSession(); + var foo = new BigRecord { Body = new String('x', 10000) }; + session.Save(foo); + session.Flush(); + session.Close(); + + session = sessionFactory.OpenSession(); + foo = session.Get(foo.Id); + session.Close(); + + Assert.That(foo, Is.Not.Null); + Assert.That(foo.Body, Is.EqualTo(new String('x', 10000))); + + }); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Tests/Orchard.Framework.Tests.csproj b/src/Orchard.Tests/Orchard.Framework.Tests.csproj index 66858c551..14dd9970c 100644 --- a/src/Orchard.Tests/Orchard.Framework.Tests.csproj +++ b/src/Orchard.Tests/Orchard.Framework.Tests.csproj @@ -188,7 +188,9 @@ + + @@ -210,6 +212,7 @@ + diff --git a/src/Orchard.Tests/Records/BigRecord.cs b/src/Orchard.Tests/Records/BigRecord.cs new file mode 100644 index 000000000..2a5e1bfd2 --- /dev/null +++ b/src/Orchard.Tests/Records/BigRecord.cs @@ -0,0 +1,9 @@ +using Orchard.Data.Conventions; + +namespace Orchard.Tests.Records { + public class BigRecord { + public virtual int Id { get; set; } + [StringLengthMax] + public virtual string Body { get; set; } + } +} \ No newline at end of file From 6264af88882b5fdd09b54c55dc2391eea7bf816f Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 31 Aug 2010 12:57:15 -0700 Subject: [PATCH 03/13] Registration settings --HG-- branch : dev --- .../SchemaCommandGeneratorTests.cs | 3 -- .../Services/DefaultMessageManager.cs | 25 ++++++------ .../Services/EmailMessageEventHandler.cs | 5 +-- .../Services/EmailMessagingChannel.cs | 36 ++++------------- .../Controllers/AccountController.cs | 18 +++++++++ .../DataMigrations/UsersDataMigration.cs | 19 +++++++++ .../Handlers/ModerationMessageAlteration.cs | 38 ++++++++++++++++++ .../RegistrationSettingsPartHandler.cs | 15 +++++++ .../Orchard.Users/Models/MessageTypes.cs | 12 ++++++ .../Models/RegistrationSettingsPart.cs | 27 +++++++++++++ .../Models/RegistrationSettingsPartRecord.cs | 12 ++++++ .../Modules/Orchard.Users/Models/UserPart.cs | 10 +++++ .../Orchard.Users/Models/UserPartRecord.cs | 3 ++ .../Orchard.Users/Models/UserStatus.cs | 6 +++ .../Orchard.Users/Orchard.Users.csproj | 7 ++++ .../Services/MembershipService.cs | 40 +++++++++++++++++-- .../Parts/Users.RegistrationSettings.ascx | 25 ++++++++++++ .../Migration/Schema/AlterTableCommand.cs | 5 +++ .../Providers/SqlCeDataServicesProvider.cs | 2 +- src/Orchard/Messaging/Models/Message.cs | 11 ----- .../Messaging/Models/MessageContext.cs | 12 +++--- .../Messaging/Services/IMessageManager.cs | 6 +-- .../Messaging/Services/IMessagingChannel.cs | 5 --- src/Orchard/Orchard.Framework.csproj | 1 - 24 files changed, 266 insertions(+), 77 deletions(-) create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Handlers/ModerationMessageAlteration.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Handlers/RegistrationSettingsPartHandler.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Models/MessageTypes.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Models/RegistrationSettingsPart.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Models/RegistrationSettingsPartRecord.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Models/UserStatus.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Views/EditorTemplates/Parts/Users.RegistrationSettings.ascx delete mode 100644 src/Orchard/Messaging/Models/Message.cs diff --git a/src/Orchard.Tests/DataMigration/SchemaCommandGeneratorTests.cs b/src/Orchard.Tests/DataMigration/SchemaCommandGeneratorTests.cs index effe65baf..d5fb8dda1 100644 --- a/src/Orchard.Tests/DataMigration/SchemaCommandGeneratorTests.cs +++ b/src/Orchard.Tests/DataMigration/SchemaCommandGeneratorTests.cs @@ -5,7 +5,6 @@ using System.Data; using System.IO; using System.Linq; using System.Reflection; -using System.Web; using Autofac; using Autofac.Features.Metadata; using NHibernate; @@ -14,10 +13,8 @@ using Orchard.Caching; using Orchard.ContentManagement.Records; using Orchard.Data; using Orchard.Data.Conventions; -using Orchard.Data.Migration; using Orchard.Data.Migration.Generator; using Orchard.Data.Migration.Interpreters; -using Orchard.Data.Migration.Records; using Orchard.Data.Migration.Schema; using Orchard.DevTools.Services; using Orchard.Environment.Configuration; diff --git a/src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs b/src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs index 996f99e57..18797432e 100644 --- a/src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs +++ b/src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs @@ -4,32 +4,29 @@ using System.Linq; using JetBrains.Annotations; using Orchard.ContentManagement; using Orchard.Core.Messaging.Models; -using Orchard.Data; using Orchard.Logging; using Orchard.Messaging.Events; using Orchard.Messaging.Models; using Orchard.Messaging.Services; using Orchard.Settings; +using Orchard.ContentManagement.Records; namespace Orchard.Core.Messaging.Services { public class DefaultMessageManager : IMessageManager { private readonly IMessageEventHandler _messageEventHandler; private readonly IEnumerable _channels; - private readonly IRepository _messageRepository; protected virtual ISite CurrentSite { get; [UsedImplicitly] private set; } public ILogger Logger { get; set; } public DefaultMessageManager( IMessageEventHandler messageEventHandler, - IEnumerable channels, - IRepository messageRepository) { + IEnumerable channels) { _messageEventHandler = messageEventHandler; _channels = channels; - _messageRepository = messageRepository; } - public void Send(Message message) { + public void Send(ContentItemRecord recipient, string type, string service = null) { if ( !HasChannels() ) return; @@ -39,15 +36,19 @@ namespace Orchard.Core.Messaging.Services { return; } - Logger.Information("Sending message {0}", message.Type); + Logger.Information("Sending message {0}", type); try { // if the service is not explicit, use the default one, as per settings configuration - if ( String.IsNullOrWhiteSpace(message.Service) ) { - message.Service = messageSettings.DefaultChannelService; + if ( String.IsNullOrWhiteSpace(service) ) { + service = messageSettings.DefaultChannelService; } - var context = new MessageContext(message); + var context = new MessageContext { + Recipient = recipient, + Type = type, + Service = service + }; _messageEventHandler.Sending(context); @@ -57,10 +58,10 @@ namespace Orchard.Core.Messaging.Services { _messageEventHandler.Sent(context); - Logger.Information("Message {0} sent", message.Type); + Logger.Information("Message {0} sent", type); } catch ( Exception e ) { - Logger.Error(e, "An error occured while sending the message {0}", message.Type); + Logger.Error(e, "An error occured while sending the message {0}", type); } } diff --git a/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessageEventHandler.cs b/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessageEventHandler.cs index d4b87486c..e67ace915 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessageEventHandler.cs +++ b/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessageEventHandler.cs @@ -1,5 +1,4 @@ using Orchard.Messaging.Events; -using Orchard.Core.Messaging.Models; using Orchard.ContentManagement; using Orchard.Users.Models; using Orchard.Messaging.Models; @@ -13,7 +12,7 @@ namespace Orchard.Email.Services { } public void Sending(MessageContext context) { - var contentItem = _contentManager.Get(context.Message.Recipient.Id); + var contentItem = _contentManager.Get(context.Recipient.Id); if ( contentItem == null ) return; @@ -21,7 +20,7 @@ namespace Orchard.Email.Services { if ( recipient == null ) return; - context.Properties.Add(EmailMessagingChannel.EmailAddress, recipient.Email); + context.MailMessage.To.Add(recipient.Email); } public void Sent(MessageContext context) { diff --git a/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessagingChannel.cs b/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessagingChannel.cs index fe8bec169..ddff31e2a 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessagingChannel.cs +++ b/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessagingChannel.cs @@ -2,14 +2,10 @@ using System.Collections.Generic; using System.Net; using System.Net.Mail; -using System.Web; -using System.Web.Hosting; using JetBrains.Annotations; using Orchard.ContentManagement; using Orchard.Localization; using Orchard.Logging; -using Orchard.Core.Messaging.Models; -using Orchard.Core.Messaging.Services; using Orchard.Email.Models; using Orchard.Settings; using Orchard.Messaging.Services; @@ -19,7 +15,6 @@ namespace Orchard.Email.Services { public class EmailMessagingChannel : IMessagingChannel { public const string EmailService = "Email"; - public const string EmailAddress = "EmailAddress"; public EmailMessagingChannel() { Logger = NullLogger.Instance; @@ -30,7 +25,7 @@ namespace Orchard.Email.Services { public Localizer T { get; set; } public void SendMessage(MessageContext context) { - if ( context.Message.Service.ToLower() != EmailService ) + if ( !context.Service.Equals(EmailService, StringComparison.InvariantCultureIgnoreCase) ) return; var smtpSettings = CurrentSite.As(); @@ -45,9 +40,7 @@ namespace Orchard.Email.Services { smtpClient.Credentials = new NetworkCredential(smtpSettings.UserName, smtpSettings.Password); } - var emailAddress = context.Properties[EmailAddress]; - - if(String.IsNullOrWhiteSpace(emailAddress)) { + if(context.MailMessage.To.Count == 0) { Logger.Error("Recipient is missing an email address"); return; } @@ -59,33 +52,18 @@ namespace Orchard.Email.Services { smtpClient.EnableSsl = smtpSettings.EnableSsl; smtpClient.DeliveryMethod = SmtpDeliveryMethod.Network; - var message = new MailMessage { - From = new MailAddress(smtpSettings.Address), - Subject = context.Message.Subject ?? "", - Body = context.Message.Body ?? "", - IsBodyHtml = context.Message.Body != null && context.Message.Body.Contains("<") && context.Message.Body.Contains(">") - }; - - message.To.Add(emailAddress); + context.MailMessage.From = new MailAddress(smtpSettings.Address); + context.MailMessage.IsBodyHtml = context.MailMessage.Body != null && context.MailMessage.Body.Contains("<") && context.MailMessage.Body.Contains(">"); try { - smtpClient.Send(message); - Logger.Debug("Message sent to {0}: {1}", emailAddress, context.Message.Subject); + smtpClient.Send(context.MailMessage); + Logger.Debug("Message sent to {0}: {1}", context.MailMessage.To[0].Address, context.Type); } catch(Exception e) { - Logger.Error(e, "An unexpected error while sending a message to {0}: {1}", emailAddress, context.Message.Subject); + Logger.Error(e, "An unexpected error while sending a message to {0}: {1}", context.MailMessage.To[0].Address, context.Type); } } - public bool IsRecipientValidated(ContentItem contentItem) { - return false; - } - - public void ValidateRecipient(ContentItem contentItem) { - var context = new MessageContext(new Message { Recipient = contentItem.Record, Body = "Please validate your account", Service = "email", Subject = "Validate your account" } ); - SendMessage(context); - } - public IEnumerable GetAvailableServices() { return new[] {EmailService}; } diff --git a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs index e11486a81..13e867d2f 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs @@ -10,6 +10,11 @@ using Orchard.Mvc.ViewModels; using Orchard.Security; using Orchard.Users.Services; using Orchard.Users.ViewModels; +using Orchard.Settings; +using JetBrains.Annotations; +using Orchard.ContentManagement; +using Orchard.Users.Models; +using Orchard.Mvc.Results; namespace Orchard.Users.Controllers { [HandleError] @@ -31,6 +36,7 @@ namespace Orchard.Users.Controllers { public ILogger Logger { get; set; } public Localizer T { get; set; } + protected virtual ISite CurrentSite { get; [UsedImplicitly] private set; } public ActionResult AccessDenied() { var returnUrl = Request.QueryString["ReturnUrl"]; @@ -87,6 +93,12 @@ namespace Orchard.Users.Controllers { } public ActionResult Register() { + // ensure users can register + var registrationSettings = CurrentSite.As(); + if ( !registrationSettings.UsersCanRegister ) { + return new NotFoundResult(); + } + ViewData["PasswordLength"] = MinPasswordLength; return View(new BaseViewModel()); @@ -94,6 +106,12 @@ namespace Orchard.Users.Controllers { [HttpPost] public ActionResult Register(string userName, string email, string password, string confirmPassword) { + // ensure users can register + var registrationSettings = CurrentSite.As(); + if ( !registrationSettings.UsersCanRegister ) { + return new NotFoundResult(); + } + ViewData["PasswordLength"] = MinPasswordLength; if (ValidateRegistration(userName, email, password, confirmPassword)) { diff --git a/src/Orchard.Web/Modules/Orchard.Users/DataMigrations/UsersDataMigration.cs b/src/Orchard.Web/Modules/Orchard.Users/DataMigrations/UsersDataMigration.cs index f3fdeeed5..9daeef15c 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/DataMigrations/UsersDataMigration.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/DataMigrations/UsersDataMigration.cs @@ -18,5 +18,24 @@ namespace Orchard.Users.DataMigrations { return 1; } + + public int UpdateFrom1() { + + // Adds registration fields to previous versions + SchemaBuilder + .AlterTable("UserPartRecord", table => table.AddColumn("RegistrationStatus", c => c.WithDefault("'Approved'"))) + .AlterTable("UserPartRecord", table => table.AddColumn("EmailStatus", c => c.WithDefault("'Approved'"))); + + // Site Settings record + SchemaBuilder.CreateTable("RegistrationSettingsPartRecord", table => table + .ContentPartRecord() + .Column("UsersCanRegister", c => c.WithDefault("'0'")) + .Column("UsersMustValidateEmail", c => c.WithDefault("'0'")) + .Column("UsersAreModerated", c => c.WithDefault("'0'")) + .Column("NotifyModeration", c => c.WithDefault("'0'")) + ); + + return 2; + } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Handlers/ModerationMessageAlteration.cs b/src/Orchard.Web/Modules/Orchard.Users/Handlers/ModerationMessageAlteration.cs new file mode 100644 index 000000000..eb9a83ba7 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Handlers/ModerationMessageAlteration.cs @@ -0,0 +1,38 @@ +using Orchard.Messaging.Events; +using Orchard.Messaging.Models; +using Orchard.ContentManagement; +using Orchard.Users.Models; + +namespace Orchard.Users.Handlers { + public class ModerationMessageAlteration : IMessageEventHandler { + private readonly IContentManager _contentManager; + + public ModerationMessageAlteration(IContentManager contentManager) { + _contentManager = contentManager; + } + + public void Sending(MessageContext context) { + var contentItem = _contentManager.Get(context.Recipient.Id); + if ( contentItem == null ) + return; + + var recipient = contentItem.As(); + if ( recipient == null ) + return; + + if ( context.Type == MessageTypes.Moderation ) { + context.MailMessage.Subject = "User needs moderation"; + context.MailMessage.Body = string.Format("The following user account needs to be moderated: {0}", recipient.UserName); + } + + if ( context.Type == MessageTypes.Validation ) { + context.MailMessage.Subject = "User account validation"; + context.MailMessage.Body = string.Format("Dear {0}, please click on the folowwing link to validate you email address: {1}", recipient.UserName, "http://foo"); + } + + } + + public void Sent(MessageContext context) { + } + } +} diff --git a/src/Orchard.Web/Modules/Orchard.Users/Handlers/RegistrationSettingsPartHandler.cs b/src/Orchard.Web/Modules/Orchard.Users/Handlers/RegistrationSettingsPartHandler.cs new file mode 100644 index 000000000..7d9997758 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Handlers/RegistrationSettingsPartHandler.cs @@ -0,0 +1,15 @@ +using JetBrains.Annotations; +using Orchard.Data; +using Orchard.ContentManagement.Handlers; +using Orchard.Users.Models; + +namespace Orchard.Users.Handlers { + [UsedImplicitly] + public class RegistrationSettingsPartHandler : ContentHandler { + public RegistrationSettingsPartHandler(IRepository repository) { + Filters.Add(new ActivatingFilter("Site")); + Filters.Add(StorageFilter.For(repository)); + Filters.Add(new TemplateFilterForRecord("RegistrationSettings", "Parts/Users.RegistrationSettings")); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Models/MessageTypes.cs b/src/Orchard.Web/Modules/Orchard.Users/Models/MessageTypes.cs new file mode 100644 index 000000000..e63e44887 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Models/MessageTypes.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Web; + +namespace Orchard.Users.Models { + public static class MessageTypes { + public const string Moderation = "ORCHARD_USERS_MODERATION"; + public const string Validation = "ORCHARD_USERS_VALIDATION"; + + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Models/RegistrationSettingsPart.cs b/src/Orchard.Web/Modules/Orchard.Users/Models/RegistrationSettingsPart.cs new file mode 100644 index 000000000..2992b1d2a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Models/RegistrationSettingsPart.cs @@ -0,0 +1,27 @@ +using Orchard.ContentManagement; +using System; + +namespace Orchard.Users.Models { + public class RegistrationSettingsPart : ContentPart { + public bool UsersCanRegister { + get { return Record.UsersCanRegister; } + set { Record.UsersCanRegister = value; } + } + + public bool UsersMustValidateEmail { + get { return Record.UsersMustValidateEmail; } + set { Record.UsersMustValidateEmail = value; } + } + + public bool UsersAreModerated { + get { return Record.UsersAreModerated; } + set { Record.UsersAreModerated = value; } + } + + public bool NotifyModeration { + get { return Record.NotifyModeration; } + set { Record.NotifyModeration = value; } + } + + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Models/RegistrationSettingsPartRecord.cs b/src/Orchard.Web/Modules/Orchard.Users/Models/RegistrationSettingsPartRecord.cs new file mode 100644 index 000000000..3a2c5d3f2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Models/RegistrationSettingsPartRecord.cs @@ -0,0 +1,12 @@ +using System.Net.Mail; +using Orchard.ContentManagement.Records; +using System.ComponentModel.DataAnnotations; + +namespace Orchard.Users.Models { + public class RegistrationSettingsPartRecord : ContentPartRecord { + public virtual bool UsersCanRegister { get; set; } + public virtual bool UsersMustValidateEmail { get; set; } + public virtual bool UsersAreModerated { get; set; } + public virtual bool NotifyModeration { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Models/UserPart.cs b/src/Orchard.Web/Modules/Orchard.Users/Models/UserPart.cs index 99ed50bed..097e4a209 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Models/UserPart.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Models/UserPart.cs @@ -21,5 +21,15 @@ namespace Orchard.Users.Models { get { return Record.NormalizedUserName; } set { Record.NormalizedUserName = value; } } + + public UserStatus RegistrationStatus { + get { return Record.RegistrationStatus; } + set { Record.RegistrationStatus = value; } + } + + public UserStatus EmailStatus { + get { return Record.EmailStatus; } + set { Record.EmailStatus = value; } + } } } diff --git a/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs b/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs index 459081fdf..f9c7d29a4 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs @@ -11,5 +11,8 @@ namespace Orchard.Users.Models { public virtual MembershipPasswordFormat PasswordFormat { get; set; } public virtual string HashAlgorithm { get; set; } public virtual string PasswordSalt { get; set; } + + public virtual UserStatus RegistrationStatus { get; set; } + public virtual UserStatus EmailStatus { get; set; } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Models/UserStatus.cs b/src/Orchard.Web/Modules/Orchard.Users/Models/UserStatus.cs new file mode 100644 index 000000000..a41b43964 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Models/UserStatus.cs @@ -0,0 +1,6 @@ +namespace Orchard.Users.Models { + public enum UserStatus { + Pending, + Approved + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj b/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj index 8e819e458..f9f8fdead 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj +++ b/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj @@ -69,9 +69,15 @@ + + + + + + @@ -95,6 +101,7 @@ + diff --git a/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs b/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs index 9c432b86a..4b03289c5 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs @@ -10,20 +10,25 @@ using Orchard.ContentManagement; using Orchard.Security; using Orchard.Users.Drivers; using Orchard.Users.Models; +using Orchard.Settings; +using Orchard.Messaging.Services; namespace Orchard.Users.Services { [UsedImplicitly] public class MembershipService : IMembershipService { private readonly IContentManager _contentManager; + private readonly IMessageManager _messageManager; private readonly IRepository _userRepository; - public MembershipService(IContentManager contentManager, IRepository userRepository) { + public MembershipService(IContentManager contentManager, IRepository userRepository, IMessageManager messageManager ) { _contentManager = contentManager; _userRepository = userRepository; + _messageManager = messageManager; Logger = NullLogger.Instance; } public ILogger Logger { get; set; } + protected virtual ISite CurrentSite { get; [UsedImplicitly] private set; } public MembershipSettings GetSettings() { var settings = new MembershipSettings(); @@ -34,14 +39,35 @@ namespace Orchard.Users.Services { public IUser CreateUser(CreateUserParams createUserParams) { Logger.Information("CreateUser {0} {1}", createUserParams.Username, createUserParams.Email); - return _contentManager.Create(UserPartDriver.ContentType.Name, init => + var registrationSettings = CurrentSite.As(); + + var user = _contentManager.Create(UserPartDriver.ContentType.Name, init => { init.Record.UserName = createUserParams.Username; init.Record.Email = createUserParams.Email; init.Record.NormalizedUserName = createUserParams.Username.ToLower(); init.Record.HashAlgorithm = "SHA1"; SetPassword(init.Record, createUserParams.Password); + init.Record.RegistrationStatus = registrationSettings.UsersAreModerated ? UserStatus.Pending : UserStatus.Approved; + init.Record.EmailStatus = registrationSettings.UsersMustValidateEmail ? UserStatus.Pending : UserStatus.Approved; + }); + + if ( registrationSettings.UsersMustValidateEmail ) { + SendEmailValidationMessage(user); + } + + if ( registrationSettings.UsersAreModerated && registrationSettings.NotifyModeration ) { + var superUser = GetUser(CurrentSite.SuperUser); + if(superUser != null) + _messageManager.Send(superUser.ContentItem.Record, MessageTypes.Moderation); + } + + return user; + } + + public void SendEmailValidationMessage(IUser user) { + _messageManager.Send(user.ContentItem.Record, MessageTypes.Validation); } public IUser GetUser(string username) { @@ -58,16 +84,22 @@ namespace Orchard.Users.Services { var lowerName = userNameOrEmail == null ? "" : userNameOrEmail.ToLower(); var userRecord = _userRepository.Get(x => x.NormalizedUserName == lowerName); + if(userRecord == null) userRecord = _userRepository.Get(x => x.Email == lowerName); + if (userRecord == null || ValidatePassword(userRecord, password) == false) return null; + if ( userRecord.EmailStatus != UserStatus.Approved ) + return null; + + if ( userRecord.RegistrationStatus != UserStatus.Approved ) + return null; + return _contentManager.Get(userRecord.Id); } - - public void SetPassword(IUser user, string password) { if (!user.Is()) throw new InvalidCastException(); diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/EditorTemplates/Parts/Users.RegistrationSettings.ascx b/src/Orchard.Web/Modules/Orchard.Users/Views/EditorTemplates/Parts/Users.RegistrationSettings.ascx new file mode 100644 index 000000000..0b2ed2f7f --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/EditorTemplates/Parts/Users.RegistrationSettings.ascx @@ -0,0 +1,25 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Import Namespace="Orchard.Users.Models"%> +
+ <%: T("Users registration")%> +
+ <%: Html.EditorFor(m => m.UsersCanRegister) %> + + <%: Html.ValidationMessage("UsersCanRegister", "*")%> +
+
+ <%: Html.EditorFor(m => m.UsersMustValidateEmail)%> + + <%: Html.ValidationMessage("UsersMustValidateEmail", "*")%> +
+
+ <%: Html.EditorFor(m => m.UsersAreModerated)%> + + <%: Html.ValidationMessage("UsersAreModerated", "*")%> +
+
+ <%: Html.EditorFor(m => m.NotifyModeration)%> + + <%: Html.ValidationMessage("NotifyModeration", "*")%> +
+
\ No newline at end of file diff --git a/src/Orchard/Data/Migration/Schema/AlterTableCommand.cs b/src/Orchard/Data/Migration/Schema/AlterTableCommand.cs index 8c5576e3c..0b8d370e9 100644 --- a/src/Orchard/Data/Migration/Schema/AlterTableCommand.cs +++ b/src/Orchard/Data/Migration/Schema/AlterTableCommand.cs @@ -18,6 +18,11 @@ namespace Orchard.Data.Migration.Schema { TableCommands.Add(command); } + public void AddColumn(string columnName, Action column = null) { + var dbType = SchemaUtils.ToDbType(typeof(T)); + AddColumn(columnName, dbType, column); + } + public void DropColumn(string columnName) { var command = new DropColumnCommand(Name, columnName); TableCommands.Add(command); diff --git a/src/Orchard/Data/Providers/SqlCeDataServicesProvider.cs b/src/Orchard/Data/Providers/SqlCeDataServicesProvider.cs index a28cdf1b5..d372f7c21 100644 --- a/src/Orchard/Data/Providers/SqlCeDataServicesProvider.cs +++ b/src/Orchard/Data/Providers/SqlCeDataServicesProvider.cs @@ -83,7 +83,7 @@ namespace Orchard.Data.Providers { protected override void InitializeParameter(IDbDataParameter dbParam, string name, SqlType sqlType) { base.InitializeParameter(dbParam, name, sqlType); - if (sqlType.Length <= 4000) { + if ( sqlType.Length <= 4000 ) { return; } diff --git a/src/Orchard/Messaging/Models/Message.cs b/src/Orchard/Messaging/Models/Message.cs deleted file mode 100644 index 32b37c8d5..000000000 --- a/src/Orchard/Messaging/Models/Message.cs +++ /dev/null @@ -1,11 +0,0 @@ -using Orchard.ContentManagement.Records; - -namespace Orchard.Messaging.Models { - public class Message { - public string Service { get; set; } - public string Subject { get; set; } - public string Body { get; set; } - public string Type { get; set; } - public ContentItemRecord Recipient { get; set; } - } -} diff --git a/src/Orchard/Messaging/Models/MessageContext.cs b/src/Orchard/Messaging/Models/MessageContext.cs index e6582f2ba..63885700f 100644 --- a/src/Orchard/Messaging/Models/MessageContext.cs +++ b/src/Orchard/Messaging/Models/MessageContext.cs @@ -1,16 +1,18 @@ using System.Collections.Generic; using System.Net.Mail; +using Orchard.ContentManagement.Records; namespace Orchard.Messaging.Models { public class MessageContext { - public Dictionary Properties { get; private set; } - public Message Message { get; private set; } public MailMessage MailMessage { get; private set; } + public string Type { get; set; } + public string Service { get; set; } + public ContentItemRecord Recipient { get; set; } + public Dictionary Properties { get; private set; } - public MessageContext(Message message) { + public MessageContext() { Properties = new Dictionary(); - Message = message; - MailMessage = new MailMessage {Body = message.Body, Subject = message.Subject}; + MailMessage = new MailMessage(); } } } diff --git a/src/Orchard/Messaging/Services/IMessageManager.cs b/src/Orchard/Messaging/Services/IMessageManager.cs index a71395170..28a3becd2 100644 --- a/src/Orchard/Messaging/Services/IMessageManager.cs +++ b/src/Orchard/Messaging/Services/IMessageManager.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; -using Orchard.Messaging.Models; +using Orchard.ContentManagement.Records; namespace Orchard.Messaging.Services { public interface IMessageManager : IDependency { /// - /// Sends a message without using the queue + /// Sends a message to a channel /// - void Send(Message message); + void Send(ContentItemRecord recipient, string type, string service = null); /// /// Wether at least one channel is active on the current site diff --git a/src/Orchard/Messaging/Services/IMessagingChannel.cs b/src/Orchard/Messaging/Services/IMessagingChannel.cs index 59db18e7d..e5d33ff46 100644 --- a/src/Orchard/Messaging/Services/IMessagingChannel.cs +++ b/src/Orchard/Messaging/Services/IMessagingChannel.cs @@ -9,11 +9,6 @@ namespace Orchard.Messaging.Services { /// void SendMessage(MessageContext message); - /// - /// Sends a message to the recipient to validate his account - /// - void ValidateRecipient(ContentItem recipient); - /// /// Provides all the handled services, the user can choose from when receving messages /// diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 19a6caed8..091baccbf 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -372,7 +372,6 @@ - From 2aa5d366cf1e14c2c3a56c62aa9c048821343e16 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 31 Aug 2010 13:20:41 -0700 Subject: [PATCH 04/13] Manage users moderation from admin --HG-- branch : dev --- .../Content/Admin/images/offline.gif | Bin 0 -> 603 bytes .../Content/Admin/images/online.gif | Bin 0 -> 1024 bytes .../Controllers/AdminController.cs | 36 ++++++++++++++++++ .../Orchard.Users/Orchard.Users.csproj | 2 + .../Orchard.Users/Views/Admin/Index.aspx | 10 ++++- 5 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Content/Admin/images/offline.gif create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Content/Admin/images/online.gif diff --git a/src/Orchard.Web/Modules/Orchard.Users/Content/Admin/images/offline.gif b/src/Orchard.Web/Modules/Orchard.Users/Content/Admin/images/offline.gif new file mode 100644 index 0000000000000000000000000000000000000000..42c8bde22365a15c7481853c2ca8b74bdeccd86b GIT binary patch literal 603 zcmV-h0;K&%Nk%w1VGsZi0Oo!G&`&=9|Nrb?RQv1e=ft`Cos0I*&+d|s-g$J~rk3x? z#qhPY^rD{We|Ye=w(rWu`ns<2cVqRYp}tl*?u32uoR;ZhTlu-K`m3Gtt*Yu~T;pqD z`jdbAshs3^ap`+-?}mQyotEiaPwKF&=2%Sq+tj*JG|XmI_2%K+rHK_vq%`cyqs8K=tS1-fL>aWK8#we)x=b?URneXH4U6WAmq@ z@MKo~+}O-gJmywR?#aXSj)Bc!QT42$#85Q(>ge`_Z01%_=2}wVWnAuNQq5>s;(K-D zczO4ee&T<9)Ny6pYiRk?%=hT!+g3~NYF^f0RO_>^`lFHIsh-DcRQSWbz*#@yd3oWh zpxkb0__nC%dUe4~Gxn>Z?u>!$$;0@DaQw%*^`oESaAyDi{{R30A^8LW004dfEC2ui z01yBW000NLfPR95goT9z4r54V2x~Nkeg<(&Z)Q3{owSQ{bi@JcHQ2(@BY>)C;GQP+)#8n)N_NnX1VIi?@!m>pMCK0wyd22fB*fx z_U_W^%d^vtdfLylsXLe%w7}`vm&c_iG79&_H=HW2JeczMwH&1tr^~qxP9Eyf$_Ir3v=k*6Xe>%-tStT5mY*bX(50 zu-ZLoF$b(?oN3>FbL-phucw{u*mhxS!H&qXlbN%w^gev^uz6puT8nbY;gp#dyZvT6 z&N$t+`R3BRqsf&gvroT0kaN^~;(`9z*C%G4@S1hJebem~_N~_SJ2H!pr){~rYTL!F z`G*tpjwI$EO6)vRRj_2=xyu)ds zamk^`#n-0+BbQ+m;0*!ApDc_F4D%UuK&n7_f`Q{MgFUB=$A$z)m)5kBnTHkzu{Y0u zaN*?>O$P=o%LNZKZX~rbvaqmlC=@VoDoZfuelaLeXzXU{ZP}Cgp?$Joqq%{iM}R}~ zY?m&H3@Jy=wgVO#&n8%2YVV(~!qBwB@QL$-cE?GdPTaVdBHX~(UKiu2JfW@0)g_35 z;j<~v63#$1&X`9le580<7+zdxIvUhz6~+?QxM2AS0YUYUoP`V;ho$(L9CZv5)t#0Y z^6PnIHmi5fY}99(8~CV|o70R>C*@Y~hi+j7(TbLeLoQ7c9n+t1RE0b~$0jDwEY8Eh GU=0BKCVeOX literal 0 HcmV?d00001 diff --git a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs index ab768eab5..bdc91bb9d 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs @@ -1,8 +1,10 @@ using System.Linq; using System.Web.Mvc; +using JetBrains.Annotations; using Orchard.ContentManagement; using Orchard.Localization; using Orchard.Security; +using Orchard.Settings; using Orchard.UI.Notify; using Orchard.Users.Drivers; using Orchard.Users.Models; @@ -27,6 +29,7 @@ namespace Orchard.Users.Controllers { public IOrchardServices Services { get; set; } public Localizer T { get; set; } + protected virtual ISite CurrentSite { get; [UsedImplicitly] private set; } public ActionResult Index() { if (!Services.Authorizer.Authorize(Permissions.ManageUsers, T("Not authorized to list users"))) @@ -145,6 +148,39 @@ namespace Orchard.Users.Controllers { return RedirectToAction("Index"); } + public ActionResult Approve(int id) { + if ( !Services.Authorizer.Authorize(Permissions.ManageUsers, T("Not authorized to manage users")) ) + return new HttpUnauthorizedResult(); + + var user = Services.ContentManager.Get(id); + + if ( user != null ) { + user.As().RegistrationStatus = UserStatus.Approved; + Services.Notifier.Information(T("User approved")); + } + + return RedirectToAction("Index"); + } + + public ActionResult Moderate(int id) { + if ( !Services.Authorizer.Authorize(Permissions.ManageUsers, T("Not authorized to manage users")) ) + return new HttpUnauthorizedResult(); + + var user = Services.ContentManager.Get(id); + + if ( user != null ) { + if ( CurrentSite.SuperUser.Equals(user.As().UserName) ) { + Services.Notifier.Error(T("Super user can't be moderated")); + } + else { + user.As().RegistrationStatus = UserStatus.Pending; + Services.Notifier.Information(T("User moderated")); + } + } + + return RedirectToAction("Index"); + } + bool IUpdateModel.TryUpdateModel(TModel model, string prefix, string[] includeProperties, string[] excludeProperties) { return TryUpdateModel(model, prefix, includeProperties, excludeProperties); } diff --git a/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj b/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj index f9f8fdead..1a605f92f 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj +++ b/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj @@ -90,6 +90,8 @@ + + diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/Admin/Index.aspx b/src/Orchard.Web/Modules/Orchard.Users/Views/Admin/Index.aspx index 41cd5b2ed..4bd5fad5b 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Views/Admin/Index.aspx +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/Admin/Index.aspx @@ -1,4 +1,5 @@ <%@ Page Language="C#" Inherits="Orchard.Mvc.ViewPage" %> +<%@ Import Namespace="Orchard.Users.Models" %> <%@ Import Namespace="Orchard.Users.ViewModels"%>

<%: Html.TitleForPage(T("Manage Users").ToString()) %>

<% using (Html.BeginFormAntiForgeryPost()) { %> @@ -22,6 +23,12 @@ { %> + <% if(row.UserPart.RegistrationStatus == UserStatus.Approved) { %> + " alt="<%:T("Approved") %>" title="<%:T("User is approved") %>" /> + <% } + else { %> + " alt="<%:T("Moderated") %>" title="<%:T("User is moderated") %>" /> + <% } %> <%: row.UserPart.UserName %> @@ -29,7 +36,8 @@ <%: Html.ActionLink(T("Edit").ToString(), "Edit", new { row.UserPart.Id })%> | - <%: Html.ActionLink(T("Remove").ToString(), "Delete", new { row.UserPart.Id })%> + <%: Html.ActionLink(T("Remove").ToString(), "Delete", new { row.UserPart.Id })%> | + <%: row.UserPart.RegistrationStatus == UserStatus.Pending ? Html.ActionLink(T("Approve").ToString(), "Approve", new { row.UserPart.Id }) : Html.ActionLink(T("Moderate").ToString(), "Moderate", new { row.UserPart.Id })%> <%}%> From 067c5db740678bdabfb9fcc4fe49ecf3fd57a9a5 Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Tue, 31 Aug 2010 14:14:14 -0700 Subject: [PATCH 05/13] EventHandler for User creation - Could allow a module to provide some logic to prevent specific users from registering --HG-- branch : dev --- .../Orchard.Users/Events/IUserEventHandler.cs | 16 ++++++++ .../Orchard.Users/Events/UserContext.cs | 8 ++++ .../Orchard.Users/Orchard.Users.csproj | 2 + .../Services/MembershipService.cs | 40 ++++++++++++++----- 4 files changed, 55 insertions(+), 11 deletions(-) create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Events/IUserEventHandler.cs create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Events/UserContext.cs diff --git a/src/Orchard.Web/Modules/Orchard.Users/Events/IUserEventHandler.cs b/src/Orchard.Web/Modules/Orchard.Users/Events/IUserEventHandler.cs new file mode 100644 index 000000000..79b0962ed --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Events/IUserEventHandler.cs @@ -0,0 +1,16 @@ +using Orchard.Events; + +namespace Orchard.Users.Events { + public interface IUserEventHandler : IEventHandler { + /// + /// Called before a User is created + /// + void Creating(UserContext context); + + /// + /// Called once a user has been created + /// + void Created(UserContext context); + } +} + diff --git a/src/Orchard.Web/Modules/Orchard.Users/Events/UserContext.cs b/src/Orchard.Web/Modules/Orchard.Users/Events/UserContext.cs new file mode 100644 index 000000000..9ae60fae2 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Events/UserContext.cs @@ -0,0 +1,8 @@ +using Orchard.Users.Models; + +namespace Orchard.Users.Events { + public class UserContext { + public UserPart User { get; set; } + public bool Cancel { get; set; } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj b/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj index 1a605f92f..465342676 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj +++ b/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj @@ -69,8 +69,10 @@ + + diff --git a/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs b/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs index 4b03289c5..6431db7f4 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs @@ -9,21 +9,26 @@ using Orchard.Logging; using Orchard.ContentManagement; using Orchard.Security; using Orchard.Users.Drivers; +using Orchard.Users.Events; using Orchard.Users.Models; using Orchard.Settings; using Orchard.Messaging.Services; +using System.Collections; +using System.Collections.Generic; namespace Orchard.Users.Services { [UsedImplicitly] public class MembershipService : IMembershipService { private readonly IContentManager _contentManager; private readonly IMessageManager _messageManager; + private readonly IEnumerable _userEventHandlers; private readonly IRepository _userRepository; - public MembershipService(IContentManager contentManager, IRepository userRepository, IMessageManager messageManager ) { + public MembershipService(IContentManager contentManager, IRepository userRepository, IMessageManager messageManager, IEnumerable userEventHandlers) { _contentManager = contentManager; _userRepository = userRepository; _messageManager = messageManager; + _userEventHandlers = userEventHandlers; Logger = NullLogger.Instance; } @@ -41,17 +46,30 @@ namespace Orchard.Users.Services { var registrationSettings = CurrentSite.As(); - var user = _contentManager.Create(UserPartDriver.ContentType.Name, init => - { - init.Record.UserName = createUserParams.Username; - init.Record.Email = createUserParams.Email; - init.Record.NormalizedUserName = createUserParams.Username.ToLower(); - init.Record.HashAlgorithm = "SHA1"; - SetPassword(init.Record, createUserParams.Password); - init.Record.RegistrationStatus = registrationSettings.UsersAreModerated ? UserStatus.Pending : UserStatus.Approved; - init.Record.EmailStatus = registrationSettings.UsersMustValidateEmail ? UserStatus.Pending : UserStatus.Approved; + var user = _contentManager.New(UserPartDriver.ContentType.Name); - }); + user.Record.UserName = createUserParams.Username; + user.Record.Email = createUserParams.Email; + user.Record.NormalizedUserName = createUserParams.Username.ToLower(); + user.Record.HashAlgorithm = "SHA1"; + SetPassword(user.Record, createUserParams.Password); + user.Record.RegistrationStatus = registrationSettings.UsersAreModerated ? UserStatus.Pending : UserStatus.Approved; + user.Record.EmailStatus = registrationSettings.UsersMustValidateEmail ? UserStatus.Pending : UserStatus.Approved; + + var userContext = new UserContext {User = user, Cancel = false}; + foreach(var userEventHandler in _userEventHandlers) { + userEventHandler.Creating(userContext); + } + + if(userContext.Cancel) { + return null; + } + + _contentManager.Create(user); + + foreach ( var userEventHandler in _userEventHandlers ) { + userEventHandler.Created(userContext); + } if ( registrationSettings.UsersMustValidateEmail ) { SendEmailValidationMessage(user); From 98d11e81f007e5c0e748cac820e2e8f7ec34500a Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 1 Sep 2010 14:39:28 -0700 Subject: [PATCH 06/13] Added email challenge --HG-- branch : dev --- .../Services/DefaultMessageManager.cs | 9 +- .../Controllers/AccountController.cs | 25 +++- .../Controllers/AdminController.cs | 1 + .../DataMigrations/UsersDataMigration.cs | 3 +- .../Handlers/ModerationMessageAlteration.cs | 2 +- .../Orchard.Users/Models/UserPartRecord.cs | 1 + .../Orchard.Users/Orchard.Users.csproj | 4 + .../Services/MembershipService.cs | 68 ++++++++-- .../Views/Account/ChallengeEmailFail.ascx | 3 + .../Views/Account/ChallengeEmailSent.ascx | 3 + .../Views/Account/ChallengeEmailSuccess.ascx | 3 + src/Orchard.Web/Web.config | 31 +---- .../Messaging/Services/IMessageManager.cs | 2 +- src/Orchard/Orchard.Framework.csproj | 4 +- src/Orchard/Security/IMembershipService.cs | 8 +- .../Providers/OrchardMembershipProvider.cs | 127 ------------------ 16 files changed, 119 insertions(+), 175 deletions(-) create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailFail.ascx create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSent.ascx create mode 100644 src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSuccess.ascx delete mode 100644 src/Orchard/Security/Providers/OrchardMembershipProvider.cs diff --git a/src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs b/src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs index 18797432e..8ed59540f 100644 --- a/src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs +++ b/src/Orchard.Web/Core/Messaging/Services/DefaultMessageManager.cs @@ -26,7 +26,7 @@ namespace Orchard.Core.Messaging.Services { _channels = channels; } - public void Send(ContentItemRecord recipient, string type, string service = null) { + public void Send(ContentItemRecord recipient, string type, string service = null, Dictionary properties = null) { if ( !HasChannels() ) return; @@ -40,7 +40,7 @@ namespace Orchard.Core.Messaging.Services { try { // if the service is not explicit, use the default one, as per settings configuration - if ( String.IsNullOrWhiteSpace(service) ) { + if (String.IsNullOrWhiteSpace(service)) { service = messageSettings.DefaultChannelService; } @@ -50,6 +50,11 @@ namespace Orchard.Core.Messaging.Services { Service = service }; + if ( properties != null ) { + foreach (var key in properties.Keys) + context.Properties.Add(key, properties[key]); + } + _messageEventHandler.Sending(context); foreach ( var channel in _channels ) { diff --git a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs index 13e867d2f..87a18ec00 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs @@ -116,10 +116,16 @@ namespace Orchard.Users.Controllers { if (ValidateRegistration(userName, email, password, confirmPassword)) { // Attempt to register the user - var user = _membershipService.CreateUser(new CreateUserParams(userName, password, email, null, null, true)); - + var user = _membershipService.CreateUser(new CreateUserParams(userName, password, email, null, null, false)); if (user != null) { + if ( user.As().EmailStatus == UserStatus.Pending ) { + string challengeToken = _membershipService.GetEncryptedChallengeToken(user.As()); + _membershipService.SendChallengeEmail(user.As(), Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new { Area = "Orchard.Users", token = challengeToken }))); + + return RedirectToAction("ChallengeEmailSent"); + } + _authenticationService.SignIn(user, false /* createPersistentCookie */); return Redirect("~/"); } @@ -173,6 +179,21 @@ namespace Orchard.Users.Controllers { return View(new BaseViewModel()); } + public ActionResult ChallengeEmailSent() { + return View(new BaseViewModel()); + } + + public ActionResult ChallengeEmail(string token) { + var user = _membershipService.ValidateChallengeToken(token); + + if ( user != null ) { + _authenticationService.SignIn(user, false /* createPersistentCookie */); + return View("ChallengeEmailSuccess"); + } + + return View("ChallengeEmailFail"); + } + protected override void OnActionExecuting(ActionExecutingContext filterContext) { if (filterContext.HttpContext.User.Identity is WindowsIdentity) { throw new InvalidOperationException("Windows authentication is not supported."); diff --git a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs index bdc91bb9d..05119e766 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs @@ -10,6 +10,7 @@ using Orchard.Users.Drivers; using Orchard.Users.Models; using Orchard.Users.Services; using Orchard.Users.ViewModels; +using Orchard.Mvc.Extensions; namespace Orchard.Users.Controllers { [ValidateInput(false)] diff --git a/src/Orchard.Web/Modules/Orchard.Users/DataMigrations/UsersDataMigration.cs b/src/Orchard.Web/Modules/Orchard.Users/DataMigrations/UsersDataMigration.cs index 9daeef15c..95b725ffb 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/DataMigrations/UsersDataMigration.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/DataMigrations/UsersDataMigration.cs @@ -24,7 +24,8 @@ namespace Orchard.Users.DataMigrations { // Adds registration fields to previous versions SchemaBuilder .AlterTable("UserPartRecord", table => table.AddColumn("RegistrationStatus", c => c.WithDefault("'Approved'"))) - .AlterTable("UserPartRecord", table => table.AddColumn("EmailStatus", c => c.WithDefault("'Approved'"))); + .AlterTable("UserPartRecord", table => table.AddColumn("EmailStatus", c => c.WithDefault("'Approved'"))) + .AlterTable("UserPartRecord", table => table.AddColumn("EmailChallengeToken")); // Site Settings record SchemaBuilder.CreateTable("RegistrationSettingsPartRecord", table => table diff --git a/src/Orchard.Web/Modules/Orchard.Users/Handlers/ModerationMessageAlteration.cs b/src/Orchard.Web/Modules/Orchard.Users/Handlers/ModerationMessageAlteration.cs index eb9a83ba7..ac2b0d46a 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Handlers/ModerationMessageAlteration.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Handlers/ModerationMessageAlteration.cs @@ -27,7 +27,7 @@ namespace Orchard.Users.Handlers { if ( context.Type == MessageTypes.Validation ) { context.MailMessage.Subject = "User account validation"; - context.MailMessage.Body = string.Format("Dear {0}, please click on the folowwing link to validate you email address: {1}", recipient.UserName, "http://foo"); + context.MailMessage.Body = string.Format("Dear {0}, please click here to validate you email address.", recipient.UserName, context.Properties["ChallengeUrl"]); } } diff --git a/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs b/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs index f9c7d29a4..c6c8c2cff 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs @@ -14,5 +14,6 @@ namespace Orchard.Users.Models { public virtual UserStatus RegistrationStatus { get; set; } public virtual UserStatus EmailStatus { get; set; } + public virtual string EmailChallengeToken { get; set; } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj b/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj index 465342676..8c037b7ac 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj +++ b/src/Orchard.Web/Modules/Orchard.Users/Orchard.Users.csproj @@ -45,6 +45,7 @@ 3.5 + @@ -98,6 +99,9 @@ + + + diff --git a/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs b/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs index 6431db7f4..046077d03 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs @@ -1,8 +1,10 @@ using System; +using System.Globalization; using System.Linq; using System.Security.Cryptography; using System.Text; using System.Web.Security; +using System.Xml.Linq; using JetBrains.Annotations; using Orchard.Data; using Orchard.Logging; @@ -13,12 +15,13 @@ using Orchard.Users.Events; using Orchard.Users.Models; using Orchard.Settings; using Orchard.Messaging.Services; -using System.Collections; using System.Collections.Generic; +using System.Web.Mvc; namespace Orchard.Users.Services { [UsedImplicitly] public class MembershipService : IMembershipService { + private static readonly TimeSpan DelayToValidate = new TimeSpan(7, 0, 0, 0); // one week to validate email private readonly IContentManager _contentManager; private readonly IMessageManager _messageManager; private readonly IEnumerable _userEventHandlers; @@ -53,8 +56,8 @@ namespace Orchard.Users.Services { user.Record.NormalizedUserName = createUserParams.Username.ToLower(); user.Record.HashAlgorithm = "SHA1"; SetPassword(user.Record, createUserParams.Password); - user.Record.RegistrationStatus = registrationSettings.UsersAreModerated ? UserStatus.Pending : UserStatus.Approved; - user.Record.EmailStatus = registrationSettings.UsersMustValidateEmail ? UserStatus.Pending : UserStatus.Approved; + user.Record.RegistrationStatus = registrationSettings.UsersAreModerated && !createUserParams.IsApproved ? UserStatus.Pending : UserStatus.Approved; + user.Record.EmailStatus = registrationSettings.UsersMustValidateEmail && !createUserParams.IsApproved ? UserStatus.Pending : UserStatus.Approved; var userContext = new UserContext {User = user, Cancel = false}; foreach(var userEventHandler in _userEventHandlers) { @@ -71,11 +74,7 @@ namespace Orchard.Users.Services { userEventHandler.Created(userContext); } - if ( registrationSettings.UsersMustValidateEmail ) { - SendEmailValidationMessage(user); - } - - if ( registrationSettings.UsersAreModerated && registrationSettings.NotifyModeration ) { + if ( registrationSettings.UsersAreModerated && registrationSettings.NotifyModeration && !createUserParams.IsApproved ) { var superUser = GetUser(CurrentSite.SuperUser); if(superUser != null) _messageManager.Send(superUser.ContentItem.Record, MessageTypes.Moderation); @@ -84,8 +83,52 @@ namespace Orchard.Users.Services { return user; } - public void SendEmailValidationMessage(IUser user) { - _messageManager.Send(user.ContentItem.Record, MessageTypes.Validation); + public void SendChallengeEmail(UserPart user, string url) { + _messageManager.Send(user.ContentItem.Record, MessageTypes.Validation, "Email", new Dictionary { { "ChallengeUrl", url } }); + } + + public IUser ValidateChallengeToken(string challengeToken) { + string username; + DateTime validateByUtc; + + if(!DecryptChallengeToken(challengeToken, out username, out validateByUtc)) { + return null; + } + + if ( validateByUtc < DateTime.UtcNow ) + return null; + + var user = GetUser(username); + if ( user == null ) + return null; + + user.As().EmailStatus = UserStatus.Approved; + + return user; + } + + public string GetEncryptedChallengeToken(UserPart user) { + var challengeToken = new XElement("Token", new XAttribute("username", user.UserName), new XAttribute("validate-by-utc", DateTime.UtcNow.Add(DelayToValidate).ToString(CultureInfo.InvariantCulture))).ToString(); + var data = Encoding.UTF8.GetBytes(challengeToken); + return MachineKey.Encode(data, MachineKeyProtection.All); + } + + private static bool DecryptChallengeToken(string challengeToken, out string username, out DateTime validateByUtc) { + username = null; + validateByUtc = DateTime.UtcNow; + + try { + var data = MachineKey.Decode(challengeToken, MachineKeyProtection.All); + var xml = Encoding.UTF8.GetString(data); + var element = XElement.Parse(xml); + username = element.Attribute("username").Value; + validateByUtc = DateTime.Parse(element.Attribute("validate-by-utc").Value, CultureInfo.InvariantCulture); + return true; + } + catch { + return false; + } + } public IUser GetUser(string username) { @@ -143,7 +186,7 @@ namespace Orchard.Users.Services { } } - private bool ValidatePassword(UserPartRecord partRecord, string password) { + private static bool ValidatePassword(UserPartRecord partRecord, string password) { // Note - the password format stored with the record is used // otherwise changing the password format on the site would invalidate // all logins @@ -197,7 +240,7 @@ namespace Orchard.Users.Services { var hashAlgorithm = HashAlgorithm.Create(partRecord.HashAlgorithm); var hashBytes = hashAlgorithm.ComputeHash(combinedBytes); - + return partRecord.Password == Convert.ToBase64String(hashBytes); } @@ -208,5 +251,6 @@ namespace Orchard.Users.Services { private static bool ValidatePasswordEncrypted(UserPartRecord partRecord, string password) { throw new NotImplementedException(); } + } } diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailFail.ascx b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailFail.ascx new file mode 100644 index 000000000..dbbd4a223 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailFail.ascx @@ -0,0 +1,3 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +

<%: Html.TitleForPage(T("Challenge Email").ToString()) %>

+

<%: T("Your email address could not be validated.") %>

diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSent.ascx b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSent.ascx new file mode 100644 index 000000000..1f4c973ea --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSent.ascx @@ -0,0 +1,3 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +

<%: Html.TitleForPage(T("Challenge Email Sent").ToString()) %>

+

<%: T("An email has been sent to you. Please click on the link it contains in order to have access on this site.") %>

diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSuccess.ascx b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSuccess.ascx new file mode 100644 index 000000000..c2f9d0734 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSuccess.ascx @@ -0,0 +1,3 @@ +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +

<%: Html.TitleForPage(T("Challenge Email").ToString()) %>

+

<%: T("Your email address has been validated.") %>

diff --git a/src/Orchard.Web/Web.config b/src/Orchard.Web/Web.config index 20f62d942..a11a2a1d0 100644 --- a/src/Orchard.Web/Web.config +++ b/src/Orchard.Web/Web.config @@ -19,6 +19,10 @@ + - - - - - - - - - diff --git a/src/Orchard/Messaging/Services/IMessageManager.cs b/src/Orchard/Messaging/Services/IMessageManager.cs index 28a3becd2..efcbad622 100644 --- a/src/Orchard/Messaging/Services/IMessageManager.cs +++ b/src/Orchard/Messaging/Services/IMessageManager.cs @@ -6,7 +6,7 @@ namespace Orchard.Messaging.Services { /// /// Sends a message to a channel /// - void Send(ContentItemRecord recipient, string type, string service = null); + void Send(ContentItemRecord recipient, string type, string service = null, Dictionary properties = null); /// /// Wether at least one channel is active on the current site diff --git a/src/Orchard/Orchard.Framework.csproj b/src/Orchard/Orchard.Framework.csproj index 091baccbf..379d9c615 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -93,6 +93,9 @@ False ..\..\lib\linqnhibernate\NHibernate.Linq.dll + + ..\Orchard.Web\Modules\Orchard.Email\Bin\Orchard.Users.dll + 3.5 @@ -746,7 +749,6 @@ - diff --git a/src/Orchard/Security/IMembershipService.cs b/src/Orchard/Security/IMembershipService.cs index 0821fcdb1..3f0914c5d 100644 --- a/src/Orchard/Security/IMembershipService.cs +++ b/src/Orchard/Security/IMembershipService.cs @@ -1,4 +1,6 @@ -namespace Orchard.Security { +using Orchard.Users.Models; + +namespace Orchard.Security { public interface IMembershipService : IDependency { MembershipSettings GetSettings(); @@ -7,5 +9,9 @@ IUser ValidateUser(string userNameOrEmail, string password); void SetPassword(IUser user, string password); + + IUser ValidateChallengeToken(string challengeToken); + void SendChallengeEmail(UserPart user, string url); + string GetEncryptedChallengeToken(UserPart user); } } diff --git a/src/Orchard/Security/Providers/OrchardMembershipProvider.cs b/src/Orchard/Security/Providers/OrchardMembershipProvider.cs deleted file mode 100644 index 5f09ddb45..000000000 --- a/src/Orchard/Security/Providers/OrchardMembershipProvider.cs +++ /dev/null @@ -1,127 +0,0 @@ -using System; -using System.Web.Security; -using Orchard.Environment; - -namespace Orchard.Security.Providers { - public class OrchardMembershipProvider : MembershipProvider { - - static IMembershipService GetService() { - throw new NotImplementedException("The OrchardMemberShipProvider is not supported anymore. Use the IMembershipService interface instead."); - } - - static MembershipSettings GetSettings() { - return GetService().GetSettings(); - } - - private MembershipUser BuildMembershipUser(IUser user) { - return new MembershipUser(Name, - user.UserName, - user.Id, - user.Email, - null, - null, - true, - false, - DateTime.UtcNow, - DateTime.UtcNow, - DateTime.UtcNow, - DateTime.UtcNow, - DateTime.UtcNow); - } - - public override MembershipUser CreateUser(string username, string password, string email, string passwordQuestion, string passwordAnswer, bool isApproved, object providerUserKey, out MembershipCreateStatus status) { - var user = GetService().CreateUser(new CreateUserParams(username, password, email, passwordQuestion, passwordAnswer, isApproved)); - - if (user == null) { - status = MembershipCreateStatus.ProviderError; - return null; - } - - status = MembershipCreateStatus.Success; - return BuildMembershipUser(user); - } - - - public override bool ChangePasswordQuestionAndAnswer(string username, string password, string newPasswordQuestion, string newPasswordAnswer) { - throw new NotImplementedException(); - } - - public override string GetPassword(string username, string answer) { - throw new NotImplementedException(); - } - - public override bool ChangePassword(string username, string oldPassword, string newPassword) { - var service = GetService(); - var user = service.ValidateUser(username, oldPassword); - if (user == null) - return false; - - service.SetPassword(user, newPassword); - return true; - } - - public override string ResetPassword(string username, string answer) { - throw new NotImplementedException(); - } - - public override void UpdateUser(MembershipUser user) { - throw new NotImplementedException(); - } - - public override bool ValidateUser(string username, string password) { - return (GetService().ValidateUser(username, password) != null); - } - - public override bool UnlockUser(string userName) { - throw new NotImplementedException(); - } - - public override MembershipUser GetUser(object providerUserKey, bool userIsOnline) { - throw new NotImplementedException(); - } - - public override MembershipUser GetUser(string username, bool userIsOnline) { - throw new NotImplementedException(); - } - - public override string GetUserNameByEmail(string email) { - throw new NotImplementedException(); - } - - public override bool DeleteUser(string username, bool deleteAllRelatedData) { - throw new NotImplementedException(); - } - - public override MembershipUserCollection GetAllUsers(int pageIndex, int pageSize, out int totalRecords) { - throw new NotImplementedException(); - } - - public override int GetNumberOfUsersOnline() { - throw new NotImplementedException(); - } - - public override MembershipUserCollection FindUsersByName(string usernameToMatch, int pageIndex, int pageSize, out int totalRecords) { - throw new NotImplementedException(); - } - - public override MembershipUserCollection FindUsersByEmail(string emailToMatch, int pageIndex, int pageSize, out int totalRecords) { - throw new NotImplementedException(); - } - - public override string ApplicationName { - get { throw new NotImplementedException(); } - set { throw new NotImplementedException(); } - } - - public override bool EnablePasswordRetrieval { get { return GetSettings().EnablePasswordRetrieval; } } - public override bool EnablePasswordReset { get { return GetSettings().EnablePasswordReset; } } - public override bool RequiresQuestionAndAnswer { get { return GetSettings().RequiresQuestionAndAnswer; } } - public override int MaxInvalidPasswordAttempts { get { return GetSettings().MaxInvalidPasswordAttempts; } } - public override int PasswordAttemptWindow { get { return GetSettings().PasswordAttemptWindow; } } - public override bool RequiresUniqueEmail { get { return GetSettings().RequiresUniqueEmail; } } - public override MembershipPasswordFormat PasswordFormat { get { return GetSettings().PasswordFormat; } } - public override int MinRequiredPasswordLength { get { return GetSettings().MinRequiredPasswordLength; } } - public override int MinRequiredNonAlphanumericCharacters { get { return GetSettings().MinRequiredNonAlphanumericCharacters; } } - public override string PasswordStrengthRegularExpression { get { return GetSettings().PasswordStrengthRegularExpression; } } - } -} From 257cb4682ad48adc731cc1801c7772213f36340f Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 1 Sep 2010 18:18:53 -0700 Subject: [PATCH 07/13] Corrected resolution of username - Using IRepository was also looking at removed content items --HG-- branch : dev --- .../Controllers/AccountController.cs | 12 +++++++++-- .../Controllers/AdminController.cs | 16 +++++++++++++++ .../Services/MembershipService.cs | 20 ++++++++----------- .../Views/Account/ChallengeEmailFail.ascx | 2 +- .../Views/Account/ChallengeEmailSuccess.ascx | 2 +- .../Orchard.Users/Views/Admin/Index.aspx | 5 ++++- 6 files changed, 40 insertions(+), 17 deletions(-) diff --git a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs index 87a18ec00..920c15140 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs @@ -183,15 +183,23 @@ namespace Orchard.Users.Controllers { return View(new BaseViewModel()); } + public ActionResult ChallengeEmailSuccess() { + return View(new BaseViewModel()); + } + + public ActionResult ChallengeEmailFail() { + return View(new BaseViewModel()); + } + public ActionResult ChallengeEmail(string token) { var user = _membershipService.ValidateChallengeToken(token); if ( user != null ) { _authenticationService.SignIn(user, false /* createPersistentCookie */); - return View("ChallengeEmailSuccess"); + return RedirectToAction("ChallengeEmailSuccess"); } - return View("ChallengeEmailFail"); + return RedirectToAction("ChallengeEmailFail"); } protected override void OnActionExecuting(ActionExecutingContext filterContext) { diff --git a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs index 05119e766..dd1eda670 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs @@ -149,6 +149,22 @@ namespace Orchard.Users.Controllers { return RedirectToAction("Index"); } + public ActionResult SendChallengeEmail(int id) { + if ( !Services.Authorizer.Authorize(Permissions.ManageUsers, T("Not authorized to manage users")) ) + return new HttpUnauthorizedResult(); + + var user = Services.ContentManager.Get(id); + + if ( user != null ) { + string challengeToken = _membershipService.GetEncryptedChallengeToken(user.As()); + _membershipService.SendChallengeEmail(user.As(), Url.AbsoluteAction(() => Url.Action("ChallengeEmail", "Account", new {Area = "Orchard.Users", token = challengeToken}))); + } + + Services.Notifier.Information(T("Challenge email sent")); + + return RedirectToAction("Index"); + } + public ActionResult Approve(int id) { if ( !Services.Authorizer.Authorize(Permissions.ManageUsers, T("Not authorized to manage users")) ) return new HttpUnauthorizedResult(); diff --git a/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs b/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs index 046077d03..69149386d 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs @@ -134,31 +134,27 @@ namespace Orchard.Users.Services { public IUser GetUser(string username) { var lowerName = username == null ? "" : username.ToLower(); - var userRecord = _userRepository.Get(x => x.NormalizedUserName == lowerName); - if (userRecord == null) { - return null; - } - return _contentManager.Get(userRecord.Id); + return _contentManager.Query().Where(u => u.NormalizedUserName == lowerName).List().FirstOrDefault(); } public IUser ValidateUser(string userNameOrEmail, string password) { var lowerName = userNameOrEmail == null ? "" : userNameOrEmail.ToLower(); - var userRecord = _userRepository.Get(x => x.NormalizedUserName == lowerName); + var user = _contentManager.Query().Where(u => u.NormalizedUserName == lowerName).List().FirstOrDefault(); - if(userRecord == null) - userRecord = _userRepository.Get(x => x.Email == lowerName); + if(user == null) + user = _contentManager.Query().Where(u => u.Email == lowerName).List().FirstOrDefault(); - if (userRecord == null || ValidatePassword(userRecord, password) == false) + if ( user == null || ValidatePassword(user.As().Record, password) == false ) return null; - if ( userRecord.EmailStatus != UserStatus.Approved ) + if ( user.EmailStatus != UserStatus.Approved ) return null; - if ( userRecord.RegistrationStatus != UserStatus.Approved ) + if ( user.RegistrationStatus != UserStatus.Approved ) return null; - return _contentManager.Get(userRecord.Id); + return user; } public void SetPassword(IUser user, string password) { diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailFail.ascx b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailFail.ascx index dbbd4a223..647e83954 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailFail.ascx +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailFail.ascx @@ -1,3 +1,3 @@ -<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %>

<%: Html.TitleForPage(T("Challenge Email").ToString()) %>

<%: T("Your email address could not be validated.") %>

diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSuccess.ascx b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSuccess.ascx index c2f9d0734..d7b8a4d1a 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSuccess.ascx +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/Account/ChallengeEmailSuccess.ascx @@ -1,3 +1,3 @@ -<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %>

<%: Html.TitleForPage(T("Challenge Email").ToString()) %>

<%: T("Your email address has been validated.") %>

diff --git a/src/Orchard.Web/Modules/Orchard.Users/Views/Admin/Index.aspx b/src/Orchard.Web/Modules/Orchard.Users/Views/Admin/Index.aspx index 4bd5fad5b..1ae630b13 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Views/Admin/Index.aspx +++ b/src/Orchard.Web/Modules/Orchard.Users/Views/Admin/Index.aspx @@ -37,7 +37,10 @@ <%: Html.ActionLink(T("Edit").ToString(), "Edit", new { row.UserPart.Id })%> | <%: Html.ActionLink(T("Remove").ToString(), "Delete", new { row.UserPart.Id })%> | - <%: row.UserPart.RegistrationStatus == UserStatus.Pending ? Html.ActionLink(T("Approve").ToString(), "Approve", new { row.UserPart.Id }) : Html.ActionLink(T("Moderate").ToString(), "Moderate", new { row.UserPart.Id })%> + <%: row.UserPart.RegistrationStatus == UserStatus.Pending ? Html.ActionLink(T("Approve").ToString(), "Approve", new { row.UserPart.Id }) : Html.ActionLink(T("Disable").ToString(), "Moderate", new { row.UserPart.Id })%> + <% if ( row.UserPart.EmailStatus == UserStatus.Pending ) { %> | + <%: Html.ActionLink(T("Challenge Email").ToString(), "SendChallengeEmail", new { row.UserPart.Id })%> + <% } %> <%}%> From fcb27fdbaeed0eeeb0d0ee9080f5cb0acdb76b6f Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Thu, 2 Sep 2010 13:51:26 -0700 Subject: [PATCH 08/13] MachineKey validation during Setup, with automatic proposed values --HG-- branch : dev --- .../Orchard.Azure.Web/Web.Debug.config | 2 +- .../Orchard.Azure.Web/Web.Release.config | 2 +- .../Controllers/SetupController.cs | 33 +++++++++++++++++++ src/Orchard.Web/Web.config | 4 +-- 4 files changed, 37 insertions(+), 4 deletions(-) diff --git a/src/Orchard.Azure/Orchard.Azure.Web/Web.Debug.config b/src/Orchard.Azure/Orchard.Azure.Web/Web.Debug.config index 1ae4a73f1..83e2f3bb9 100644 --- a/src/Orchard.Azure/Orchard.Azure.Web/Web.Debug.config +++ b/src/Orchard.Azure/Orchard.Azure.Web/Web.Debug.config @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/src/Orchard.Azure/Orchard.Azure.Web/Web.Release.config b/src/Orchard.Azure/Orchard.Azure.Web/Web.Release.config index 1ae4a73f1..83e2f3bb9 100644 --- a/src/Orchard.Azure/Orchard.Azure.Web/Web.Release.config +++ b/src/Orchard.Azure/Orchard.Azure.Web/Web.Release.config @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Setup/Controllers/SetupController.cs b/src/Orchard.Web/Modules/Orchard.Setup/Controllers/SetupController.cs index e901b031a..5e37f7e4e 100644 --- a/src/Orchard.Web/Modules/Orchard.Setup/Controllers/SetupController.cs +++ b/src/Orchard.Web/Modules/Orchard.Setup/Controllers/SetupController.cs @@ -1,5 +1,9 @@ using System; +using System.Configuration; +using System.Security.Cryptography; +using System.Web.Configuration; using System.Web.Mvc; +using System.Linq; using Orchard.FileSystems.AppData; using Orchard.Setup.Services; using Orchard.Setup.ViewModels; @@ -34,7 +38,34 @@ namespace Orchard.Setup.Controllers { return View(model); } + private bool ValidateMachineKey() { + // Get the machineKey section. + var section = ConfigurationManager.GetSection("system.web/machineKey") as MachineKeySection; + + if (section == null + || section.DecryptionKey.Contains("AutoGenerate") + || section.ValidationKey.Contains("AutoGenerate")) { + + var rng = new RNGCryptoServiceProvider(); + var decryptionData = new byte[32]; + var validationData = new byte[64]; + + rng.GetBytes(decryptionData); + rng.GetBytes(validationData); + + string decryptionKey = BitConverter.ToString(decryptionData).Replace("-", ""); + string validationKey = BitConverter.ToString(validationData).Replace("-", ""); + + ModelState.AddModelError("MachineKey", T("You need to define a MachineKey value in your web.config file. Here is one for you:\n ", validationKey, decryptionKey).ToString()); + return false; + } + + return true; + } + public ActionResult Index() { + ValidateMachineKey(); + var initialSettings = _setupService.Prime(); return IndexViewResult(new SetupViewModel { AdminUsername = "admin", DatabaseIsPreconfigured = !string.IsNullOrEmpty(initialSettings.DataProvider)}); } @@ -49,6 +80,8 @@ namespace Orchard.Setup.Controllers { ModelState.AddModelError("ConfirmPassword", T("Password confirmation must match").ToString()); } + ValidateMachineKey(); + if (!ModelState.IsValid) { return IndexViewResult(model); } diff --git a/src/Orchard.Web/Web.config b/src/Orchard.Web/Web.config index a11a2a1d0..43e31b3d0 100644 --- a/src/Orchard.Web/Web.config +++ b/src/Orchard.Web/Web.config @@ -19,8 +19,8 @@ - From a253985a4083affbe167f2334f725ebed41a5c4c Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Thu, 2 Sep 2010 15:32:47 -0700 Subject: [PATCH 09/13] Specific Driver for SmtpSettingPart in order to encrypt/decrypt the password --HG-- branch : dev --- Orchard.proj | 8 +++++ .../Drivers/SmtpSettingsPartDriver.cs | 29 +++++++++++++++++++ .../Handlers/SmtpSettingsPartHandler.cs | 1 - .../Orchard.Email/Models/SmtpSettingsPart.cs | 8 +++-- .../Orchard.Email/Orchard.Email.csproj | 1 + .../Parts/Smtp.SiteSettings.ascx | 2 +- 6 files changed, 44 insertions(+), 5 deletions(-) create mode 100644 src/Orchard.Web/Modules/Orchard.Email/Drivers/SmtpSettingsPartDriver.cs diff --git a/Orchard.proj b/Orchard.proj index 00e76724f..84644851d 100644 --- a/Orchard.proj +++ b/Orchard.proj @@ -170,6 +170,14 @@ XPath="/configuration/system.web/compilation/@debug" Value="false" /> + + + + diff --git a/src/Orchard.Web/Modules/Orchard.Email/Drivers/SmtpSettingsPartDriver.cs b/src/Orchard.Web/Modules/Orchard.Email/Drivers/SmtpSettingsPartDriver.cs new file mode 100644 index 000000000..eb936c5ad --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Drivers/SmtpSettingsPartDriver.cs @@ -0,0 +1,29 @@ +using Orchard.ContentManagement; +using Orchard.ContentManagement.Drivers; +using Orchard.Email.Models; +using Orchard.Localization; + +namespace Orchard.Email.Drivers { + + // We define a specific driver instead of using a TemplateFilterForRecord, because we need the ;odel to be the part and not the record. + // Thus the encryption/decryption will be done when accessing the part's property + + public class SmtpSettingsPartDriver : ContentPartDriver { + public SmtpSettingsPartDriver() { + T = NullLocalizer.Instance; + } + + public Localizer T { get; set; } + + protected override string Prefix { get { return "SmtpSettings"; } } + + protected override DriverResult Editor(SmtpSettingsPart termPart) { + return ContentPartTemplate(termPart, "Parts/Smtp.SiteSettings"); + } + + protected override DriverResult Editor(SmtpSettingsPart termPart, IUpdateModel updater) { + updater.TryUpdateModel(termPart, Prefix, null, null); + return Editor(termPart); + } + } +} \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Handlers/SmtpSettingsPartHandler.cs b/src/Orchard.Web/Modules/Orchard.Email/Handlers/SmtpSettingsPartHandler.cs index b24131f06..dda95b909 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Handlers/SmtpSettingsPartHandler.cs +++ b/src/Orchard.Web/Modules/Orchard.Email/Handlers/SmtpSettingsPartHandler.cs @@ -9,7 +9,6 @@ namespace Orchard.Email.Handlers { public SmtpSettingsPartHandler(IRepository repository) { Filters.Add(new ActivatingFilter("Site")); Filters.Add(StorageFilter.For(repository)); - Filters.Add(new TemplateFilterForRecord("SmtpSettings", "Parts/Smtp.SiteSettings")); } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPart.cs b/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPart.cs index 057159041..964d022df 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPart.cs +++ b/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPart.cs @@ -1,4 +1,6 @@ -using Orchard.ContentManagement; +using System.Text; +using System.Web.Security; +using Orchard.ContentManagement; using System; namespace Orchard.Email.Models { @@ -40,8 +42,8 @@ namespace Orchard.Email.Models { } public string Password { - get { return Record.Password; } - set { Record.Password = value; } + get { return String.IsNullOrWhiteSpace(Record.Password) ? String.Empty : Encoding.UTF8.GetString(MachineKey.Decode(Record.Password, MachineKeyProtection.All)); ; } + set { Record.Password = String.IsNullOrWhiteSpace(value) ? String.Empty : MachineKey.Encode(Encoding.UTF8.GetBytes(value), MachineKeyProtection.All); } } } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj b/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj index 0c1ba7057..5848e615c 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj +++ b/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj @@ -66,6 +66,7 @@ + diff --git a/src/Orchard.Web/Modules/Orchard.Email/Views/EditorTemplates/Parts/Smtp.SiteSettings.ascx b/src/Orchard.Web/Modules/Orchard.Email/Views/EditorTemplates/Parts/Smtp.SiteSettings.ascx index d9cdefe8e..a6ac4e3f6 100644 --- a/src/Orchard.Web/Modules/Orchard.Email/Views/EditorTemplates/Parts/Smtp.SiteSettings.ascx +++ b/src/Orchard.Web/Modules/Orchard.Email/Views/EditorTemplates/Parts/Smtp.SiteSettings.ascx @@ -1,4 +1,4 @@ -<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> +<%@ Control Language="C#" Inherits="Orchard.Mvc.ViewUserControl" %> <%@ Import Namespace="Orchard.Email.Models"%> <%@ Import Namespace="System.Net.Mail" %>
From 1154e0cf88465bfeab5eff7102fc8c261933cefa Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Thu, 2 Sep 2010 15:33:18 -0700 Subject: [PATCH 10/13] Updated Web.Config and build scripts to have a default machine key for development, and removing it for builds --HG-- branch : dev --- AzurePackage.proj | 8 ++++++++ src/Orchard.Azure/Orchard.Azure.Web/Web.Debug.config | 1 + src/Orchard.Azure/Orchard.Azure.Web/Web.Release.config | 1 + src/Orchard.Azure/Orchard.Azure.Web/Web.config | 1 + src/Orchard.Web/Web.config | 6 ++---- 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/AzurePackage.proj b/AzurePackage.proj index 1155e34c4..0a876e275 100644 --- a/AzurePackage.proj +++ b/AzurePackage.proj @@ -99,6 +99,14 @@ XPath="/configuration/system.web/compilation/@debug" Value="false" /> + + + + diff --git a/src/Orchard.Azure/Orchard.Azure.Web/Web.Debug.config b/src/Orchard.Azure/Orchard.Azure.Web/Web.Debug.config index 83e2f3bb9..5b18590d2 100644 --- a/src/Orchard.Azure/Orchard.Azure.Web/Web.Debug.config +++ b/src/Orchard.Azure/Orchard.Azure.Web/Web.Debug.config @@ -2,6 +2,7 @@ + diff --git a/src/Orchard.Azure/Orchard.Azure.Web/Web.Release.config b/src/Orchard.Azure/Orchard.Azure.Web/Web.Release.config index 83e2f3bb9..5b18590d2 100644 --- a/src/Orchard.Azure/Orchard.Azure.Web/Web.Release.config +++ b/src/Orchard.Azure/Orchard.Azure.Web/Web.Release.config @@ -2,6 +2,7 @@ + diff --git a/src/Orchard.Azure/Orchard.Azure.Web/Web.config b/src/Orchard.Azure/Orchard.Azure.Web/Web.config index 20f62d942..6832e89bb 100644 --- a/src/Orchard.Azure/Orchard.Azure.Web/Web.config +++ b/src/Orchard.Azure/Orchard.Azure.Web/Web.config @@ -19,6 +19,7 @@ +