Refactoring spam filter customization

--HG--
branch : 1.x
This commit is contained in:
Sebastien Ros
2012-11-07 18:15:32 -08:00
parent 19a22c6473
commit 0527cb43bb
12 changed files with 193 additions and 109 deletions

View File

@@ -1,28 +0,0 @@
using System.Linq;
using Orchard.AntiSpam.Models;
using Orchard.AntiSpam.Services;
using Orchard.AntiSpam.Settings;
namespace Orchard.AntiSpam.EventHandlers {
public class DefaultCheckSpamEventHandler : ICheckSpamEventHandler {
private readonly ISpamService _spamService;
public DefaultCheckSpamEventHandler(ISpamService spamService) {
_spamService = spamService;
}
public void CheckSpam(dynamic context) {
if(!_spamService.GetSpamFilters().Any()) {
return;
}
context.Checked = true;
if(string.IsNullOrWhiteSpace(context.Text)) {
return;
}
context.IsSpam = _spamService.CheckForSpam(context.Text, SpamFilterAction.One, context.Content) == SpamStatus.Spam;
}
}
}

View File

@@ -1,14 +0,0 @@
using Orchard.Events;
namespace Orchard.AntiSpam.EventHandlers {
public interface ICheckSpamEventHandler : IEventHandler {
/// <param name="context">
/// Dynamic object representing the parameters for the call
/// - Content (in IContent): the IContent that should trigger events when checked
/// - Text (in string): the text which is submitted for spam analysis
/// - Checked (out bool): will be assigned to true if the spam could be checked
/// - IsPam (out bool): True if the text has been reported as spam
/// </param>
void CheckSpam(dynamic context);
}
}

View File

@@ -0,0 +1,56 @@
namespace Orchard.AntiSpam.Models {
public class CommentCheckContext {
/// <summary>
/// The front page or home URL of the instance making the request. For a blog
/// or wiki this would be the front page. Note: Must be a full URI, including http://.
/// </summary>
public string Url { get; set; }
/// <summary>
/// IP address of the comment submitter.
/// </summary>
public string UserIp { get; set; }
/// <summary>
/// User agent string of the web browser submitting the comment - typically
/// the HTTP_USER_AGENT cgi variable. Not to be confused with the user agent
/// of your Akismet library.
/// </summary>
public string UserAgent { get; set; }
/// <summary>
/// The content of the HTTP_REFERER header should be sent here.
/// </summary>
public string Referrer { get; set; }
/// <summary>
/// The permanent location of the entry the comment was submitted to.
/// </summary>
public string Permalink { get; set; }
/// <summary>
/// May be blank, comment, trackback, pingback, or a made up value like "registration".
/// </summary>
public string CommentType { get; set; }
/// <summary>
/// Name submitted with the comment
/// </summary>
public string CommentAuthor { get; set; }
/// <summary>
/// Email address submitted with the comment
/// </summary>
public string CommentAuthorEmail { get; set; }
/// <summary>
/// URL submitted with comment
/// </summary>
public string CommentAuthorUrl { get; set; }
/// <summary>
/// The content that was submitted.
/// </summary>
public string CommentContent { get; set; }
}
}

View File

@@ -93,8 +93,6 @@
<Compile Include="Drivers\SpamFilterPartDriver.cs" /> <Compile Include="Drivers\SpamFilterPartDriver.cs" />
<Compile Include="Drivers\ReCaptchaPartDriver.cs" /> <Compile Include="Drivers\ReCaptchaPartDriver.cs" />
<Compile Include="Drivers\SubmissionLimitPartDriver.cs" /> <Compile Include="Drivers\SubmissionLimitPartDriver.cs" />
<Compile Include="EventHandlers\DefaultCheckSpamEventHandler.cs" />
<Compile Include="EventHandlers\ICheckSpamEventHandler.cs" />
<Compile Include="Handlers\AkismetSettingsPartHandler.cs" /> <Compile Include="Handlers\AkismetSettingsPartHandler.cs" />
<Compile Include="Handlers\ReCaptchaSettingsPartHandler.cs" /> <Compile Include="Handlers\ReCaptchaSettingsPartHandler.cs" />
<Compile Include="Handlers\TypePadSettingsPartHandler.cs" /> <Compile Include="Handlers\TypePadSettingsPartHandler.cs" />
@@ -102,6 +100,7 @@
<Compile Include="Migrations.cs" /> <Compile Include="Migrations.cs" />
<Compile Include="Models\AkismetSettingsPartRecord.cs" /> <Compile Include="Models\AkismetSettingsPartRecord.cs" />
<Compile Include="Models\AkismetSettingsPart.cs" /> <Compile Include="Models\AkismetSettingsPart.cs" />
<Compile Include="Models\CommentCheckContext.cs" />
<Compile Include="Models\ReCaptchaSettingsPart.cs" /> <Compile Include="Models\ReCaptchaSettingsPart.cs" />
<Compile Include="Models\ReCaptchaSettingsPartRecord.cs" /> <Compile Include="Models\ReCaptchaSettingsPartRecord.cs" />
<Compile Include="Models\TypePadSettingsPart.cs" /> <Compile Include="Models\TypePadSettingsPart.cs" />

View File

@@ -6,7 +6,6 @@ using System.Text;
using System.Web; using System.Web;
using Orchard.AntiSpam.Models; using Orchard.AntiSpam.Models;
using Orchard.Logging; using Orchard.Logging;
using Orchard.Utility.Extensions;
namespace Orchard.AntiSpam.Services { namespace Orchard.AntiSpam.Services {
public class AkismetApiSpamFilter : ISpamFilter { public class AkismetApiSpamFilter : ISpamFilter {
@@ -26,9 +25,9 @@ namespace Orchard.AntiSpam.Services {
public ILogger Logger { get; set; } public ILogger Logger { get; set; }
public SpamStatus CheckForSpam(string text) { public SpamStatus CheckForSpam(CommentCheckContext context) {
try { try {
var result = ExecuteValidateRequest(text, "comment-check"); var result = ExecuteValidateRequest(context, "comment-check");
if (HandleValidateResponse(_context, result)) { if (HandleValidateResponse(_context, result)) {
return SpamStatus.Spam; return SpamStatus.Spam;
@@ -42,25 +41,25 @@ namespace Orchard.AntiSpam.Services {
} }
} }
public void ReportSpam(string text) { public void ReportSpam(CommentCheckContext context) {
try { try {
var result = ExecuteValidateRequest(text, "submit-spam"); var result = ExecuteValidateRequest(context, "submit-spam");
} }
catch (Exception e) { catch (Exception e) {
Logger.Error(e, "An error occured while reporting spam"); Logger.Error(e, "An error occured while reporting spam");
} }
} }
public void ReportHam(string text) { public void ReportHam(CommentCheckContext context) {
try { try {
var result = ExecuteValidateRequest(text, "submit-ham"); var result = ExecuteValidateRequest(context, "submit-ham");
} }
catch (Exception e) { catch (Exception e) {
Logger.Error(e, "An error occured while reporting ham"); Logger.Error(e, "An error occured while reporting ham");
} }
} }
private string ExecuteValidateRequest(string text, string action) { private string ExecuteValidateRequest(CommentCheckContext context, string action) {
var uri = String.Format(AkismetApiPattern, _apiKey, _endpoint, action); var uri = String.Format(AkismetApiPattern, _apiKey, _endpoint, action);
WebRequest request = WebRequest.Create(uri); WebRequest request = WebRequest.Create(uri);
@@ -68,13 +67,16 @@ namespace Orchard.AntiSpam.Services {
request.Timeout = 5000; //milliseconds request.Timeout = 5000; //milliseconds
request.ContentType = "application/x-www-form-urlencoded"; request.ContentType = "application/x-www-form-urlencoded";
var postData = String.Format(CultureInfo.InvariantCulture, "blog={0}&user_ip={1}&user_agent={2}&referrer={3}&comment_content={4}", var postData = "blog=" + HttpUtility.UrlEncode(context.Url)
HttpUtility.UrlEncode(_context.Request.ToApplicationRootUrlString()), + "&user_ip=" + HttpUtility.UrlEncode(context.UserIp)
HttpUtility.UrlEncode(_context.Request.ServerVariables["REMOTE_ADDR"]), + "&user_agent=" + HttpUtility.UrlEncode(context.UserAgent)
HttpUtility.UrlEncode(_context.Request.UserAgent), + "&referrer=" + HttpUtility.UrlEncode(context.Referrer)
HttpUtility.UrlEncode(Convert.ToString(_context.Request.UrlReferrer)), + "&permalink=" + HttpUtility.UrlEncode(context.Permalink)
HttpUtility.UrlEncode(text) + "&comment_type=" + HttpUtility.UrlEncode(context.CommentType)
); + "&comment_author=" + HttpUtility.UrlEncode(context.CommentAuthor)
+ "&comment_author_email=" + HttpUtility.UrlEncode(context.CommentAuthorEmail)
+ "&comment_author_url=" + HttpUtility.UrlEncode(context.CommentAuthorUrl)
+ "&comment_content=" + HttpUtility.UrlEncode(context.CommentContent);
byte[] content = Encoding.UTF8.GetBytes(postData); byte[] content = Encoding.UTF8.GetBytes(postData);
using (Stream stream = request.GetRequestStream()) { using (Stream stream = request.GetRequestStream()) {

View File

@@ -13,22 +13,25 @@ namespace Orchard.AntiSpam.Services {
private readonly IEnumerable<ISpamFilterProvider> _providers; private readonly IEnumerable<ISpamFilterProvider> _providers;
private readonly ISpamEventHandler _spamEventHandler; private readonly ISpamEventHandler _spamEventHandler;
private readonly IRulesManager _rulesManager; private readonly IRulesManager _rulesManager;
private readonly IWorkContextAccessor _workContextAccessor;
public DefaultSpamService( public DefaultSpamService(
ITokenizer tokenizer, ITokenizer tokenizer,
IEnumerable<ISpamFilterProvider> providers, IEnumerable<ISpamFilterProvider> providers,
ISpamEventHandler spamEventHandler, ISpamEventHandler spamEventHandler,
IRulesManager rulesManager IRulesManager rulesManager,
IWorkContextAccessor workContextAccessor
) { ) {
_tokenizer = tokenizer; _tokenizer = tokenizer;
_providers = providers; _providers = providers;
_spamEventHandler = spamEventHandler; _spamEventHandler = spamEventHandler;
_rulesManager = rulesManager; _rulesManager = rulesManager;
_workContextAccessor = workContextAccessor;
} }
public SpamStatus CheckForSpam(string text, SpamFilterAction action, IContent content) { public SpamStatus CheckForSpam(CommentCheckContext context, SpamFilterAction action, IContent content) {
if (string.IsNullOrWhiteSpace(text)) { if (string.IsNullOrWhiteSpace(context.CommentContent)) {
return SpamStatus.Ham; return SpamStatus.Ham;
} }
@@ -38,13 +41,13 @@ namespace Orchard.AntiSpam.Services {
switch (action) { switch (action) {
case SpamFilterAction.AllOrNothing: case SpamFilterAction.AllOrNothing:
if (spamFilters.All(x => x.CheckForSpam(text) == SpamStatus.Spam)) { if (spamFilters.All(x => x.CheckForSpam(context) == SpamStatus.Spam)) {
result = SpamStatus.Spam; result = SpamStatus.Spam;
} }
break; break;
case SpamFilterAction.One: case SpamFilterAction.One:
if (spamFilters.Any(x => x.CheckForSpam(text) == SpamStatus.Spam)) { if (spamFilters.Any(x => x.CheckForSpam(context) == SpamStatus.Spam)) {
result = SpamStatus.Spam; result = SpamStatus.Spam;
} }
@@ -71,57 +74,67 @@ namespace Orchard.AntiSpam.Services {
} }
public SpamStatus CheckForSpam(SpamFilterPart part) { public SpamStatus CheckForSpam(SpamFilterPart part) {
var settings = part.TypePartDefinition.Settings.GetModel<SpamFilterPartSettings>(); var settings = part.TypePartDefinition.Settings.GetModel<SpamFilterPartSettings>();
var context = CreateCommentCheckContext(part, _workContextAccessor.GetContext());
// evaluate the text to submit to the spam filters if (string.IsNullOrWhiteSpace(context.CommentContent)) {
var text = _tokenizer.Replace(settings.Pattern, new Dictionary<string, object> { { "Content", part.ContentItem } });
if (string.IsNullOrWhiteSpace(text)) {
return SpamStatus.Ham; return SpamStatus.Ham;
} }
var result = CheckForSpam(text, settings.Action, part); var result = CheckForSpam(context, settings.Action, part);
return result; return result;
} }
public void ReportSpam(string text) { public void ReportSpam(CommentCheckContext context) {
var spamFilters = GetSpamFilters().ToList(); var spamFilters = GetSpamFilters().ToList();
foreach(var filter in spamFilters) { foreach(var filter in spamFilters) {
filter.ReportSpam(text); filter.ReportSpam(context);
} }
} }
public void ReportSpam(SpamFilterPart part) { public void ReportSpam(SpamFilterPart part) {
var settings = part.TypePartDefinition.Settings.GetModel<SpamFilterPartSettings>(); ReportSpam(CreateCommentCheckContext(part, _workContextAccessor.GetContext()));
// evaluate the text to submit to the spam filters
var text = _tokenizer.Replace(settings.Pattern, new Dictionary<string, object> { { "Content", part.ContentItem } });
ReportSpam(text);
} }
public void ReportHam(string text) { public void ReportHam(CommentCheckContext context) {
var spamFilters = GetSpamFilters().ToList(); var spamFilters = GetSpamFilters().ToList();
foreach (var filter in spamFilters) { foreach (var filter in spamFilters) {
filter.ReportHam(text); filter.ReportHam(context);
} }
} }
public void ReportHam(SpamFilterPart part) { public void ReportHam(SpamFilterPart part) {
var settings = part.TypePartDefinition.Settings.GetModel<SpamFilterPartSettings>(); ReportHam(CreateCommentCheckContext(part, _workContextAccessor.GetContext()));
// evaluate the text to submit to the spam filters
var text = _tokenizer.Replace(settings.Pattern, new Dictionary<string, object> { { "Content", part.ContentItem } });
ReportHam(text);
} }
public IEnumerable<ISpamFilter> GetSpamFilters() { public IEnumerable<ISpamFilter> GetSpamFilters() {
return _providers.SelectMany(x => x.GetSpamFilters()).Where(x => x != null); return _providers.SelectMany(x => x.GetSpamFilters()).Where(x => x != null);
} }
private CommentCheckContext CreateCommentCheckContext(SpamFilterPart part, WorkContext workContext) {
var settings = part.TypePartDefinition.Settings.GetModel<SpamFilterPartSettings>();
var data = new Dictionary<string, object> {{"Content", part.ContentItem}};
var context = new CommentCheckContext {
Url = _tokenizer.Replace(settings.UrlPattern, data),
Permalink = _tokenizer.Replace(settings.PermalinkPattern, data),
CommentAuthor = _tokenizer.Replace(settings.CommentAuthorPattern, data),
CommentAuthorEmail = _tokenizer.Replace(settings.CommentAuthorEmailPattern, data),
CommentAuthorUrl = _tokenizer.Replace(settings.CommentAuthorUrlPattern, data),
CommentContent = _tokenizer.Replace(settings.CommentContentPattern, data),
};
if(workContext.HttpContext != null) {
context.UserIp = workContext.HttpContext.Request.ServerVariables["REMOTE_ADDR"];
context.UserAgent = workContext.HttpContext.Request.UserAgent;
context.Referrer = Convert.ToString(workContext.HttpContext.Request.UrlReferrer);
}
return context;
}
} }
} }

View File

@@ -9,20 +9,20 @@ namespace Orchard.AntiSpam.Services {
/// <summary> /// <summary>
/// Checks if some content is spam. /// Checks if some content is spam.
/// </summary> /// </summary>
/// <param name="text">The text to check.</param> /// <param name="context">The comment to check.</param>
/// <returns><value>SpamStatus.Spam</value> if the text has been categorized as spam, <value>SpamStatus.Ham</value> otherwise.</returns> /// <returns><value>SpamStatus.Spam</value> if the comment has been categorized as spam, <value>SpamStatus.Ham</value> otherwise.</returns>
SpamStatus CheckForSpam(string text); SpamStatus CheckForSpam(CommentCheckContext context);
/// <summary> /// <summary>
/// Explicitely report some content as spam in order to improve the service. /// Explicitely report some content as spam in order to improve the service.
/// </summary> /// </summary>
/// <param name="text">The text to report as spam.</param> /// <param name="context">The comment to report as spam.</param>
void ReportSpam(string text); void ReportSpam(CommentCheckContext context);
/// <summary> /// <summary>
/// Explicitely report some content as ham in order to improve the service. /// Explicitely report some content as ham in order to improve the service.
/// </summary> /// </summary>
/// <param name="text">The text to report as ham (false positive).</param> /// <param name="context">The comment to report as ham (false positive).</param>
void ReportHam(string text); void ReportHam(CommentCheckContext context);
} }
} }

View File

@@ -5,14 +5,14 @@ using Orchard.ContentManagement;
namespace Orchard.AntiSpam.Services { namespace Orchard.AntiSpam.Services {
public interface ISpamService : IDependency { public interface ISpamService : IDependency {
SpamStatus CheckForSpam(string text, SpamFilterAction action, IContent content); SpamStatus CheckForSpam(CommentCheckContext text, SpamFilterAction action, IContent content);
SpamStatus CheckForSpam(SpamFilterPart part); SpamStatus CheckForSpam(SpamFilterPart part);
/// <summary> /// <summary>
/// Explicitely report some content as spam in order to improve the service. /// Explicitely report some content as spam in order to improve the service.
/// </summary> /// </summary>
/// <param name="text">The text to report as spam.</param> /// <param name="context">The comment context to report as spam.</param>
void ReportSpam(string text); void ReportSpam(CommentCheckContext context);
/// <summary> /// <summary>
/// Explicitely report some content as ham in order to improve the service. /// Explicitely report some content as ham in order to improve the service.
@@ -23,8 +23,8 @@ namespace Orchard.AntiSpam.Services {
/// <summary> /// <summary>
/// Explicitely report some content as ham in order to improve the service. /// Explicitely report some content as ham in order to improve the service.
/// </summary> /// </summary>
/// <param name="text">The text to report as ham (false positive).</param> /// <param name="context">The comment context to report as ham (false positive).</param>
void ReportHam(string text); void ReportHam(CommentCheckContext context);
/// <summary> /// <summary>
/// Explicitely report some content as ham in order to improve the service. /// Explicitely report some content as ham in order to improve the service.

View File

@@ -1,8 +1,14 @@
namespace Orchard.AntiSpam.Settings { namespace Orchard.AntiSpam.Settings {
public class SpamFilterPartSettings { public class SpamFilterPartSettings {
public SpamFilterAction Action { get; set; } public SpamFilterAction Action { get; set; }
public string Pattern { get; set; }
public bool DeleteSpam { get; set; } public bool DeleteSpam { get; set; }
public string UrlPattern { get; set; }
public string PermalinkPattern { get; set; }
public string CommentAuthorPattern { get; set; }
public string CommentAuthorEmailPattern { get; set; }
public string CommentAuthorUrlPattern { get; set; }
public string CommentContentPattern { get; set; }
} }
/// <summary> /// <summary>

View File

@@ -30,8 +30,13 @@ namespace Orchard.AntiSpam.Settings {
if (updateModel.TryUpdateModel(settings, "SpamFilterPartSettings", null, null)) { if (updateModel.TryUpdateModel(settings, "SpamFilterPartSettings", null, null)) {
builder.WithSetting("SpamFilterPartSettings.Action", settings.Action.ToString()); builder.WithSetting("SpamFilterPartSettings.Action", settings.Action.ToString());
builder.WithSetting("SpamFilterPartSettings.Pattern", settings.Pattern);
builder.WithSetting("SpamFilterPartSettings.DeleteSpam", settings.DeleteSpam.ToString(CultureInfo.InvariantCulture)); builder.WithSetting("SpamFilterPartSettings.DeleteSpam", settings.DeleteSpam.ToString(CultureInfo.InvariantCulture));
builder.WithSetting("SpamFilterPartSettings.UrlPattern", settings.UrlPattern);
builder.WithSetting("SpamFilterPartSettings.PermalinkPattern", settings.PermalinkPattern);
builder.WithSetting("SpamFilterPartSettings.CommentAuthorPattern", settings.CommentAuthorPattern);
builder.WithSetting("SpamFilterPartSettings.CommentAuthorUrlPattern", settings.CommentAuthorUrlPattern);
builder.WithSetting("SpamFilterPartSettings.CommentAuthorEmailPattern", settings.CommentAuthorEmailPattern);
builder.WithSetting("SpamFilterPartSettings.CommentContentPattern", settings.CommentContentPattern);
} }
yield return DefinitionTemplate(settings); yield return DefinitionTemplate(settings);

View File

@@ -15,14 +15,6 @@
</div> </div>
</fieldset> </fieldset>
<fieldset>
<label for="@Html.FieldIdFor(m => m.Pattern)">@T("Analyzed text")</label>
<div>
@Html.TextBoxFor(m => m.Pattern, new { @class = "text large tokenized" })
<span class="hint">@T("The tokenized pattern generating the text to submit to spam filters.")</span>
</div>
</fieldset>
<fieldset> <fieldset>
<div> <div>
@Html.EditorFor(m => m.DeleteSpam) @Html.EditorFor(m => m.DeleteSpam)
@@ -30,3 +22,52 @@
<span class="hint">@T("Enable to have spam automatically deleted when found. You won't be able to find false positive.")</span> <span class="hint">@T("Enable to have spam automatically deleted when found. You won't be able to find false positive.")</span>
</div> </div>
</fieldset> </fieldset>
<fieldset>
<label for="@Html.FieldIdFor(m => m.UrlPattern)">@T("Url")</label>
<div>
@Html.TextBoxFor(m => m.UrlPattern, new { @class = "text large tokenized" })
<span class="hint">@T("The front page or home URL of the instance making the request. For a blog or wiki this would be the front page. Note: Must be a full URI, including http://.")</span>
</div>
</fieldset>
<fieldset>
<label for="@Html.FieldIdFor(m => m.PermalinkPattern)">@T("Permalink")</label>
<div>
@Html.TextBoxFor(m => m.PermalinkPattern, new { @class = "text large tokenized" })
<span class="hint">@T("The permanent location of the entry the content item was submitted to.")</span>
</div>
</fieldset>
<fieldset>
<label for="@Html.FieldIdFor(m => m.CommentAuthorPattern)">@T("Author")</label>
<div>
@Html.TextBoxFor(m => m.CommentAuthorPattern, new { @class = "text large tokenized" })
<span class="hint">@T("Name submitted with the content item.")</span>
</div>
</fieldset>
<fieldset>
<label for="@Html.FieldIdFor(m => m.CommentAuthorEmailPattern)">@T("Author's email")</label>
<div>
@Html.TextBoxFor(m => m.CommentAuthorEmailPattern, new { @class = "text large tokenized" })
<span class="hint">@T("Email address submitted with the content item.")</span>
</div>
</fieldset>
<fieldset>
<label for="@Html.FieldIdFor(m => m.CommentAuthorUrlPattern)">@T("Author's url")</label>
<div>
@Html.TextBoxFor(m => m.CommentAuthorUrlPattern, new { @class = "text large tokenized" })
<span class="hint">@T("URL submitted with the content item.")</span>
</div>
</fieldset>
<fieldset>
<label for="@Html.FieldIdFor(m => m.CommentContentPattern)">@T("Comment content")</label>
<div>
@Html.TextBoxFor(m => m.CommentContentPattern, new { @class = "text large tokenized" })
<span class="hint">@T("The content that was submitted.")</span>
</div>
</fieldset>

View File

@@ -21,10 +21,12 @@ namespace Orchard.Comments.Tokens {
public Localizer T { get; set; } public Localizer T { get; set; }
public void Describe(dynamic context) { public void Describe(dynamic context) {
context.For("Content", T("Content Items"), T("Content Items")) context.For("Content", T("Comments"), T("Comments"))
.Token("CommentedOn", T("Commented On"), T("The content item this comment was created on.")) .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("CommentMessage", T("Comment Message"), T("The text of the comment itself"))
.Token("CommentAuthor", T("Comment Author"), T("The author of the comment.")) .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."))
; ;
} }
@@ -34,6 +36,8 @@ namespace Orchard.Comments.Tokens {
.Chain("CommentedOn", "Content", (Func<IContent, object>)(content => _contentManager.Get(content.As<CommentPart>().CommentedOn))) .Chain("CommentedOn", "Content", (Func<IContent, object>)(content => _contentManager.Get(content.As<CommentPart>().CommentedOn)))
.Token("CommentMessage", (Func<IContent, object>)(content => content.As<CommentPart>().CommentText)) .Token("CommentMessage", (Func<IContent, object>)(content => content.As<CommentPart>().CommentText))
.Token("CommentAuthor", (Func<IContent, object>)CommentAuthor) .Token("CommentAuthor", (Func<IContent, object>)CommentAuthor)
.Token("CommentAuthorUrl", (Func<IContent, object>)(content => content.As<CommentPart>().SiteName))
.Token("CommentAuthorEmail", (Func<IContent, object>)(content => content.As<CommentPart>().Email))
; ;
} }