From d945a8eaf1fe4d851fd72a1a0373efe31aed8c0a Mon Sep 17 00:00:00 2001 From: Sebastien Ros Date: Wed, 6 Mar 2013 14:34:07 -0800 Subject: [PATCH] Adding Approve, Delete and Moderate GET actions driven by tokens Allow comments tokens to be chained with Text ones (HtmlEncode, ...) --HG-- branch : 1.x --- .../Controllers/CommentController.cs | 30 +++++++++++ .../Modules/Orchard.Comments/Module.txt | 2 +- .../Orchard.Comments/Orchard.Comments.csproj | 4 ++ .../Services/CommentService.cs | 41 ++++++++++++-- .../Services/ICommentService.cs | 3 ++ .../Orchard.Comments/Tokens/CommentTokens.cs | 54 ++++++++++++++----- 6 files changed, 116 insertions(+), 18 deletions(-) diff --git a/src/Orchard.Web/Modules/Orchard.Comments/Controllers/CommentController.cs b/src/Orchard.Web/Modules/Orchard.Comments/Controllers/CommentController.cs index 96f6f095d..8714dc228 100644 --- a/src/Orchard.Web/Modules/Orchard.Comments/Controllers/CommentController.cs +++ b/src/Orchard.Web/Modules/Orchard.Comments/Controllers/CommentController.cs @@ -123,6 +123,36 @@ namespace Orchard.Comments.Controllers { return this.RedirectLocal(returnUrl, "~/"); } + public ActionResult Approve(string nonce) { + int id; + if (_commentService.DecryptNonce(nonce, out id)) { + _commentService.ApproveComment(id); + } + + Services.Notifier.Information(T("Comment approved successfully")); + return Redirect("~/"); + } + + public ActionResult Delete(string nonce) { + int id; + if (_commentService.DecryptNonce(nonce, out id)) { + _commentService.DeleteComment(id); + } + + Services.Notifier.Information(T("Comment deleted successfully")); + return Redirect("~/"); + } + + public ActionResult Moderate(string nonce) { + int id; + if (_commentService.DecryptNonce(nonce, out id)) { + _commentService.UnapproveComment(id); + } + + Services.Notifier.Information(T("Comment moderated successfully")); + return Redirect("~/"); + } + 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.Comments/Module.txt b/src/Orchard.Web/Modules/Orchard.Comments/Module.txt index d0b452cd3..75a234529 100644 --- a/src/Orchard.Web/Modules/Orchard.Comments/Module.txt +++ b/src/Orchard.Web/Modules/Orchard.Comments/Module.txt @@ -9,5 +9,5 @@ Features: Orchard.Comments: Name: Comments Description: Standard content item comments. - Dependencies: Settings + Dependencies: Settings, Orchard.Tokens Category: Social diff --git a/src/Orchard.Web/Modules/Orchard.Comments/Orchard.Comments.csproj b/src/Orchard.Web/Modules/Orchard.Comments/Orchard.Comments.csproj index 91eb263fe..564599631 100644 --- a/src/Orchard.Web/Modules/Orchard.Comments/Orchard.Comments.csproj +++ b/src/Orchard.Web/Modules/Orchard.Comments/Orchard.Comments.csproj @@ -119,6 +119,10 @@ {9916839C-39FC-4CEB-A5AF-89CA7E87119F} Orchard.Core + + {6f759635-13d7-4e94-bcc9-80445d63f117} + Orchard.Tokens + diff --git a/src/Orchard.Web/Modules/Orchard.Comments/Services/CommentService.cs b/src/Orchard.Web/Modules/Orchard.Comments/Services/CommentService.cs index 85407974e..c47603852 100644 --- a/src/Orchard.Web/Modules/Orchard.Comments/Services/CommentService.cs +++ b/src/Orchard.Web/Modules/Orchard.Comments/Services/CommentService.cs @@ -1,15 +1,28 @@ -using JetBrains.Annotations; +using System; +using System.Globalization; +using System.Text; +using System.Xml.Linq; +using JetBrains.Annotations; using Orchard.Comments.Models; using Orchard.Logging; using Orchard.ContentManagement; +using Orchard.Security; +using Orchard.Services; namespace Orchard.Comments.Services { [UsedImplicitly] public class CommentService : ICommentService { private readonly IOrchardServices _orchardServices; + private readonly IClock _clock; + private readonly IEncryptionService _encryptionService; - public CommentService(IOrchardServices orchardServices) { + public CommentService( + IOrchardServices orchardServices, + IClock clock, + IEncryptionService encryptionService) { _orchardServices = orchardServices; + _clock = clock; + _encryptionService = encryptionService; Logger = NullLogger.Instance; } @@ -64,7 +77,7 @@ namespace Orchard.Comments.Services { } public void DeleteComment(int commentId) { - _orchardServices.ContentManager.Remove(_orchardServices.ContentManager.Get(commentId)); + _orchardServices.ContentManager.Remove(_orchardServices.ContentManager.Get(commentId).ContentItem); } public bool CommentsDisabledForCommentedContent(int id) { @@ -79,6 +92,28 @@ namespace Orchard.Comments.Services { _orchardServices.ContentManager.Get(id, VersionOptions.Latest).CommentsActive = true; } + public string CreateNonce(CommentPart comment, TimeSpan delay) { + var challengeToken = new XElement("n", new XAttribute("c", comment.Id), new XAttribute("v", _clock.UtcNow.ToUniversalTime().Add(delay).ToString(CultureInfo.InvariantCulture))).ToString(); + var data = Encoding.UTF8.GetBytes(challengeToken); + return Convert.ToBase64String(_encryptionService.Encode(data)); + } + + public bool DecryptNonce(string nonce, out int id) { + id = 0; + + try { + var data = _encryptionService.Decode(Convert.FromBase64String(nonce)); + var xml = Encoding.UTF8.GetString(data); + var element = XElement.Parse(xml); + id = Convert.ToInt32(element.Attribute("c").Value); + var validateByUtc = DateTime.Parse(element.Attribute("v").Value, CultureInfo.InvariantCulture); + return _clock.UtcNow <= validateByUtc; + } + catch { + return false; + } + + } private CommentPart GetCommentWithQueryHints(int id) { return _orchardServices.ContentManager.Get(id, VersionOptions.Latest, new QueryHints().ExpandParts()); } diff --git a/src/Orchard.Web/Modules/Orchard.Comments/Services/ICommentService.cs b/src/Orchard.Web/Modules/Orchard.Comments/Services/ICommentService.cs index 556dd9762..f42d87bc1 100644 --- a/src/Orchard.Web/Modules/Orchard.Comments/Services/ICommentService.cs +++ b/src/Orchard.Web/Modules/Orchard.Comments/Services/ICommentService.cs @@ -1,3 +1,4 @@ +using System; using Orchard.Comments.Models; using Orchard.ContentManagement; @@ -16,5 +17,7 @@ namespace Orchard.Comments.Services { bool CommentsDisabledForCommentedContent(int id); void DisableCommentsForCommentedContent(int id); void EnableCommentsForCommentedContent(int id); + bool DecryptNonce(string nonce, out int id); + string CreateNonce(CommentPart comment, TimeSpan delay); } } \ No newline at end of file diff --git a/src/Orchard.Web/Modules/Orchard.Comments/Tokens/CommentTokens.cs b/src/Orchard.Web/Modules/Orchard.Comments/Tokens/CommentTokens.cs index c485e2887..67ba1cbb0 100644 --- a/src/Orchard.Web/Modules/Orchard.Comments/Tokens/CommentTokens.cs +++ b/src/Orchard.Web/Modules/Orchard.Comments/Tokens/CommentTokens.cs @@ -1,46 +1,72 @@ using System; +using System.Web.Mvc; using Orchard.Comments.Models; +using Orchard.Comments.Services; using Orchard.ContentManagement; -using Orchard.Events; using Orchard.Localization; +using Orchard.Mvc.Extensions; +using Orchard.Tokens; namespace Orchard.Comments.Tokens { - public interface ITokenProvider : IEventHandler { - void Describe(dynamic context); - void Evaluate(dynamic context); - } public class CommentTokens : ITokenProvider { private readonly IContentManager _contentManager; + private readonly IWorkContextAccessor _workContextAccessor; + private readonly ICommentService _commentService; - public CommentTokens(IContentManager contentManager) { + public CommentTokens( + IContentManager contentManager, + IWorkContextAccessor workContextAccessor, + ICommentService commentService) { _contentManager = contentManager; + _workContextAccessor = workContextAccessor; + _commentService = commentService; T = NullLocalizer.Instance; } public Localizer T { get; set; } - public void Describe(dynamic context) { + public void Describe(DescribeContext context) { context.For("Content", T("Comments"), T("Comments")) .Token("CommentedOn", T("Commented On"), T("The content item this comment was created on.")) .Token("CommentMessage", T("Comment Message"), T("The text of the comment itself")) .Token("CommentAuthor", T("Comment Author"), T("The author of the comment.")) .Token("CommentAuthorUrl", T("Comment Author Url"), T("The url provided by the author of the comment.")) .Token("CommentAuthorEmail", T("Comment Author Email"), T("The email provided by the author of the comment.")) + .Token("CommentApproveUrl", T("Comment approval Url"), T("The absolute url to follow in order to approve this comment.")) + .Token("CommentModerateUrl", T("Comment moderation Url"), T("The absolute url to follow in order to moderate this comment.")) + .Token("CommentDeleteUrl", T("Comment deletion Url"), T("The absolute url to follow in order to delete this comment.")) ; } - public void Evaluate(dynamic context) { + public void Evaluate(EvaluateContext context) { context.For("Content") - .Token("CommentedOn", (Func)(content => content.As().CommentedOn)) - .Chain("CommentedOn", "Content", (Func)(content => _contentManager.Get(content.As().CommentedOn))) - .Token("CommentMessage", (Func)(content => content.As().CommentText)) - .Token("CommentAuthor", (Func)CommentAuthor) - .Token("CommentAuthorUrl", (Func)(content => content.As().SiteName)) - .Token("CommentAuthorEmail", (Func)(content => content.As().Email)) + .Token("CommentedOn", content => content.As().CommentedOn) + .Chain("CommentedOn", "Content", content => _contentManager.Get(content.As().CommentedOn)) + .Token("CommentMessage", content => content.As().CommentText) + .Chain("CommentMessage", "Text", content => content.As().CommentText) + .Token("CommentAuthor", CommentAuthor) + .Chain("CommentAuthor", "Text", CommentAuthor) + .Token("CommentAuthorUrl", content => content.As().SiteName) + .Chain("CommentAuthorUrl", "Text", content => content.As().SiteName) + .Token("CommentAuthorEmail", content => content.As().Email) + .Chain("CommentAuthorEmail", "Text", content => content.As().Email) + .Token("CommentApproveUrl", content => CreateProtectedUrl("Approve", content.As())) + .Token("CommentModerateUrl", content => CreateProtectedUrl("Moderate", content.As())) + .Token("CommentDeleteUrl", content => CreateProtectedUrl("Delete", content.As())) ; } + private string CreateProtectedUrl(string action, CommentPart part) { + var workContext = _workContextAccessor.GetContext(); + if (workContext.HttpContext != null) { + var url = new UrlHelper(workContext.HttpContext.Request.RequestContext); + return url.AbsoluteAction(action, "Comment", new {area = "Orchard.Comments", nonce = _commentService.CreateNonce(part, TimeSpan.FromDays(7))}); + } + + return null; + } + private static string CommentAuthor(IContent comment) { var commentPart = comment.As(); return String.IsNullOrWhiteSpace(commentPart.UserName) ? commentPart.Author : commentPart.UserName;