Including Orchard.AntiSpam

--HG--
branch : 1.x
This commit is contained in:
Sebastien Ros
2012-08-22 15:34:39 -07:00
parent b1b6895d39
commit 67bdd45068
57 changed files with 1866 additions and 0 deletions

View File

@@ -0,0 +1,16 @@
using Orchard.Localization;
using Orchard.UI.Navigation;
namespace Orchard.AntiSpam {
public class AdminMenu : INavigationProvider {
public Localizer T { get; set; }
public string MenuName { get { return "admin"; } }
public void GetNavigation(NavigationBuilder builder) {
builder.Add(T("Spam"), "11",
menu => menu
.Add(T("Manage Spam"), "1.0", item => item.Action("Index", "Admin", new { area = "Orchard.AntiSpam" }).Permission(Permissions.ManageAntiSpam))
);
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appSettings>
<add key="webpages:Enabled" value="false" />
</appSettings>
<system.web>
<httpHandlers>
<!-- iis6 - for any request in this location, return via managed static file handler -->
<add path="*" verb="*" type="System.Web.StaticFileHandler" />
</httpHandlers>
</system.web>
<system.webServer>
<staticContent>
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" />
</staticContent>
<handlers accessPolicy="Script,Read">
<!--
iis7 - for any request to a file exists on disk, return it via native http module.
accessPolicy 'Script' is to allow for a managed 404 page.
-->
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
</handlers>
</system.webServer>
</configuration>

View File

@@ -0,0 +1,135 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web.Mvc;
using System.Web.Routing;
using Orchard.AntiSpam.Models;
using Orchard.AntiSpam.Services;
using Orchard.AntiSpam.ViewModels;
using Orchard.ContentManagement;
using Orchard.Core.Common.Models;
using Orchard.DisplayManagement;
using Orchard.Localization;
using Orchard.Settings;
using Orchard.UI.Navigation;
namespace Orchard.AntiSpam.Controllers {
[ValidateInput(false)]
public class AdminController : Controller {
private readonly ISiteService _siteService;
private readonly ISpamService _spamService;
public AdminController(
IOrchardServices services,
IShapeFactory shapeFactory,
ISiteService siteService,
ISpamService spamService) {
_siteService = siteService;
_spamService = spamService;
Services = services;
T = NullLocalizer.Instance;
Shape = shapeFactory;
}
dynamic Shape { get; set; }
public IOrchardServices Services { get; set; }
public Localizer T { get; set; }
public ActionResult Index(SpamIndexOptions options, PagerParameters pagerParameters) {
if (!Services.Authorizer.Authorize(Permissions.ManageAntiSpam, T("Not authorized to manage spam")))
return new HttpUnauthorizedResult();
var pager = new Pager(_siteService.GetSiteSettings(), pagerParameters);
// default options
if (options == null)
options = new SpamIndexOptions();
var query = Services.ContentManager.Query().ForPart<SpamFilterPart>();
switch(options.Filter) {
case SpamFilter.Spam:
query = query.Where<SpamFilterPartRecord>(x => x.Status == SpamStatus.Spam);
break;
case SpamFilter.Ham:
query = query.Where<SpamFilterPartRecord>(x => x.Status == SpamStatus.Ham);
break;
case SpamFilter.All:
break;
default:
throw new ArgumentOutOfRangeException();
}
var pagerShape = Shape.Pager(pager).TotalItemCount(query.Count());
switch (options.Order) {
case SpamOrder.Creation:
query = query.Join<CommonPartRecord>().OrderByDescending(u => u.CreatedUtc);
break;
}
var results = query
.Slice(pager.GetStartIndex(), pager.PageSize);
var model = new SpamIndexViewModel {
Spams = results.Select(x => new SpamEntry {
Spam = x.As<SpamFilterPart>(),
Shape = Services.ContentManager.BuildDisplay(x, "SummaryAdmin")
}).ToList(),
Options = options,
Pager = pagerShape
};
// maintain previous route data when generating page links
var routeData = new RouteData();
routeData.Values.Add("Options.Filter", options.Filter);
routeData.Values.Add("Options.Search", options.Search);
routeData.Values.Add("Options.Order", options.Order);
pagerShape.RouteData(routeData);
return View(model);
}
[HttpPost]
[Core.Contents.Controllers.FormValueRequired("submit.BulkEdit")]
public ActionResult Index(SpamIndexOptions options, IEnumerable<int> itemIds) {
if (!Services.Authorizer.Authorize(Permissions.ManageAntiSpam, T("Not authorized to manage spam")))
return new HttpUnauthorizedResult();
switch (options.BulkAction) {
case SpamBulkAction.None:
break;
case SpamBulkAction.Spam:
foreach (var checkedId in itemIds) {
var spam = Services.ContentManager.Get(checkedId);
if(spam != null) {
spam.As<SpamFilterPart>().Status = SpamStatus.Spam;
_spamService.ReportSpam(spam.As<SpamFilterPart>());
}
}
break;
case SpamBulkAction.Ham:
foreach (var checkedId in itemIds) {
var ham = Services.ContentManager.Get(checkedId);
if (ham != null) {
ham.As<SpamFilterPart>().Status = SpamStatus.Ham;
_spamService.ReportHam(ham.As<SpamFilterPart>());
}
}
break;
case SpamBulkAction.Delete:
foreach (var checkedId in itemIds) {
Services.ContentManager.Remove(Services.ContentManager.Get(checkedId));
}
break;
}
Services.ContentManager.Flush();
return Index(options, new PagerParameters());
}
}
}

View File

@@ -0,0 +1,98 @@
using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Web;
using Orchard.AntiSpam.Models;
using Orchard.AntiSpam.Settings;
using Orchard.AntiSpam.ViewModels;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.Localization;
namespace Orchard.AntiSpam.Drivers {
public class ReCaptchaPartDriver : ContentPartDriver<ReCaptchaPart> {
private readonly IWorkContextAccessor _workContextAccessor;
private const string ReCaptchaUrl = "http://www.google.com/recaptcha/api";
private const string ReCaptchaSecureUrl = "https://www.google.com/recaptcha/api";
public ReCaptchaPartDriver(IWorkContextAccessor workContextAccessor) {
_workContextAccessor = workContextAccessor;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
protected override DriverResult Editor(ReCaptchaPart part, dynamic shapeHelper) {
return ContentShape("Parts_ReCaptcha_Fields", () => {
var settings = part.TypePartDefinition.Settings.GetModel<ReCaptchaPartSettings>();
var viewModel = new ReCaptchaPartEditViewModel {
PublicKey = settings.PublicKey
};
return shapeHelper.EditorTemplate(TemplateName: "Parts.ReCaptcha.Fields", Model: viewModel, Prefix: Prefix);
});
}
protected override DriverResult Editor(ReCaptchaPart part, IUpdateModel updater, dynamic shapeHelper) {
var submitViewModel = new ReCaptchaPartSubmitViewModel();
if(updater.TryUpdateModel(submitViewModel, String.Empty, null, null)) {
var settings = part.TypePartDefinition.Settings.GetModel<ReCaptchaPartSettings>();
var context = _workContextAccessor.GetContext().HttpContext;
var result = ExecuteValidateRequest(
settings.PrivateKey,
context.Request.ServerVariables["REMOTE_ADDR"],
submitViewModel.recaptcha_challenge_field,
submitViewModel.recaptcha_response_field
);
if(!HandleValidateResponse(context, result)) {
updater.AddModelError("Parts_ReCaptcha_Fields", T("Incorrect word"));
}
}
return Editor(part, shapeHelper);
}
private static string ExecuteValidateRequest(string privateKey, string remoteip, string challenge, string response) {
WebRequest request = WebRequest.Create(ReCaptchaUrl + "/verify");
request.Method = "POST";
request.Timeout = 5000; //milliseconds
request.ContentType = "application/x-www-form-urlencoded";
var postData = String.Format(CultureInfo.InvariantCulture,
"privatekey={0}&remoteip={1}&challenge={2}&response={3}",
privateKey,
remoteip,
challenge,
response
);
byte[] content = Encoding.UTF8.GetBytes(postData);
using (Stream stream = request.GetRequestStream()) {
stream.Write(content, 0, content.Length);
}
using (WebResponse webResponse = request.GetResponse()) {
using (var reader = new StreamReader(webResponse.GetResponseStream())) {
return reader.ReadToEnd();
}
}
}
internal static bool HandleValidateResponse(HttpContextBase context, string response) {
if (!String.IsNullOrEmpty(response)) {
string[] results = response.Split('\n');
if (results.Length > 0) {
bool rval = Convert.ToBoolean(results[0], CultureInfo.InvariantCulture);
return rval;
}
}
return false;
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using Orchard.AntiSpam.Models;
using Orchard.ContentManagement.Drivers;
using Orchard.ContentManagement.Handlers;
using Orchard.Localization;
namespace Orchard.AntiSpam.Drivers {
public class SpamFilterPartDriver : ContentPartDriver<SpamFilterPart> {
private const string TemplateName = "Parts/SpamFilter";
public SpamFilterPartDriver(IOrchardServices services) {
T = NullLocalizer.Instance;
Services = services;
}
public Localizer T { get; set; }
public IOrchardServices Services { get; set; }
protected override string Prefix {
get { return "SpamFilter"; }
}
protected override DriverResult Display(SpamFilterPart part, string displayType, dynamic shapeHelper) {
return ContentShape("Parts_SpamFilter_Metadata_SummaryAdmin",
() => shapeHelper.Parts_SpamFilter_Metadata_SummaryAdmin());
}
protected override void Importing(SpamFilterPart part, ImportContentContext context) {
var status = context.Attribute(part.PartDefinition.Name, "Status");
if (status != null) {
SpamStatus value;
if(Enum.TryParse(status, out value)) {
part.Status = value;
}
}
}
protected override void Exporting(SpamFilterPart part, ExportContentContext context) {
context.Element(part.PartDefinition.Name).SetAttributeValue("Status", part.Status.ToString());
}
}
}

View File

@@ -0,0 +1,72 @@
using System;
using Orchard.AntiSpam.Models;
using Orchard.AntiSpam.Settings;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Drivers;
using Orchard.Core.Common.Models;
using Orchard.Localization;
using Orchard.Services;
namespace Orchard.AntiSpam.Drivers {
public class SubmissionLimitPartDriver : ContentPartDriver<SubmissionLimitPart> {
private readonly IContentManager _contentManager;
private readonly IClock _clock;
public SubmissionLimitPartDriver(IContentManager contentManager, IClock clock) {
_contentManager = contentManager;
_clock = clock;
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
protected override DriverResult Editor(SubmissionLimitPart part, dynamic shapeHelper) {
return null;
}
protected override DriverResult Editor(SubmissionLimitPart part, IUpdateModel updater, dynamic shapeHelper) {
var settings = part.TypePartDefinition.Settings.GetModel<SubmissionLimitPartSettings>();
DateTime min = DateTime.MinValue, max = DateTime.MinValue;
var now = _clock.UtcNow;
switch(settings.Unit) {
case (int)SubmissionLimitUnit.Hour:
min = new DateTime(now.Year, now.Month, now.Day, now.Hour, 0, 0, DateTimeKind.Utc);
max = min.AddHours(1);
break;
case (int)SubmissionLimitUnit.Day:
min = new DateTime(now.Year, now.Month, now.Day, 0, 0, 0, DateTimeKind.Utc);
max = min.AddDays(1);
break;
case (int)SubmissionLimitUnit.Month:
min = new DateTime(now.Year, now.Month, 0, 0, 0, 0, DateTimeKind.Utc);
max = min.AddMonths(1);
break;
case (int)SubmissionLimitUnit.Year:
min = new DateTime(now.Year, 0, 0, now.Hour, 0, 0, DateTimeKind.Utc);
max = min.AddYears(1);
break;
case (int)SubmissionLimitUnit.Overall:
break;
}
var query = _contentManager.Query()
.ForVersion(VersionOptions.AllVersions)
.ForType(part.ContentItem.ContentType);
if (min != DateTime.MinValue) {
query = query.Join<CommonPartRecord>().Where(x => x.CreatedUtc > min && x.CreatedUtc < max);
}
var result = query.Count();
if(result > settings.Limit) {
updater.AddModelError("Limit", T("The limit of submissions has been reached."));
}
return null;
}
}
}

View File

@@ -0,0 +1,29 @@
using JetBrains.Annotations;
using Orchard.AntiSpam.Models;
using Orchard.ContentManagement;
using Orchard.Data;
using Orchard.ContentManagement.Handlers;
using Orchard.Environment.Extensions;
using Orchard.Localization;
namespace Orchard.AntiSpam.Handlers {
[UsedImplicitly]
[OrchardFeature("Akismet.Filter")]
public class AkismetSettingsPartHandler : ContentHandler {
public AkismetSettingsPartHandler(IRepository<AkismetSettingsPartRecord> repository) {
T = NullLocalizer.Instance;
Filters.Add(new ActivatingFilter<AkismetSettingsPart>("Site"));
Filters.Add(StorageFilter.For(repository));
Filters.Add(new TemplateFilterForRecord<AkismetSettingsPartRecord>("AkismetSettings", "Parts/AntiSpam.AkismetSettings", "spam"));
}
public Localizer T { get; set; }
protected override void GetItemMetadata(GetContentItemMetadataContext context) {
if (context.ContentItem.ContentType != "Site")
return;
base.GetItemMetadata(context);
context.Metadata.EditorGroupInfo.Add(new GroupInfo(T("Spam")));
}
}
}

View File

@@ -0,0 +1,16 @@
using Orchard.AntiSpam.Models;
using Orchard.AntiSpam.Services;
using Orchard.ContentManagement.Handlers;
using Orchard.Data;
namespace Orchard.AntiSpam.Handlers {
public class SpamFilterPartHandler : ContentHandler {
public SpamFilterPartHandler(ISpamService spamService, IRepository<SpamFilterPartRecord> repository) {
Filters.Add(StorageFilter.For(repository));
OnUpdated<SpamFilterPart>( (context, part) => {
part.Status = spamService.CheckForSpam(part);
});
}
}
}

View File

@@ -0,0 +1,29 @@
using JetBrains.Annotations;
using Orchard.AntiSpam.Models;
using Orchard.ContentManagement;
using Orchard.Data;
using Orchard.ContentManagement.Handlers;
using Orchard.Environment.Extensions;
using Orchard.Localization;
namespace Orchard.AntiSpam.Handlers {
[UsedImplicitly]
[OrchardFeature("TypePad.Filter")]
public class TypePadSettingsPartHandler : ContentHandler {
public TypePadSettingsPartHandler(IRepository<TypePadSettingsPartRecord> repository) {
T = NullLocalizer.Instance;
Filters.Add(new ActivatingFilter<TypePadSettingsPart>("Site"));
Filters.Add(StorageFilter.For(repository));
Filters.Add(new TemplateFilterForRecord<TypePadSettingsPartRecord>("TypePadSettings", "Parts/AntiSpam.TypePadSettings", "spam"));
}
public Localizer T { get; set; }
protected override void GetItemMetadata(GetContentItemMetadataContext context) {
if (context.ContentItem.ContentType != "Site")
return;
base.GetItemMetadata(context);
context.Metadata.EditorGroupInfo.Add(new GroupInfo(T("Spam")));
}
}
}

View File

@@ -0,0 +1,62 @@
using Orchard.ContentManagement.MetaData;
using Orchard.Core.Contents.Extensions;
using Orchard.Data.Migration;
using Orchard.Environment.Extensions;
namespace Orchard.AntiSpam {
public class Migrations : DataMigrationImpl {
public int Create() {
ContentDefinitionManager.AlterPartDefinition("SubmissionLimitPart", cfg => cfg
.Attachable()
);
ContentDefinitionManager.AlterPartDefinition("ReCaptchaPart", cfg => cfg
.Attachable()
);
ContentDefinitionManager.AlterPartDefinition("SpamFilterPart", cfg => cfg
.Attachable()
);
SchemaBuilder.CreateTable("SpamFilterPartRecord",
table => table.ContentPartVersionRecord()
.Column<string>("Status", c => c.WithLength(64))
);
return 1;
}
}
[OrchardFeature("Akismet.Filter")]
public class AkismetMigrations : DataMigrationImpl {
public int Create() {
SchemaBuilder.CreateTable("AkismetSettingsPartRecord",
table => table.ContentPartVersionRecord()
.Column<bool>("TrustAuthenticatedUsers")
.Column<string>("ApiKey")
);
return 1;
}
}
[OrchardFeature("TypePad.Filter")]
public class TypePadMigrations : DataMigrationImpl {
public int Create() {
SchemaBuilder.CreateTable("TypePadSettingsPartRecord",
table => table.ContentPartVersionRecord()
.Column<bool>("TrustAuthenticatedUsers")
.Column<string>("ApiKey")
);
return 1;
}
}
}

View File

@@ -0,0 +1,6 @@
using Orchard.ContentManagement;
namespace Orchard.AntiSpam.Models {
public class AkismetSettingsPart : ContentPart<AkismetSettingsPartRecord> {
}
}

View File

@@ -0,0 +1,11 @@
using Orchard.ContentManagement.Records;
namespace Orchard.AntiSpam.Models {
public class AkismetSettingsPartRecord : ContentPartRecord {
public virtual bool TrustAuthenticatedUsers { get; set; }
// API key for the Akismet (http://akismet.com/personal/)
public virtual string ApiKey { get; set; }
}
}

View File

@@ -0,0 +1,6 @@
using Orchard.ContentManagement;
namespace Orchard.AntiSpam.Models {
public class ReCaptchaPart : ContentPart {
}
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Orchard.ContentManagement.Records;
namespace Orchard.AntiSpam.Models {
public class SpamFilterPartRecord : ContentPartRecord {
public virtual SpamStatus Status { get; set; }
}
}

View File

@@ -0,0 +1,16 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using Orchard.ContentManagement;
using Orchard.ContentManagement.Records;
namespace Orchard.AntiSpam.Models {
public class SpamFilterPart : ContentPart<SpamFilterPartRecord> {
public SpamStatus Status {
get { return Record.Status; }
set { Record.Status = value; }
}
}
}

View File

@@ -0,0 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace Orchard.AntiSpam.Models {
public enum SpamStatus {
Spam,
Ham
}
}

View File

@@ -0,0 +1,6 @@
using Orchard.ContentManagement;
namespace Orchard.AntiSpam.Models {
public class SubmissionLimitPart : ContentPart {
}
}

View File

@@ -0,0 +1,6 @@
using Orchard.ContentManagement;
namespace Orchard.AntiSpam.Models {
public class TypePadSettingsPart : ContentPart<TypePadSettingsPartRecord> {
}
}

View File

@@ -0,0 +1,11 @@
using Orchard.ContentManagement.Records;
namespace Orchard.AntiSpam.Models {
public class TypePadSettingsPartRecord : ContentPartRecord {
public virtual bool TrustAuthenticatedUsers { get; set; }
// API key for TypePad AntiSpam (http://antispam.typepad.com/info/get-api-key.html).
public virtual string ApiKey { get; set; }
}
}

View File

@@ -0,0 +1,23 @@
Name: AntiSpam
AntiForgery: enabled
Author: The Orchard Team
Website: http://orchardproject.net
Version: 1.5
OrchardVersion: 1.0
Description: Provides anti-spam services to protect your content from malicious submissions.
Features:
Orchard.AntiSpam:
Name: Anti-Spam
Description: Provides anti-spam services to protect your content from malicious submissions.
Category: Security
Dependencies: Orchard.Tokens
Akismet.Filter:
Name: Akismet Anti-Spam Filter
Description: Provides an anti-spam filter based on Akismet.
Category: Security
Dependencies: Orchard.AntiSpam
TypePad.Filter:
Name: TypePad Anti-Spam Filter
Description: Provides an anti-spam filter based on TypePad.
Category: Security
Dependencies: Orchard.AntiSpam

View File

@@ -0,0 +1,204 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
<PropertyGroup>
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
<ProductVersion>9.0.30729</ProductVersion>
<SchemaVersion>2.0</SchemaVersion>
<ProjectGuid>{91BC2E7F-DA04-421C-98EF-76D37CEC130C}</ProjectGuid>
<ProjectTypeGuids>{349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc}</ProjectTypeGuids>
<OutputType>Library</OutputType>
<AppDesignerFolder>Properties</AppDesignerFolder>
<RootNamespace>Orchard.AntiSpam</RootNamespace>
<AssemblyName>Orchard.AntiSpam</AssemblyName>
<TargetFrameworkVersion>v4.0</TargetFrameworkVersion>
<MvcBuildViews>false</MvcBuildViews>
<FileUpgradeFlags>
</FileUpgradeFlags>
<OldToolsVersion>4.0</OldToolsVersion>
<UpgradeBackupLocation>
</UpgradeBackupLocation>
<TargetFrameworkProfile />
<UseIISExpress>false</UseIISExpress>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
<DebugSymbols>true</DebugSymbols>
<DebugType>full</DebugType>
<Optimize>false</Optimize>
<OutputPath>bin\</OutputPath>
<DefineConstants>DEBUG;TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DebugType>pdbonly</DebugType>
<Optimize>true</Optimize>
<OutputPath>bin\</OutputPath>
<DefineConstants>TRACE</DefineConstants>
<ErrorReport>prompt</ErrorReport>
<WarningLevel>4</WarningLevel>
<CodeAnalysisRuleSet>AllRules.ruleset</CodeAnalysisRuleSet>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.CSharp" />
<Reference Include="System" />
<Reference Include="System.Data" />
<Reference Include="System.ComponentModel.DataAnnotations">
<RequiredTargetFramework>3.5</RequiredTargetFramework>
</Reference>
<Reference Include="System.Web.DynamicData" />
<Reference Include="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<SpecificVersion>False</SpecificVersion>
<HintPath>..\..\..\..\lib\aspnetmvc\System.Web.Mvc.dll</HintPath>
</Reference>
<Reference Include="System.Web" />
<Reference Include="System.Web.Abstractions" />
<Reference Include="System.Web.Routing" />
<Reference Include="System.Xml" />
<Reference Include="System.Configuration" />
<Reference Include="System.Xml.Linq" />
</ItemGroup>
<ItemGroup>
<Content Include="Content\Admin\Images\spam.gif" />
<Content Include="Web.config" />
<Content Include="Views\Web.config" />
<Content Include="Scripts\Web.config" />
<Content Include="Styles\Web.config" />
<Content Include="Properties\AssemblyInfo.cs" />
<Content Include="Module.txt" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\Orchard\Orchard.Framework.csproj">
<Project>{2D1D92BB-4555-4CBE-8D0E-63563D6CE4C6}</Project>
<Name>Orchard.Framework</Name>
</ProjectReference>
<ProjectReference Include="..\..\Core\Orchard.Core.csproj">
<Project>{9916839C-39FC-4CEB-A5AF-89CA7E87119F}</Project>
<Name>Orchard.Core</Name>
</ProjectReference>
<ProjectReference Include="..\Orchard.Tokens\Orchard.Tokens.csproj">
<Project>{6F759635-13D7-4E94-BCC9-80445D63F117}</Project>
<Name>Orchard.Tokens</Name>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<Compile Include="AdminMenu.cs" />
<Compile Include="Controllers\AdminController.cs" />
<Compile Include="Drivers\SpamFilterPartDriver.cs" />
<Compile Include="Drivers\ReCaptchaPartDriver.cs" />
<Compile Include="Drivers\SubmissionLimitPartDriver.cs" />
<Compile Include="Handlers\AkismetSettingsPartHandler.cs" />
<Compile Include="Handlers\TypePadSettingsPartHandler.cs" />
<Compile Include="Handlers\SpamFilterPartHandler.cs" />
<Compile Include="Migrations.cs" />
<Compile Include="Models\AkismetSettingsPartRecord.cs" />
<Compile Include="Models\AkismetSettingsPart.cs" />
<Compile Include="Models\TypePadSettingsPart.cs" />
<Compile Include="Models\TypePadSettingsPartRecord.cs" />
<Compile Include="Models\SpamFilterPart.cs" />
<Compile Include="Models\ReCaptchaPart.cs" />
<Compile Include="Models\SpamFilterPartRecord.cs" />
<Compile Include="Models\SpamStatus.cs" />
<Compile Include="Models\SubmissionLimitPart.cs" />
<Compile Include="Permissions.cs" />
<Compile Include="Rules\AntiSpamEvents.cs" />
<Compile Include="Rules\IRulesManager.cs" />
<Compile Include="Services\AkismetApiSpamFilter.cs" />
<Compile Include="Services\AkismetSpamFilterProvider.cs" />
<Compile Include="Services\ISpamEventHandler.cs" />
<Compile Include="Services\TypePadSpamFilterProvider.cs" />
<Compile Include="Services\DefaultSpamFilterProvider.cs" />
<Compile Include="Services\NullSpamFilterProvider.cs" />
<Compile Include="Services\ISpamFilterProvider.cs" />
<Compile Include="Services\DefaultSpamService.cs" />
<Compile Include="Services\ISpamFilter.cs" />
<Compile Include="Services\ISpamService.cs" />
<Compile Include="Settings\SpamFilterPartSettings.cs" />
<Compile Include="Settings\SpamFilterPartSettingsEvents.cs" />
<Compile Include="Settings\ReCaptchaPartSettings.cs" />
<Compile Include="Settings\ReCaptchaPartSettingsEvents.cs" />
<Compile Include="Settings\SubmissionLimitPartSettings.cs" />
<Compile Include="Settings\SubmissionLimitPartSettingsEvents.cs" />
<Compile Include="ViewModels\SpamIndexViewModel.cs" />
<Compile Include="ViewModels\ReCaptchaPartEditViewModel.cs" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Admin\Index.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\DefinitionTemplates\ReCaptchaPartSettings.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\DefinitionTemplates\SubmissionLimitPartSettings.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\EditorTemplates\Parts.ReCaptcha.Fields.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\DefinitionTemplates\SpamFilterPartSettings.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\Parts\SpamFilter.Metadata.SummaryAdmin.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Placement.info">
<SubType>Designer</SubType>
</Content>
</ItemGroup>
<ItemGroup>
<Content Include="Content\Web.config" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\EditorTemplates\Parts\AntiSpam.AkismetSettings.cshtml" />
</ItemGroup>
<ItemGroup>
<Content Include="Views\EditorTemplates\Parts\AntiSpam.TypePadSettings.cshtml" />
</ItemGroup>
<ItemGroup />
<PropertyGroup>
<VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">10.0</VisualStudioVersion>
<VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
</PropertyGroup>
<Import Project="$(MSBuildBinPath)\Microsoft.CSharp.targets" />
<Import Project="$(VSToolsPath)\WebApplications\Microsoft.WebApplication.targets" Condition="'$(VSToolsPath)' != ''" />
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v10.0\WebApplications\Microsoft.WebApplication.targets" Condition="false" />
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
Other similar extension points exist, see Microsoft.Common.targets.
<Target Name="BeforeBuild">
</Target> -->
<Target Name="AfterBuild" DependsOnTargets="AfterBuildCompiler">
<PropertyGroup>
<AreasManifestDir>$(ProjectDir)\..\Manifests</AreasManifestDir>
</PropertyGroup>
<!-- If this is an area child project, uncomment the following line:
<CreateAreaManifest AreaName="$(AssemblyName)" AreaType="Child" AreaPath="$(ProjectDir)" ManifestPath="$(AreasManifestDir)" ContentFiles="@(Content)" />
-->
<!-- If this is an area parent project, uncomment the following lines:
<CreateAreaManifest AreaName="$(AssemblyName)" AreaType="Parent" AreaPath="$(ProjectDir)" ManifestPath="$(AreasManifestDir)" ContentFiles="@(Content)" />
<CopyAreaManifests ManifestPath="$(AreasManifestDir)" CrossCopy="false" RenameViews="true" />
-->
</Target>
<Target Name="AfterBuildCompiler" Condition="'$(MvcBuildViews)'=='true'">
<AspNetCompiler VirtualPath="temp" PhysicalPath="$(ProjectDir)\..\$(ProjectName)" />
</Target>
<ProjectExtensions>
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<UseIIS>False</UseIIS>
<AutoAssignPort>True</AutoAssignPort>
<DevelopmentServerPort>45979</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>
<IISUrl>
</IISUrl>
<NTLMAuthentication>False</NTLMAuthentication>
<UseCustomServer>True</UseCustomServer>
<CustomServerUrl>http://orchard.codeplex.com</CustomServerUrl>
<SaveServerSettingsInUserFile>False</SaveServerSettingsInUserFile>
</WebProjectProperties>
</FlavorProperties>
</VisualStudio>
</ProjectExtensions>
</Project>

View File

@@ -0,0 +1,43 @@
using System.Collections.Generic;
using Orchard.Environment.Extensions.Models;
using Orchard.Security.Permissions;
namespace Orchard.AntiSpam {
public class Permissions : IPermissionProvider {
public static readonly Permission ManageAntiSpam = new Permission { Description = "Manage anti-spam", Name = "ManageAntiSpam" };
public virtual Feature Feature { get; set; }
public IEnumerable<Permission> GetPermissions() {
return new[] {
ManageAntiSpam,
};
}
public IEnumerable<PermissionStereotype> GetDefaultStereotypes() {
return new[] {
new PermissionStereotype {
Name = "Administrator",
Permissions = new[] {ManageAntiSpam}
},
new PermissionStereotype {
Name = "Editor",
Permissions = new[] {ManageAntiSpam}
},
new PermissionStereotype {
Name = "Moderator",
Permissions = new[] {ManageAntiSpam}
},
new PermissionStereotype {
Name = "Author",
Permissions = new Permission[0]
},
new PermissionStereotype {
Name = "Contributor",
Permissions = new Permission[0]
},
};
}
}
}

View File

@@ -0,0 +1,9 @@
<Placement>
<Place Parts_ReCaptcha_Fields="Content:10"/>
<Match DisplayType="SummaryAdmin">
<Place Parts_SpamFilter_Metadata_SummaryAdmin="Meta:2"/>
</Match>
<Place Parts_AntiSpam_AkismetSettings="Content:9"/>
<Place Parts_AntiSpam_TypePadSettings="Content:10"/>
</Placement>

View File

@@ -0,0 +1,36 @@
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Security;
// 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.AntiSpam")]
[assembly: AssemblyDescription("")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyProduct("Orchard")]
[assembly: AssemblyCopyright("")]
[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("ba9fd801-bcde-400a-bcdc-6f40538678b8")]
// 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("1.5")]
[assembly: AssemblyFileVersion("1.5")]

View File

@@ -0,0 +1,26 @@
using System;
using Orchard.Events;
using Orchard.Localization;
namespace Orchard.AntiSpam.Rules {
public interface IEventProvider : IEventHandler {
void Describe(dynamic describe);
}
public class AntiSpamEvents : IEventProvider {
public AntiSpamEvents() {
T = NullLocalizer.Instance;
}
public Localizer T { get; set; }
public void Describe(dynamic describe) {
Func<dynamic, bool> alwaysTrue = c => true;
describe.For("AntiSpam", T("Anti-Spam"), T("Anti-Spam"))
.Element("Spam", T("Spam Reported"), T("Content is categorized as spam."), alwaysTrue, (Func<dynamic, LocalizedString>)(context => T("When content is categorized as spam.")), null)
.Element("Ham", T("Ham Reported"), T("Content is categorized as ham."), alwaysTrue, (Func<dynamic, LocalizedString>)(context => T("When content is categorized as ham.")), null)
;
}
}
}

View File

@@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using Orchard.Events;
namespace Orchard.AntiSpam.Rules {
public interface IRulesManager : IEventHandler {
void TriggerEvent(string category, string type, Func<Dictionary<string, object>> tokensContext);
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appSettings>
<add key="webpages:Enabled" value="false" />
</appSettings>
<system.web>
<httpHandlers>
<!-- iis6 - for any request in this location, return via managed static file handler -->
<add path="*" verb="*" type="System.Web.StaticFileHandler" />
</httpHandlers>
</system.web>
<system.webServer>
<staticContent>
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" />
</staticContent>
<handlers accessPolicy="Script,Read">
<!--
iis7 - for any request to a file exists on disk, return it via native http module.
accessPolicy 'Script' is to allow for a managed 404 page.
-->
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
</handlers>
</system.webServer>
</configuration>

View File

@@ -0,0 +1,101 @@
using System;
using System.Globalization;
using System.IO;
using System.Net;
using System.Text;
using System.Web;
using Orchard.AntiSpam.Models;
using Orchard.Logging;
using Orchard.Utility.Extensions;
namespace Orchard.AntiSpam.Services {
public class AkismetApiSpamFilter : ISpamFilter {
private readonly string _endpoint;
private readonly string _apiKey;
private readonly HttpContextBase _context;
private const string AkismetApiPattern = "http://{0}.{1}/1.1/{2}";
public AkismetApiSpamFilter(string endpoint, string apiKey, HttpContextBase context) {
_endpoint = endpoint;
_apiKey = apiKey;
_context = context;
Logger = NullLogger.Instance;
}
public ILogger Logger { get; set; }
public SpamStatus CheckForSpam(string text) {
try {
var result = ExecuteValidateRequest(text, "comment-check");
if (HandleValidateResponse(_context, result)) {
return SpamStatus.Spam;
}
return SpamStatus.Ham;
}
catch(Exception e) {
Logger.Error(e, "An error occured while checking for spam");
return SpamStatus.Spam;
}
}
public void ReportSpam(string text) {
try {
var result = ExecuteValidateRequest(text, "submit-spam");
}
catch (Exception e) {
Logger.Error(e, "An error occured while reporting spam");
}
}
public void ReportHam(string text) {
try {
var result = ExecuteValidateRequest(text, "submit-ham");
}
catch (Exception e) {
Logger.Error(e, "An error occured while reporting ham");
}
}
private string ExecuteValidateRequest(string text, string action) {
var uri = String.Format(AkismetApiPattern, _apiKey, _endpoint, action);
WebRequest request = WebRequest.Create(uri);
request.Method = "POST";
request.Timeout = 5000; //milliseconds
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}",
HttpUtility.UrlEncode(_context.Request.ToApplicationRootUrlString()),
HttpUtility.UrlEncode(_context.Request.ServerVariables["REMOTE_ADDR"]),
HttpUtility.UrlEncode(_context.Request.UserAgent),
HttpUtility.UrlEncode(Convert.ToString(_context.Request.UrlReferrer)),
HttpUtility.UrlEncode(text)
);
byte[] content = Encoding.UTF8.GetBytes(postData);
using (Stream stream = request.GetRequestStream()) {
stream.Write(content, 0, content.Length);
}
using (WebResponse webResponse = request.GetResponse()) {
using (var reader = new StreamReader(webResponse.GetResponseStream())) {
return reader.ReadToEnd();
}
}
}
internal static bool HandleValidateResponse(HttpContextBase context, string response) {
if (!String.IsNullOrEmpty(response)) {
string[] results = response.Split('\n');
if (results.Length > 0) {
bool rval = Convert.ToBoolean(results[0], CultureInfo.InvariantCulture);
return rval;
}
}
return false;
}
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using Orchard.AntiSpam.Models;
using Orchard.ContentManagement;
using Orchard.Environment.Extensions;
namespace Orchard.AntiSpam.Services {
/// <summary>
/// Implements <see cref="ISpamFilterProvider"/> by returning an Akismet filter
/// </summary>
[OrchardFeature("Akismet.Filter")]
public class AkismetSpamFilterProvider : ISpamFilterProvider {
private readonly IOrchardServices _orchardServices;
private const string AkismetServiceUrl = "rest.akismet.com";
public AkismetSpamFilterProvider(IOrchardServices orchardServices) {
_orchardServices = orchardServices;
}
public IEnumerable<ISpamFilter> GetSpamFilters() {
var settings = _orchardServices.WorkContext.CurrentSite.As<AkismetSettingsPart>().Record;
if (string.IsNullOrWhiteSpace(settings.ApiKey)) {
yield break;
}
// don't return any filter if authenticated users are trusted, and current user authenticated
if(_orchardServices.WorkContext.CurrentUser != null && settings.TrustAuthenticatedUsers) {
yield break;
}
var filter = new AkismetApiSpamFilter(AkismetServiceUrl, settings.ApiKey, _orchardServices.WorkContext.HttpContext);
yield return filter;
}
}
}

View File

@@ -0,0 +1,12 @@
using System.Collections.Generic;
namespace Orchard.AntiSpam.Services {
/// <summary>
/// Implements <see cref="ISpamFilterProvider"/> by returning Akismet and TypePad services if they are configured
/// </summary>
public class DefaultSpamFilterProvider : ISpamFilterProvider {
public IEnumerable<ISpamFilter> GetSpamFilters() {
yield break;
}
}
}

View File

@@ -0,0 +1,113 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Orchard.AntiSpam.Models;
using Orchard.AntiSpam.Rules;
using Orchard.AntiSpam.Settings;
using Orchard.Tokens;
namespace Orchard.AntiSpam.Services {
public class DefaultSpamService : ISpamService {
private readonly ITokenizer _tokenizer;
private readonly IEnumerable<ISpamFilterProvider> _providers;
private readonly ISpamEventHandler _spamEventHandler;
private readonly IRulesManager _rulesManager;
public DefaultSpamService(
ITokenizer tokenizer,
IEnumerable<ISpamFilterProvider> providers,
ISpamEventHandler spamEventHandler,
IRulesManager rulesManager
) {
_tokenizer = tokenizer;
_providers = providers;
_spamEventHandler = spamEventHandler;
_rulesManager = rulesManager;
}
public SpamStatus CheckForSpam(string text, SpamFilterAction action) {
var spamFilters = GetSpamFilters().ToList();
switch (action) {
case SpamFilterAction.AllOrNothing:
if (spamFilters.All(x => x.CheckForSpam(text) == SpamStatus.Spam)) {
return SpamStatus.Spam;
}
return SpamStatus.Ham;
case SpamFilterAction.One:
if (spamFilters.Any(x => x.CheckForSpam(text) == SpamStatus.Spam)) {
return SpamStatus.Spam;
}
return SpamStatus.Ham;
default:
throw new ArgumentOutOfRangeException();
}
}
public SpamStatus CheckForSpam(SpamFilterPart part) {
var settings = part.TypePartDefinition.Settings.GetModel<SpamFilterPartSettings>();
// evaluate the text to submit to the spam filters
var text = _tokenizer.Replace(settings.Pattern, new Dictionary<string, object> { { "Content", part.ContentItem } });
var result = CheckForSpam(text, settings.Action);
// trigger events and rules
switch (result) {
case SpamStatus.Spam:
_spamEventHandler.SpamReported(part);
_rulesManager.TriggerEvent("AntiSpam", "Spam", () => new Dictionary<string, object> { { "Content", part.ContentItem } });
break;
case SpamStatus.Ham:
_spamEventHandler.HamReported(part);
_rulesManager.TriggerEvent("AntiSpam", "Ham", () => new Dictionary<string, object> { { "Content", part.ContentItem } });
break;
default:
throw new ArgumentOutOfRangeException();
}
return result;
}
public void ReportSpam(string text) {
var spamFilters = GetSpamFilters().ToList();
foreach(var filter in spamFilters) {
filter.ReportSpam(text);
}
}
public void ReportSpam(SpamFilterPart part) {
var settings = part.TypePartDefinition.Settings.GetModel<SpamFilterPartSettings>();
// 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) {
var spamFilters = GetSpamFilters().ToList();
foreach (var filter in spamFilters) {
filter.ReportHam(text);
}
}
public void ReportHam(SpamFilterPart part) {
var settings = part.TypePartDefinition.Settings.GetModel<SpamFilterPartSettings>();
// 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() {
return _providers.SelectMany(x => x.GetSpamFilters()).Where(x => x != null);
}
}
}

View File

@@ -0,0 +1,9 @@
using Orchard.ContentManagement;
using Orchard.Events;
namespace Orchard.AntiSpam.Services {
public interface ISpamEventHandler : IEventHandler {
void SpamReported(IContent content);
void HamReported(IContent content);
}
}

View File

@@ -0,0 +1,28 @@
using Orchard.AntiSpam.Models;
namespace Orchard.AntiSpam.Services {
/// <summary>
/// Implementations of <see cref="ISpamFilter"/> are used to filter some user submitted
/// content using anti-spam services
/// </summary>
public interface ISpamFilter : IDependency {
/// <summary>
/// Checks if some content is spam.
/// </summary>
/// <param name="text">The text to check.</param>
/// <returns><value>SpamStatus.Spam</value> if the text has been categorized as spam, <value>SpamStatus.Ham</value> otherwise.</returns>
SpamStatus CheckForSpam(string text);
/// <summary>
/// Explicitely report some content as spam in order to improve the service.
/// </summary>
/// <param name="text">The text to report as spam.</param>
void ReportSpam(string text);
/// <summary>
/// Explicitely report some content as ham in order to improve the service.
/// </summary>
/// <param name="text">The text to report as ham (false positive).</param>
void ReportHam(string text);
}
}

View File

@@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace Orchard.AntiSpam.Services {
public interface ISpamFilterProvider : IDependency {
IEnumerable<ISpamFilter> GetSpamFilters();
}
}

View File

@@ -0,0 +1,36 @@
using System.Collections.Generic;
using Orchard.AntiSpam.Models;
using Orchard.AntiSpam.Settings;
namespace Orchard.AntiSpam.Services {
public interface ISpamService : IDependency {
SpamStatus CheckForSpam(string text, SpamFilterAction action);
SpamStatus CheckForSpam(SpamFilterPart part);
/// <summary>
/// Explicitely report some content as spam in order to improve the service.
/// </summary>
/// <param name="text">The text to report as spam.</param>
void ReportSpam(string text);
/// <summary>
/// Explicitely report some content as ham in order to improve the service.
/// </summary>
/// <param name="part">The content part to report as ham (false positive).</param>
void ReportSpam(SpamFilterPart part);
/// <summary>
/// Explicitely report some content as ham in order to improve the service.
/// </summary>
/// <param name="text">The text to report as ham (false positive).</param>
void ReportHam(string text);
/// <summary>
/// Explicitely report some content as ham in order to improve the service.
/// </summary>
/// <param name="part">The content part to report as ham (false positive).</param>
void ReportHam(SpamFilterPart part);
IEnumerable<ISpamFilter> GetSpamFilters();
}
}

View File

@@ -0,0 +1,13 @@
using System.Collections.Generic;
namespace Orchard.AntiSpam.Services {
/// <summary>
/// Default implementation for <see cref="ISpamFilterProvider"/> in order to have at least one result
/// when it is resolved, event if there is no other concrete provider.
/// </summary>
public class NullSpamFilterProvider : ISpamFilterProvider {
public IEnumerable<ISpamFilter> GetSpamFilters() {
yield break;
}
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using Orchard.AntiSpam.Models;
using Orchard.ContentManagement;
using Orchard.Environment.Extensions;
namespace Orchard.AntiSpam.Services {
/// <summary>
/// Implements <see cref="ISpamFilterProvider"/> by returning an TypePad filter
/// </summary>
[OrchardFeature("TypePad.Filter")]
public class TypePadSpamFilterProvider : ISpamFilterProvider {
private readonly IOrchardServices _orchardServices;
private const string TypePadServiceUrl = "api.antispam.typepad.com";
public TypePadSpamFilterProvider(IOrchardServices orchardServices) {
_orchardServices = orchardServices;
}
public IEnumerable<ISpamFilter> GetSpamFilters() {
var settings = _orchardServices.WorkContext.CurrentSite.As<TypePadSettingsPart>().Record;
if(string.IsNullOrWhiteSpace(settings.ApiKey)) {
yield break;
}
// don't return any filter if authenticated users are trusted, and current user authenticated
if(_orchardServices.WorkContext.CurrentUser != null && settings.TrustAuthenticatedUsers) {
yield break;
}
var filter = new AkismetApiSpamFilter(TypePadServiceUrl, settings.ApiKey, _orchardServices.WorkContext.HttpContext);
yield return filter;
}
}
}

View File

@@ -0,0 +1,6 @@
namespace Orchard.AntiSpam.Settings {
public class ReCaptchaPartSettings {
public string PublicKey { get; set; }
public string PrivateKey { get; set; }
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using Orchard.ContentManagement;
using Orchard.ContentManagement.MetaData;
using Orchard.ContentManagement.MetaData.Builders;
using Orchard.ContentManagement.MetaData.Models;
using Orchard.ContentManagement.ViewModels;
using Orchard.Localization;
namespace Orchard.AntiSpam.Settings {
public class ReCaptchaPartSettingsEvents : ContentDefinitionEditorEventsBase {
public Localizer T { get; set; }
public override IEnumerable<TemplateViewModel> TypePartEditor(ContentTypePartDefinition definition) {
if (definition.PartDefinition.Name != "ReCaptchaPart")
yield break;
var settings = definition.Settings.GetModel<ReCaptchaPartSettings>();
yield return DefinitionTemplate(settings);
}
public override IEnumerable<TemplateViewModel> TypePartEditorUpdate(ContentTypePartDefinitionBuilder builder, IUpdateModel updateModel) {
if (builder.Name != "ReCaptchaPart")
yield break;
var settings = new ReCaptchaPartSettings {
};
if (updateModel.TryUpdateModel(settings, "ReCaptchaPartSettings", null, null)) {
builder.WithSetting("ReCaptchaPartSettings.PublicKey", settings.PublicKey);
builder.WithSetting("ReCaptchaPartSettings.PrivateKey", settings.PrivateKey);
}
yield return DefinitionTemplate(settings);
}
}
}

View File

@@ -0,0 +1,14 @@
namespace Orchard.AntiSpam.Settings {
public class SpamFilterPartSettings {
public SpamFilterAction Action { get; set; }
public string Pattern { get; set; }
}
/// <summary>
/// The action to take when spam filters occur
/// </summary>
public enum SpamFilterAction {
AllOrNothing, // Mark as spam if all provider declare spam
One // Mark as spam if at least one declares spam
}
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Generic;
using Orchard.ContentManagement;
using Orchard.ContentManagement.MetaData;
using Orchard.ContentManagement.MetaData.Builders;
using Orchard.ContentManagement.MetaData.Models;
using Orchard.ContentManagement.ViewModels;
using Orchard.Localization;
namespace Orchard.AntiSpam.Settings {
public class SpamFilterPartSettingsEvents : ContentDefinitionEditorEventsBase {
public Localizer T { get; set; }
public override IEnumerable<TemplateViewModel> TypePartEditor(ContentTypePartDefinition definition) {
if (definition.PartDefinition.Name != "SpamFilterPart")
yield break;
var settings = definition.Settings.GetModel<SpamFilterPartSettings>();
yield return DefinitionTemplate(settings);
}
public override IEnumerable<TemplateViewModel> TypePartEditorUpdate(ContentTypePartDefinitionBuilder builder, IUpdateModel updateModel) {
if (builder.Name != "SpamFilterPart")
yield break;
var settings = new SpamFilterPartSettings {
};
if (updateModel.TryUpdateModel(settings, "SpamFilterPartSettings", null, null)) {
builder.WithSetting("SpamFilterPartSettings.Action", settings.Action.ToString());
builder.WithSetting("SpamFilterPartSettings.Pattern", settings.Pattern);
}
yield return DefinitionTemplate(settings);
}
}
}

View File

@@ -0,0 +1,14 @@
namespace Orchard.AntiSpam.Settings {
public class SubmissionLimitPartSettings {
public int Limit { get; set; }
public int Unit { get; set; }
}
public enum SubmissionLimitUnit {
Hour,
Day,
Month,
Year,
Overall
}
}

View File

@@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.Globalization;
using Orchard.ContentManagement;
using Orchard.ContentManagement.MetaData;
using Orchard.ContentManagement.MetaData.Builders;
using Orchard.ContentManagement.MetaData.Models;
using Orchard.ContentManagement.ViewModels;
using Orchard.Localization;
namespace Orchard.AntiSpam.Settings {
public class SubmissionLimitPartSettingsEvents : ContentDefinitionEditorEventsBase {
public Localizer T { get; set; }
public override IEnumerable<TemplateViewModel> TypePartEditor(ContentTypePartDefinition definition) {
if (definition.PartDefinition.Name != "SubmissionLimitPart")
yield break;
var settings = definition.Settings.GetModel<SubmissionLimitPartSettings>();
yield return DefinitionTemplate(settings);
}
public override IEnumerable<TemplateViewModel> TypePartEditorUpdate(ContentTypePartDefinitionBuilder builder, IUpdateModel updateModel) {
if (builder.Name != "SubmissionLimitPart")
yield break;
var settings = new SubmissionLimitPartSettings {
};
if (updateModel.TryUpdateModel(settings, "SubmissionLimitPartSettings", null, null)) {
builder.WithSetting("SubmissionLimitPartSettings.Limit", settings.Limit.ToString(CultureInfo.InvariantCulture));
builder.WithSetting("SubmissionLimitPartSettings.Unit", settings.Unit.ToString(CultureInfo.InvariantCulture));
}
yield return DefinitionTemplate(settings);
}
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
<appSettings>
<add key="webpages:Enabled" value="false" />
</appSettings>
<system.web>
<httpHandlers>
<!-- iis6 - for any request in this location, return via managed static file handler -->
<add path="*" verb="*" type="System.Web.StaticFileHandler" />
</httpHandlers>
</system.web>
<system.webServer>
<staticContent>
<clientCache cacheControlMode="UseMaxAge" cacheControlMaxAge="7.00:00:00" />
</staticContent>
<handlers accessPolicy="Script,Read">
<!--
iis7 - for any request to a file exists on disk, return it via native http module.
accessPolicy 'Script' is to allow for a managed 404 page.
-->
<add name="StaticFile" path="*" verb="*" modules="StaticFileModule" preCondition="integratedMode" resourceType="File" requireAccess="Read" />
</handlers>
</system.webServer>
</configuration>

View File

@@ -0,0 +1,10 @@
namespace Orchard.AntiSpam.ViewModels {
public class ReCaptchaPartEditViewModel {
public string PublicKey { get; set; }
}
public class ReCaptchaPartSubmitViewModel {
public string recaptcha_challenge_field { get; set; }
public string recaptcha_response_field { get; set; }
}
}

View File

@@ -0,0 +1,41 @@
using System.Collections.Generic;
using Orchard.AntiSpam.Models;
namespace Orchard.AntiSpam.ViewModels {
public class SpamIndexViewModel {
public IList<SpamEntry> Spams { get; set; }
public SpamIndexOptions Options { get; set; }
public dynamic Pager { get; set; }
}
public class SpamEntry {
public SpamFilterPart Spam { get; set; }
public dynamic Shape { get; set; }
public bool IsChecked { get; set; }
}
public class SpamIndexOptions {
public string Search { get; set; }
public SpamOrder Order { get; set; }
public SpamFilter Filter { get; set; }
public SpamBulkAction BulkAction { get; set; }
}
public enum SpamOrder {
Creation
}
public enum SpamFilter {
Spam,
Ham,
All,
}
public enum SpamBulkAction {
None,
Spam,
Ham,
Delete
}
}

View File

@@ -0,0 +1,61 @@
@model SpamIndexViewModel
@using Orchard.AntiSpam.ViewModels
@using Orchard.Utility.Extensions
@{
var spamIndex = 0;
var pageSizes = new List<int?>() { 10, 50, 100 };
var defaultPageSize = WorkContext.CurrentSite.PageSize;
if (!pageSizes.Contains(defaultPageSize)) {
pageSizes.Add(defaultPageSize);
}
}
<h1>@Html.TitleForPage(T("Manage Spam").ToString()) </h1>
@using (Html.BeginFormAntiForgeryPost()) {
@Html.ValidationSummary()
<div class="manage"></div>
<fieldset class="bulk-actions">
<label for="publishActions">@T("Actions:")</label>
<select id="publishActions" name="@Html.NameOf(m => m.Options.BulkAction)">
@Html.SelectOption(Model.Options.BulkAction, SpamBulkAction.None, T("Choose action...").ToString())
@Html.SelectOption(Model.Options.BulkAction, SpamBulkAction.Spam, T("Mark as Spam").ToString())
@Html.SelectOption(Model.Options.BulkAction, SpamBulkAction.Ham, T("Mark as Ham").ToString())
@Html.SelectOption(Model.Options.BulkAction, SpamBulkAction.Delete, T("Delete").ToString())
</select>
<button type="submit" name="submit.BulkEdit" value="@T("Apply")">@T("Apply")</button>
</fieldset>
<fieldset class="bulk-actions">
<label for="filterResults">@T("Filter:")</label>
<select id="filterResults" name="@Html.NameOf(m => m.Options.Filter)">
@Html.SelectOption(Model.Options.Filter, SpamFilter.All, T("any (show all)").ToString())
@Html.SelectOption(Model.Options.Filter, SpamFilter.Spam, T("Spam").ToString())
@Html.SelectOption(Model.Options.Filter, SpamFilter.Ham, T("Ham").ToString())
</select>
</fieldset>
<fieldset class="bulk-actions">
<label for="sortResults">@T("Sort by:")</label>
<select id="sortResults" name="@Html.NameOf(m => m.Options.Order)">
@Html.SelectOption(Model.Options.Order, SpamOrder.Creation, T("Creation date").ToString())
</select>
<input type="hidden" name="Page" value="1" />
<label for="pageSize">@T("Show:")</label>
<select id="pageSize" name="PageSize">
@Html.SelectOption((int)Model.Pager.PageSize, 0, T("All").ToString())
@foreach (int size in pageSizes.OrderBy(p => p)) {
@Html.SelectOption((int)Model.Pager.PageSize, size, size.ToString())
}
</select>
<button type="submit" name="submit.Filter" value="@T("Filter")">@T("Filter")</button>
</fieldset>
<fieldset class="contentItems bulk-items">
@foreach (var entry in Model.Spams) {
@Display(entry.Shape)
spamIndex++;
}
@Display(Model.Pager)
</fieldset>
}

View File

@@ -0,0 +1,18 @@
@model ReCaptchaPartSettings
@using Orchard.AntiSpam.Settings;
<fieldset>
<div>
@Html.TextBoxFor(m => m.PublicKey, new { @class = "textMedium"})
<span class="hint">@T("Your public key.")</span>
</div>
</fieldset>
<fieldset>
<div>
@Html.TextBoxFor(m => m.PrivateKey, new { @class = "textMedium"})
<span class="hint">@T("Your private key.")</span>
</div>
</fieldset>
@T("Get custom keys:") <a href="http://www.google.com/recaptcha">http://www.google.com/recaptcha</a>

View File

@@ -0,0 +1,24 @@
@model SpamFilterPartSettings
@using Orchard.AntiSpam.Settings;
@Display.TokenHint()
<fieldset>
<label>@T("When should items for this content type be categorized as Spam ?")</label>
<div>
<input type="radio" name="@Html.FieldNameFor(m => m.Action)" id="radio-1" value="@SpamFilterAction.One" @if (Model.Action == SpamFilterAction.One) { <text>checked="checked"</text> } />
<label for="radio-1" class="forcheckbox">@T("At least one filter reports spam")</label>
</div>
<div>
<input type="radio" name="@Html.FieldNameFor(m => m.Action)" id="radio-2" value="@SpamFilterAction.AllOrNothing" @if (Model.Action == SpamFilterAction.AllOrNothing) { <text>checked="checked"</text> } />
<label for="radio-2" class="forcheckbox">@T("All filters report spam")</label>
</div>
</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>

View File

@@ -0,0 +1,21 @@
@model SubmissionLimitPartSettings
@using Orchard.AntiSpam.Settings;
<fieldset>
<div>
@Html.TextBoxFor(m => m.Limit, new { @class = "text-small"})
<span class="hint">@T("The number of allowed submissions.")</span>
</div>
</fieldset>
<fieldset>
<div>
<select name="@Html.FieldNameFor(m => m.Unit)" size="1">
@Html.SelectOption(Model.Unit, (int)SubmissionLimitUnit.Day, T("Day").Text)
@Html.SelectOption(Model.Unit, (int)SubmissionLimitUnit.Hour, T("Hour").Text)
@Html.SelectOption(Model.Unit, (int)SubmissionLimitUnit.Month, T("Month").Text)
@Html.SelectOption(Model.Unit, (int)SubmissionLimitUnit.Year, T("Year").Text)
@Html.SelectOption(Model.Unit, (int)SubmissionLimitUnit.Overall, T("Overall").Text)
</select>
<span class="hint">@T("The period to take into account.")</span>
</div>
</fieldset>

View File

@@ -0,0 +1,8 @@
@model Orchard.AntiSpam.ViewModels.ReCaptchaPartEditViewModel
<script type="text/javascript" src="//www.google.com/recaptcha/api/challenge?k=@Model.PublicKey"></script>
<noscript>
<iframe src="//www.google.com/recaptcha/api/noscript?k=@Model.PublicKey" height="300" width="500" frameborder="0"></iframe><br>
<textarea name="recaptcha_challenge_field" rows="3" cols="40">
</textarea>
<input type="hidden" name="recaptcha_response_field" value="manual_challenge">
</noscript>

View File

@@ -0,0 +1,17 @@
@model Orchard.AntiSpam.Models.AkismetSettingsPartRecord
@using Orchard.Utility.Extensions;
<fieldset>
<legend>@T("Akismet")</legend>
<div>
@Html.EditorFor(m => m.TrustAuthenticatedUsers)
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.TrustAuthenticatedUsers)">@T("Trust content submitted by authenticated users")</label>
<span class="hint">@T("Check if authenticated users should have their content automatically approved.")</span>
</div>
<div>
<label for="@Html.FieldIdFor(m => m.ApiKey)">@T("Api key")</label>
@Html.TextBoxFor(m => m.ApiKey, new { @class = "textMedium" })
@Html.ValidationMessage("ApiKey", "*")
<span class="hint">@T("API key for <a href=\"http://akismet.com/personal/\">Akismet</a> service.")</span>
</div>
</fieldset>

View File

@@ -0,0 +1,17 @@
@model Orchard.AntiSpam.Models.TypePadSettingsPartRecord
@using Orchard.Utility.Extensions;
<fieldset>
<legend>@T("TypePad")</legend>
<div>
@Html.EditorFor(m => m.TrustAuthenticatedUsers)
<label class="forcheckbox" for="@Html.FieldIdFor(m => m.TrustAuthenticatedUsers)">@T("Trust content submitted by authenticated users")</label>
<span class="hint">@T("Check if authenticated users should have their content automatically approved.")</span>
</div>
<div>
<label for="@Html.FieldIdFor(m => m.ApiKey)">@T("Api key")</label>
@Html.TextBoxFor(m => m.ApiKey, new { @class = "textMedium" })
@Html.ValidationMessage("ApiKey", "*")
<span class="hint">@T("API key for <a href=\"http://antispam.typepad.com/info/get-api-key.html\">TypePad AntiSpam</a> service.")</span>
</div>
</fieldset>

View File

@@ -0,0 +1,9 @@
@using Orchard.AntiSpam.Models
@if (Model.ContentPart.Status == SpamStatus.Spam) {
<ul class="pageStatus">
<li>
<img class="icon" src="@Href("~/Modules/Orchard.AntiSpam/Content/Admin/Images/spam.gif")" alt="@T("Spam")" title="@T("The content has been categorized as spam")" />
</li>
<li> @T("Spam")&nbsp;&#124;&nbsp;</li>
</ul>
}

View File

@@ -0,0 +1,41 @@
<?xml version="1.0"?>
<configuration>
<appSettings>
<add key="webpages:Enabled" value="false" />
</appSettings>
<system.web>
<httpHandlers>
</httpHandlers>
<!--
Enabling request validation in view pages would cause validation to occur
after the input has already been processed by the controller. By default
MVC performs request validation before a controller processes the input.
To change this behavior apply the ValidateInputAttribute to a
controller or action.
-->
<pages
validateRequest="false"
pageParserFilterType="System.Web.Mvc.ViewTypeParserFilter, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"
pageBaseType="System.Web.Mvc.ViewPage, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL"
userControlBaseType="System.Web.Mvc.ViewUserControl, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL">
<controls>
<add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35, processorArchitecture=MSIL" namespace="System.Web.Mvc" tagPrefix="mvc" />
</controls>
</pages>
</system.web>
<system.webServer>
<validation validateIntegratedModeConfiguration="false"/>
<handlers>
</handlers>
</system.webServer>
<runtime>
<assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1">
<dependentAssembly>
<assemblyIdentity name="System.Web.Mvc" publicKeyToken="31bf3856ad364e35" />
<bindingRedirect oldVersion="2.0.0.0" newVersion="3.0.0.0" />
</dependentAssembly>
</assemblyBinding>
</runtime>
</configuration>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0"?>
<configuration>
<configSections>
<sectionGroup name="system.web.webPages.razor" type="System.Web.WebPages.Razor.Configuration.RazorWebSectionGroup, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35">
<remove name="host" />
<remove name="pages" />
<section name="host" type="System.Web.WebPages.Razor.Configuration.HostSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
<section name="pages" type="System.Web.WebPages.Razor.Configuration.RazorPagesSection, System.Web.WebPages.Razor, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31BF3856AD364E35" requirePermission="false" />
</sectionGroup>
</configSections>
<system.web.webPages.razor>
<host factoryType="System.Web.Mvc.MvcWebRazorHostFactory, System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<pages pageBaseType="Orchard.Mvc.ViewEngines.Razor.WebViewPage">
<namespaces>
<add namespace="System.Web.Mvc" />
<add namespace="System.Web.Mvc.Ajax" />
<add namespace="System.Web.Mvc.Html" />
<add namespace="System.Web.Routing" />
<add namespace="System.Web.WebPages" />
<add namespace="System.Linq"/>
<add namespace="System.Collections.Generic"/>
<add namespace="Orchard.Mvc.Html"/>
</namespaces>
</pages>
</system.web.webPages.razor>
<system.web>
<compilation targetFramework="4.0">
<assemblies>
<add assembly="System.Web.Abstractions, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add assembly="System.Web.Routing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
<add assembly="System.Data.Linq, Version=4.0.0.0, Culture=neutral, PublicKeyToken=B77A5C561934E089"/>
<add assembly="System.Web.Mvc, Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35" />
<add assembly="System.Web.WebPages, Version=1.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35"/>
</assemblies>
</compilation>
</system.web>
</configuration>