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/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.Azure/Orchard.Azure.Web/Web.Debug.config b/src/Orchard.Azure/Orchard.Azure.Web/Web.Debug.config index 1ae4a73f1..5b18590d2 100644 --- a/src/Orchard.Azure/Orchard.Azure.Web/Web.Debug.config +++ b/src/Orchard.Azure/Orchard.Azure.Web/Web.Debug.config @@ -2,7 +2,8 @@ - + + \ 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..5b18590d2 100644 --- a/src/Orchard.Azure/Orchard.Azure.Web/Web.Release.config +++ b/src/Orchard.Azure/Orchard.Azure.Web/Web.Release.config @@ -2,7 +2,8 @@ - + + \ No newline at end of file diff --git a/src/Orchard.Azure/Orchard.Azure.Web/Web.config b/src/Orchard.Azure/Orchard.Azure.Web/Web.config index 46814700e..d2cee7743 100644 --- a/src/Orchard.Azure/Orchard.Azure.Web/Web.config +++ b/src/Orchard.Azure/Orchard.Azure.Web/Web.config @@ -19,6 +19,7 @@ + + + + + + + + + + + + + + + + diff --git a/src/Orchard.Web/Core/Orchard.Core.csproj b/src/Orchard.Web/Core/Orchard.Core.csproj index d7933f400..dab4ff1ae 100644 --- a/src/Orchard.Web/Core/Orchard.Core.csproj +++ b/src/Orchard.Web/Core/Orchard.Core.csproj @@ -90,6 +90,13 @@ + + + + + + + @@ -246,6 +253,8 @@ + + @@ -364,6 +373,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/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 new file mode 100644 index 000000000..dda95b909 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Handlers/SmtpSettingsPartHandler.cs @@ -0,0 +1,14 @@ +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)); + } + } +} \ 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..964d022df --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Models/SmtpSettingsPart.cs @@ -0,0 +1,49 @@ +using System.Text; +using System.Web.Security; +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 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/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..5848e615c --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Orchard.Email.csproj @@ -0,0 +1,142 @@ + + + + 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..e67ace915 --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessageEventHandler.cs @@ -0,0 +1,29 @@ +using Orchard.Messaging.Events; +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.Recipient.Id); + if ( contentItem == null ) + return; + + var recipient = contentItem.As(); + if ( recipient == null ) + return; + + 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 new file mode 100644 index 000000000..ddff31e2a --- /dev/null +++ b/src/Orchard.Web/Modules/Orchard.Email/Services/EmailMessagingChannel.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Mail; +using JetBrains.Annotations; +using Orchard.ContentManagement; +using Orchard.Localization; +using Orchard.Logging; +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 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.Service.Equals(EmailService, StringComparison.InvariantCultureIgnoreCase) ) + 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); + } + + if(context.MailMessage.To.Count == 0) { + 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; + + 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(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}", context.MailMessage.To[0].Address, context.Type); + } + } + + 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..a6ac4e3f6 --- /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/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/Modules/Orchard.Setup/Services/SetupService.cs b/src/Orchard.Web/Modules/Orchard.Setup/Services/SetupService.cs index dbd37e585..182e2d1e2 100644 --- a/src/Orchard.Web/Modules/Orchard.Setup/Services/SetupService.cs +++ b/src/Orchard.Web/Modules/Orchard.Setup/Services/SetupService.cs @@ -77,6 +77,7 @@ namespace Orchard.Setup.Services { "Routable", "Settings", //"XmlRpc", + "Messaging", "Orchard.Users", "Orchard.Roles", //"TinyMce", 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 000000000..42c8bde22 Binary files /dev/null and b/src/Orchard.Web/Modules/Orchard.Users/Content/Admin/images/offline.gif differ diff --git a/src/Orchard.Web/Modules/Orchard.Users/Content/Admin/images/online.gif b/src/Orchard.Web/Modules/Orchard.Users/Content/Admin/images/online.gif new file mode 100644 index 000000000..f55c73a2f Binary files /dev/null and b/src/Orchard.Web/Modules/Orchard.Users/Content/Admin/images/online.gif differ diff --git a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs index 6432ce1c0..a6057bd1f 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AccountController.cs @@ -9,6 +9,11 @@ using Orchard.Mvc.Extensions; 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] @@ -30,6 +35,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"]; @@ -86,6 +92,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(); @@ -93,14 +105,26 @@ 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)) { // 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("~/"); } @@ -154,6 +178,29 @@ namespace Orchard.Users.Controllers { return View(); } + public ActionResult ChallengeEmailSent() { + return View(); + } + + public ActionResult ChallengeEmailSuccess() { + return View(); + } + + public ActionResult ChallengeEmailFail() { + return View(); + } + + public ActionResult ChallengeEmail(string token) { + var user = _membershipService.ValidateChallengeToken(token); + + if ( user != null ) { + _authenticationService.SignIn(user, false /* createPersistentCookie */); + return RedirectToAction("ChallengeEmailSuccess"); + } + + return RedirectToAction("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 73b183f85..ae8ddd258 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Controllers/AdminController.cs @@ -1,12 +1,15 @@ 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.Models; using Orchard.Users.Services; using Orchard.Users.ViewModels; +using Orchard.Mvc.Extensions; namespace Orchard.Users.Controllers { [ValidateInput(false)] @@ -26,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"))) @@ -144,6 +148,55 @@ 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(); + + 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/DataMigrations/UsersDataMigration.cs b/src/Orchard.Web/Modules/Orchard.Users/DataMigrations/UsersDataMigration.cs index f3fdeeed5..95b725ffb 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/DataMigrations/UsersDataMigration.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/DataMigrations/UsersDataMigration.cs @@ -18,5 +18,25 @@ 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'"))) + .AlterTable("UserPartRecord", table => table.AddColumn("EmailChallengeToken")); + + // 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/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/Handlers/ModerationMessageAlteration.cs b/src/Orchard.Web/Modules/Orchard.Users/Handlers/ModerationMessageAlteration.cs new file mode 100644 index 000000000..ac2b0d46a --- /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 here to validate you email address.", recipient.UserName, context.Properties["ChallengeUrl"]); + } + + } + + 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..c6c8c2cff 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Models/UserPartRecord.cs @@ -11,5 +11,9 @@ 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; } + public virtual string EmailChallengeToken { 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 93302d22b..e5aab7b6d 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 + @@ -68,9 +69,17 @@ + + + + + + + + @@ -83,10 +92,15 @@ + + + + + @@ -94,6 +108,7 @@ + diff --git a/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs b/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs index 4e3f4d2aa..5d65b1212 100644 --- a/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs +++ b/src/Orchard.Web/Modules/Orchard.Users/Services/MembershipService.cs @@ -1,28 +1,37 @@ 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; using Orchard.ContentManagement; using Orchard.Security; +using Orchard.Users.Events; using Orchard.Users.Models; +using Orchard.Settings; +using Orchard.Messaging.Services; +using System.Collections.Generic; 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 IRepository _userRepository; + private readonly IMessageManager _messageManager; + private readonly IEnumerable _userEventHandlers; - public MembershipService(IContentManager contentManager, IRepository userRepository) { + public MembershipService(IContentManager contentManager, IMessageManager messageManager, IEnumerable userEventHandlers) { _contentManager = contentManager; - _userRepository = userRepository; + _messageManager = messageManager; + _userEventHandlers = userEventHandlers; Logger = NullLogger.Instance; } public ILogger Logger { get; set; } + protected virtual ISite CurrentSite { get; [UsedImplicitly] private set; } public MembershipSettings GetSettings() { var settings = new MembershipSettings(); @@ -33,40 +42,124 @@ namespace Orchard.Users.Services { public IUser CreateUser(CreateUserParams createUserParams) { Logger.Information("CreateUser {0} {1}", createUserParams.Username, createUserParams.Email); - return _contentManager.Create("User", 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); - }); + var registrationSettings = CurrentSite.As(); + + var user = _contentManager.New("User"); + + 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); + + if ( registrationSettings != null ) { + user.Record.RegistrationStatus = registrationSettings.UsersAreModerated ? UserStatus.Pending : UserStatus.Approved; + user.Record.EmailStatus = registrationSettings.UsersMustValidateEmail ? UserStatus.Pending : UserStatus.Approved; + } + + if(createUserParams.IsApproved) { + user.Record.RegistrationStatus = UserStatus.Approved; + user.Record.EmailStatus = 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 != null && registrationSettings.UsersAreModerated && registrationSettings.NotifyModeration && !createUserParams.IsApproved ) { + var superUser = GetUser(CurrentSite.SuperUser); + if(superUser != null) + _messageManager.Send(superUser.ContentItem.Record, MessageTypes.Moderation); + } + + return user; + } + + public void SendChallengeEmail(IUser 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(IUser 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) { 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); - if(userRecord == null) - userRecord = _userRepository.Get(x => x.Email == lowerName); - if (userRecord == null || ValidatePassword(userRecord, password) == false) + var user = _contentManager.Query().Where(u => u.NormalizedUserName == lowerName).List().FirstOrDefault(); + + if(user == null) + user = _contentManager.Query().Where(u => u.Email == lowerName).List().FirstOrDefault(); + + if ( user == null || ValidatePassword(user.As().Record, password) == false ) return null; - return _contentManager.Get(userRecord.Id); + if ( user.EmailStatus != UserStatus.Approved ) + return null; + + if ( user.RegistrationStatus != UserStatus.Approved ) + return null; + + return user; } - - public void SetPassword(IUser user, string password) { if (!user.Is()) throw new InvalidCastException(); @@ -92,7 +185,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 @@ -146,7 +239,7 @@ namespace Orchard.Users.Services { var hashAlgorithm = HashAlgorithm.Create(partRecord.HashAlgorithm); var hashBytes = hashAlgorithm.ComputeHash(combinedBytes); - + return partRecord.Password == Convert.ToBase64String(hashBytes); } @@ -157,5 +250,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..647e83954 --- /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..d7b8a4d1a --- /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/Modules/Orchard.Users/Views/Admin/Index.aspx b/src/Orchard.Web/Modules/Orchard.Users/Views/Admin/Index.aspx index 41cd5b2ed..1ae630b13 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,11 @@ <%: 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("Disable").ToString(), "Moderate", new { row.UserPart.Id })%> + <% if ( row.UserPart.EmailStatus == UserStatus.Pending ) { %> | + <%: Html.ActionLink(T("Challenge Email").ToString(), "SendChallengeEmail", new { row.UserPart.Id })%> + <% } %> <%}%> 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.Web/Web.config b/src/Orchard.Web/Web.config index 56ab3731f..0d8683f45 100644 --- a/src/Orchard.Web/Web.config +++ b/src/Orchard.Web/Web.config @@ -42,6 +42,8 @@ + + - - - - - - - - - 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/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/MessageContext.cs b/src/Orchard/Messaging/Models/MessageContext.cs new file mode 100644 index 000000000..63885700f --- /dev/null +++ b/src/Orchard/Messaging/Models/MessageContext.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Net.Mail; +using Orchard.ContentManagement.Records; + +namespace Orchard.Messaging.Models { + public class MessageContext { + 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() { + Properties = new Dictionary(); + MailMessage = new MailMessage(); + } + } +} diff --git a/src/Orchard/Messaging/Services/IMessageManager.cs b/src/Orchard/Messaging/Services/IMessageManager.cs new file mode 100644 index 000000000..efcbad622 --- /dev/null +++ b/src/Orchard/Messaging/Services/IMessageManager.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using Orchard.ContentManagement.Records; + +namespace Orchard.Messaging.Services { + public interface IMessageManager : IDependency { + /// + /// Sends a message to a channel + /// + void Send(ContentItemRecord recipient, string type, string service = null, Dictionary properties = null); + + /// + /// 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..e5d33ff46 --- /dev/null +++ b/src/Orchard/Messaging/Services/IMessagingChannel.cs @@ -0,0 +1,17 @@ +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); + + /// + /// 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 82ee1a764..3ab49104c 100644 --- a/src/Orchard/Orchard.Framework.csproj +++ b/src/Orchard/Orchard.Framework.csproj @@ -397,6 +397,10 @@ + + + + @@ -773,7 +777,6 @@ - diff --git a/src/Orchard/Security/IMembershipService.cs b/src/Orchard/Security/IMembershipService.cs index 0821fcdb1..5e36c7e8d 100644 --- a/src/Orchard/Security/IMembershipService.cs +++ b/src/Orchard/Security/IMembershipService.cs @@ -7,5 +7,9 @@ IUser ValidateUser(string userNameOrEmail, string password); void SetPassword(IUser user, string password); + + IUser ValidateChallengeToken(string challengeToken); + void SendChallengeEmail(IUser user, string url); + string GetEncryptedChallengeToken(IUser 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; } } - } -}