From 5d4087351bbdc4848ab23b7270f465edd67b0ee7 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Mon, 5 Jan 2026 11:29:16 +0300 Subject: [PATCH 1/3] Fix GDPR module --- src/OrchardCoreContrib.Gdpr/AdminMenu.cs | 63 +++++------ .../Controllers/HomeController.cs | 20 ++-- .../CookieConsentFilter.cs | 66 +++++------ .../Drivers/GdprSettingsDisplayDriver.cs | 103 ++++++------------ .../GdprPermissions.cs | 14 +++ src/OrchardCoreContrib.Gdpr/GdprSettings.cs | 33 +++--- src/OrchardCoreContrib.Gdpr/Manifest.cs | 8 -- .../OrchardCoreContrib.Gdpr.csproj | 2 + src/OrchardCoreContrib.Gdpr/Permissions.cs | 56 ++++------ src/OrchardCoreContrib.Gdpr/Startup.cs | 65 ++++++----- .../Views/CookieConsent.cshtml | 25 +++-- .../Views/GdprSettings.Edit.cshtml | 4 +- .../Views/Home/Privacy.cshtml | 4 +- 13 files changed, 203 insertions(+), 260 deletions(-) create mode 100644 src/OrchardCoreContrib.Gdpr/GdprPermissions.cs diff --git a/src/OrchardCoreContrib.Gdpr/AdminMenu.cs b/src/OrchardCoreContrib.Gdpr/AdminMenu.cs index 1a2fb78d..13615b83 100644 --- a/src/OrchardCoreContrib.Gdpr/AdminMenu.cs +++ b/src/OrchardCoreContrib.Gdpr/AdminMenu.cs @@ -1,41 +1,38 @@ -using Microsoft.Extensions.Localization; +using Microsoft.AspNetCore.Routing; +using Microsoft.Extensions.Localization; using OrchardCore.Navigation; using OrchardCoreContrib.Gdpr.Drivers; -using System; -using System.Threading.Tasks; -namespace OrchardCoreContrib.Gdpr -{ - using OrchardCoreContrib.Navigation; +namespace OrchardCoreContrib.Gdpr; - /// - /// Represents an admin menu for GDPR module. - /// - public class AdminMenu : AdminNavigationProvider - { - private readonly IStringLocalizer S; +using OrchardCoreContrib.Navigation; - /// - /// Initializes a new instance of . - /// - /// The . - public AdminMenu(IStringLocalizer stringLocalizer) - { - S = stringLocalizer; - } +/// +/// Represents an admin menu for GDPR module. +/// +/// +/// Initializes a new instance of . +/// +/// The . +public class AdminMenu(IStringLocalizer S) : AdminNavigationProvider +{ + private static readonly RouteValueDictionary _routeValues = new() + { + { "area", "OrchardCore.Settings" }, + { "groupId", GdprSettingsDisplayDriver.GroupId }, + }; - /// - public override void BuildNavigation(NavigationBuilder builder) - { - builder - .Add(S["Configuration"], configuration => configuration - .Add(S["Settings"], settings => settings - .Add(S["GDPR"], S["GDPR"].PrefixPosition(), entry => entry - .AddClass("gdpr").Id("gdpr") - .Action("Index", "Admin", new { area = "OrchardCore.Settings", groupId = GdprSettingsDisplayDriver.GroupId }) - .Permission(Permissions.ManageGdprSettings) - .LocalNav() - ))); - } + /// + public override void BuildNavigation(NavigationBuilder builder) + { + builder + .Add(S["Configuration"], configuration => configuration + .Add(S["Settings"], settings => settings + .Add(S["GDPR"], S["GDPR"].PrefixPosition(), entry => entry + .AddClass("gdpr").Id("gdpr") + .Action("Index", "Admin", _routeValues) + .Permission(GdprPermissions.ManageGdprSettings) + .LocalNav() + ))); } } diff --git a/src/OrchardCoreContrib.Gdpr/Controllers/HomeController.cs b/src/OrchardCoreContrib.Gdpr/Controllers/HomeController.cs index ade6e9ea..22d073a4 100644 --- a/src/OrchardCoreContrib.Gdpr/Controllers/HomeController.cs +++ b/src/OrchardCoreContrib.Gdpr/Controllers/HomeController.cs @@ -1,21 +1,15 @@ using Microsoft.AspNetCore.Mvc; -using OrchardCore.Entities; using OrchardCore.Settings; -namespace OrchardCoreContrib.Gdpr.Controllers +namespace OrchardCoreContrib.Gdpr.Controllers; + +public class HomeController(ISiteService site) : Controller { - public class HomeController : Controller + [HttpGet] + public async Task Privacy() { - private readonly GdprSettings _gdprSettings; - - public HomeController(ISiteService site) - { - _gdprSettings = site.GetSiteSettingsAsync() - .GetAwaiter().GetResult() - .As(); - } + var gdprSettings = (await site.GetSiteSettingsAsync()).As(); - [HttpGet] - public IActionResult Privacy() => View(_gdprSettings); + return View(gdprSettings); } } diff --git a/src/OrchardCoreContrib.Gdpr/CookieConsentFilter.cs b/src/OrchardCoreContrib.Gdpr/CookieConsentFilter.cs index a9f47710..82e9491b 100644 --- a/src/OrchardCoreContrib.Gdpr/CookieConsentFilter.cs +++ b/src/OrchardCoreContrib.Gdpr/CookieConsentFilter.cs @@ -4,55 +4,39 @@ using OrchardCore.Admin; using OrchardCore.DisplayManagement; using OrchardCore.DisplayManagement.Layout; -using System; -using System.Threading.Tasks; -namespace OrchardCoreContrib.Gdpr +namespace OrchardCoreContrib.Gdpr; + +/// +/// Represents a filter that inject a cookie consent shape into the layout. +/// +public class CookieConsentFilter( + IOptions adminOptions, + ILayoutAccessor layoutAccessor, + IShapeFactory shapeFactory) : IAsyncResultFilter { - /// - /// Represents a filter that inject a cookie consent shape into the layout. - /// - public class CookieConsentFilter : IAsyncResultFilter - { - private readonly string _adminUrlPrefix; - private readonly ILayoutAccessor _layoutAccessor; - private readonly IShapeFactory _shapeFactory; + private readonly string _adminUrlPrefix = adminOptions.Value.AdminUrlPrefix; - /// - /// Initializes a new instance of . - /// - /// The . - /// The , - /// The . - public CookieConsentFilter( - IOptions adminOptions, - ILayoutAccessor layoutAccessor, - IShapeFactory shapeFactory) + /// + public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + { + if (context.HttpContext.Request.Path.Value.Contains(_adminUrlPrefix, StringComparison.OrdinalIgnoreCase)) { - _adminUrlPrefix = adminOptions.Value.AdminUrlPrefix; - _layoutAccessor = layoutAccessor; - _shapeFactory = shapeFactory; + await next(); + + return; } - /// - public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) + var consentFeature = context.HttpContext.Features.Get(); + if (!consentFeature?.CanTrack ?? false) { - if (context.HttpContext.Request.Path.Value.Contains(_adminUrlPrefix, StringComparison.OrdinalIgnoreCase)) - { - await next(); + var layout = await layoutAccessor.GetLayoutAsync(); + + var contentZone = layout.Zones["Content"]; - return; - } - - var consentFeature = context.HttpContext.Features.Get(); - if (!consentFeature?.CanTrack ?? false) - { - dynamic layout = await _layoutAccessor.GetLayoutAsync(); - var contentZone = layout.Zones["Content"]; - contentZone.Add(await _shapeFactory.New.CookieConsent()); - } - - await next(); + await contentZone.AddAsync((object)await shapeFactory.New.CookieConsent()); } + + await next(); } } diff --git a/src/OrchardCoreContrib.Gdpr/Drivers/GdprSettingsDisplayDriver.cs b/src/OrchardCoreContrib.Gdpr/Drivers/GdprSettingsDisplayDriver.cs index 7cbb8a00..f5db596a 100644 --- a/src/OrchardCoreContrib.Gdpr/Drivers/GdprSettingsDisplayDriver.cs +++ b/src/OrchardCoreContrib.Gdpr/Drivers/GdprSettingsDisplayDriver.cs @@ -1,91 +1,60 @@ using Microsoft.AspNetCore.Authorization; -using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Http; using OrchardCore.DisplayManagement.Entities; using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using OrchardCore.Environment.Shell; using OrchardCore.Settings; -using System.Collections.Generic; -using System.Threading.Tasks; -namespace OrchardCoreContrib.Gdpr.Drivers +namespace OrchardCoreContrib.Gdpr.Drivers; + +/// +/// Represents a display driver for . +/// +public class GdprSettingsDisplayDriver( + IShellHost shellHost, + ShellSettings shellSettings, + IHttpContextAccessor httpContextAccessor, + IAuthorizationService authorizationService) : SectionDisplayDriver { - /// - /// Represents a display driver for . - /// - public class GdprSettingsDisplayDriver : SectionDisplayDriver - { - public const string GroupId = "gdpr"; + public const string GroupId = "gdpr"; + + /// - private readonly IDataProtectionProvider _dataProtectionProvider; - private readonly IShellHost _shellHost; - private readonly ShellSettings _shellSettings; - private readonly IHttpContextAccessor _httpContextAccessor; - private readonly IAuthorizationService _authorizationService; + public override async Task EditAsync(ISite model, GdprSettings section, BuildEditorContext context) + { + var user = httpContextAccessor.HttpContext?.User; - /// - /// Initializes a new instance of . - /// - /// The . - /// The . - /// The . - /// The . - /// The . - public GdprSettingsDisplayDriver( - IDataProtectionProvider dataProtectionProvider, - IShellHost shellHost, - ShellSettings shellSettings, - IHttpContextAccessor httpContextAccessor, - IAuthorizationService authorizationService) + if (!await authorizationService.AuthorizeAsync(user, GdprPermissions.ManageGdprSettings)) { - _dataProtectionProvider = dataProtectionProvider; - _shellHost = shellHost; - _shellSettings = shellSettings; - _httpContextAccessor = httpContextAccessor; - _authorizationService = authorizationService; + return null; } - /// - - public override async Task EditAsync(ISite model, GdprSettings section, BuildEditorContext context) + return Initialize("GdprSettings_Edit", model => { - var user = _httpContextAccessor.HttpContext?.User; - - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageGdprSettings)) - { - return null; - } + model.Summary = section.Summary; + model.Detail = section.Detail; + }).Location("Content:5") + .OnGroup(GroupId); + } - var shapes = new List - { - Initialize("GdprSettings_Edit", model => - { - model.Summary = section.Summary; - model.Detail = section.Detail; - }).Location("Content:5").OnGroup(GroupId) - }; + /// + public override async Task UpdateAsync(ISite model, GdprSettings section, UpdateEditorContext context) + { + var user = httpContextAccessor.HttpContext?.User; - return Combine(shapes); + if (!await authorizationService.AuthorizeAsync(user, GdprPermissions.ManageGdprSettings)) + { + return null; } - /// - public override async Task UpdateAsync(ISite model, GdprSettings section, UpdateEditorContext context) + if (context.GroupId == GroupId) { - var user = _httpContextAccessor.HttpContext?.User; - - if (!await _authorizationService.AuthorizeAsync(user, Permissions.ManageGdprSettings)) - { - return null; - } + await context.Updater.TryUpdateModelAsync(section, Prefix); - if (context.GroupId == GroupId) - { - await context.Updater.TryUpdateModelAsync(section, Prefix); - await _shellHost.ReleaseShellContextAsync(_shellSettings); - } - - return await EditAsync(model, section, context); + await shellHost.ReleaseShellContextAsync(shellSettings); } + + return await EditAsync(model, section, context); } } diff --git a/src/OrchardCoreContrib.Gdpr/GdprPermissions.cs b/src/OrchardCoreContrib.Gdpr/GdprPermissions.cs new file mode 100644 index 00000000..b3031d9e --- /dev/null +++ b/src/OrchardCoreContrib.Gdpr/GdprPermissions.cs @@ -0,0 +1,14 @@ +using OrchardCore.Security.Permissions; + +namespace OrchardCoreContrib.Gdpr; + +/// +/// Provides predefined permissions related to the GDPR module. +/// +public class GdprPermissions +{ + /// + /// Gets a permission for managing a GDPR settings. + /// + public static readonly Permission ManageGdprSettings = new("ManageGdprSettings", "Manage GDPR Settings"); +} diff --git a/src/OrchardCoreContrib.Gdpr/GdprSettings.cs b/src/OrchardCoreContrib.Gdpr/GdprSettings.cs index 1c78d38d..c9c278c5 100644 --- a/src/OrchardCoreContrib.Gdpr/GdprSettings.cs +++ b/src/OrchardCoreContrib.Gdpr/GdprSettings.cs @@ -1,22 +1,21 @@ -namespace OrchardCoreContrib.Gdpr +namespace OrchardCoreContrib.Gdpr; + +/// +/// Represents a GDPR settings +/// +public class GdprSettings { - /// - /// Represents a GDPR settings - /// - public class GdprSettings - { - private static readonly string DefaultSummary = "Use this page to summarize your privacy and cookie use policy."; + private static readonly string DefaultSummary = "Use this page to summarize your privacy and cookie use policy."; - private static readonly string DefaultDetail = "Use this page to detail your site's privacy policy."; + private static readonly string DefaultDetail = "Use this page to detail your site's privacy policy."; - /// - /// Gets or sets a summary that will be used in the cookie consent UI. - /// - public string Summary { get; set; } = DefaultSummary; + /// + /// Gets or sets a summary that will be used in the cookie consent UI. + /// + public string Summary { get; set; } = DefaultSummary; - /// - /// Gets or sets a detail that will be used in the privacy policy page. - /// - public string Detail { get; set; } = DefaultDetail; - } + /// + /// Gets or sets a detail that will be used in the privacy policy page. + /// + public string Detail { get; set; } = DefaultDetail; } diff --git a/src/OrchardCoreContrib.Gdpr/Manifest.cs b/src/OrchardCoreContrib.Gdpr/Manifest.cs index e1b9565f..a719f5ab 100644 --- a/src/OrchardCoreContrib.Gdpr/Manifest.cs +++ b/src/OrchardCoreContrib.Gdpr/Manifest.cs @@ -7,13 +7,5 @@ Website = ManifestConstants.Website, Version = "1.0.0", Description = "Supports EU General Data Protection Regulation (GDPR).", - Dependencies = new string[] { "OrchardCore.Gdpr" }, - Category = "Security" -)] - -[assembly: Feature( - Id = "OrchardCoreContrib.Gdpr", - Name = "GDPR", - Description = "Supports EU General Data Protection Regulation (GDPR).", Category = "Security" )] diff --git a/src/OrchardCoreContrib.Gdpr/OrchardCoreContrib.Gdpr.csproj b/src/OrchardCoreContrib.Gdpr/OrchardCoreContrib.Gdpr.csproj index a759f801..22fcd7a8 100644 --- a/src/OrchardCoreContrib.Gdpr/OrchardCoreContrib.Gdpr.csproj +++ b/src/OrchardCoreContrib.Gdpr/OrchardCoreContrib.Gdpr.csproj @@ -6,6 +6,7 @@ The Orchard Core Contrib Team Supports EU General Data Protection Regulation (GDPR). + README.md BSD-3-Clause https://github.com/OrchardCoreContrib/OrchardCoreContrib.Modules/tree/main/src/OrchardCoreContrib.Gdpr/README.md https://github.com/OrchardCoreContrib/OrchardCoreContrib.Modules @@ -26,6 +27,7 @@ + diff --git a/src/OrchardCoreContrib.Gdpr/Permissions.cs b/src/OrchardCoreContrib.Gdpr/Permissions.cs index 8780dcb6..e30dd948 100644 --- a/src/OrchardCoreContrib.Gdpr/Permissions.cs +++ b/src/OrchardCoreContrib.Gdpr/Permissions.cs @@ -1,41 +1,31 @@ using OrchardCore.Security.Permissions; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -namespace OrchardCoreContrib.Gdpr +namespace OrchardCoreContrib.Gdpr; + +/// +/// Represents a permissions that will be applied into GDPR module. +/// +public class Permissions : IPermissionProvider { - /// - /// Represents a permissions that will be applied into GDPR module. - /// - public class Permissions : IPermissionProvider - { - /// - /// Gets a permission for managing a GDPR settings. - /// - public static readonly Permission ManageGdprSettings = new Permission("ManageGdprSettings", "Manage GDPR Settings"); + private readonly IEnumerable _allPermissions = + [ + GdprPermissions.ManageGdprSettings, + ]; - /// - public Task> GetPermissionsAsync() - { - return Task.FromResult(new[] - { - ManageGdprSettings - } - .AsEnumerable()); - } + /// + public Task> GetPermissionsAsync() => Task.FromResult(_allPermissions); - /// - public IEnumerable GetDefaultStereotypes() - { - return new[] + + /// + public IEnumerable GetDefaultStereotypes() + { + return + [ + new PermissionStereotype { - new PermissionStereotype - { - Name = "Administrator", - Permissions = new[] { ManageGdprSettings } - }, - }; - } + Name = "Administrator", + Permissions = _allPermissions + }, + ]; } } diff --git a/src/OrchardCoreContrib.Gdpr/Startup.cs b/src/OrchardCoreContrib.Gdpr/Startup.cs index ad3d0a49..97a9d623 100644 --- a/src/OrchardCoreContrib.Gdpr/Startup.cs +++ b/src/OrchardCoreContrib.Gdpr/Startup.cs @@ -11,45 +11,44 @@ using OrchardCore.Settings; using OrchardCoreContrib.Gdpr.Controllers; using OrchardCoreContrib.Gdpr.Drivers; -using System; -namespace OrchardCoreContrib.Gdpr +namespace OrchardCoreContrib.Gdpr; + +/// +/// Represensts a startup point to register the required services by GDPR module. +/// +public class Startup : StartupBase { - /// - /// Represensts a startup point to register the required services by GDPR module. - /// - public class Startup : StartupBase + /// + public override void ConfigureServices(IServiceCollection services) { - /// - public override void ConfigureServices(IServiceCollection services) - { - services.AddScoped(); - services.AddScoped, GdprSettingsDisplayDriver>(); - services.AddScoped(); + services.AddScoped(); + services.AddScoped, GdprSettingsDisplayDriver>(); + services.AddScoped(); - services.Configure(options => - { - options.CheckConsentNeeded = context => true; - options.MinimumSameSitePolicy = SameSiteMode.None; - }); + services.Configure(options => + { + options.CheckConsentNeeded = context => true; + options.MinimumSameSitePolicy = SameSiteMode.None; + }); - services.Configure((options) => - { - options.Filters.Add(typeof(CookieConsentFilter)); - }); - } + services.Configure(options => options.Filters.Add()); + } - /// - public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) - { - builder.UseCookiePolicy(); + /// + public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) + { + builder.UseCookiePolicy(); - routes.MapAreaControllerRoute( - name: "GdprPrivacy", - areaName: "OrchardCoreContrib.Gdpr", - pattern: "/Privacy", - defaults: new { controller = typeof(HomeController).ControllerName(), action = nameof(HomeController.Privacy) } - ); - } + routes.MapAreaControllerRoute( + name: "GdprPrivacy", + areaName: "OrchardCoreContrib.Gdpr", + pattern: "/Privacy", + defaults: new + { + controller = typeof(HomeController).ControllerName(), + action = nameof(HomeController.Privacy) + } + ); } } diff --git a/src/OrchardCoreContrib.Gdpr/Views/CookieConsent.cshtml b/src/OrchardCoreContrib.Gdpr/Views/CookieConsent.cshtml index aedbddd1..1650f030 100644 --- a/src/OrchardCoreContrib.Gdpr/Views/CookieConsent.cshtml +++ b/src/OrchardCoreContrib.Gdpr/Views/CookieConsent.cshtml @@ -2,14 +2,14 @@ @using OrchardCore.Entities @using OrchardCore.Settings @using OrchardCoreContrib.Gdpr -@inject ISiteService Site +@inject ISiteService SiteService @{ var consentFeature = Context.Features.Get(); + var cookieString = consentFeature?.CreateConsentCookie(); - var gdprSettings = Site.GetSiteSettingsAsync() - .GetAwaiter().GetResult() - .As(); + + var gdprSettings = (await SiteService.GetSiteSettingsAsync()).As(); } \ No newline at end of file diff --git a/src/OrchardCoreContrib.Gdpr/Views/GdprSettings.Edit.cshtml b/src/OrchardCoreContrib.Gdpr/Views/GdprSettings.Edit.cshtml index 4866174d..6599f3da 100644 --- a/src/OrchardCoreContrib.Gdpr/Views/GdprSettings.Edit.cshtml +++ b/src/OrchardCoreContrib.Gdpr/Views/GdprSettings.Edit.cshtml @@ -1,8 +1,6 @@ @using OrchardCoreContrib.Gdpr @model GdprSettings -

@T["The current tenant will be reloaded when the settings are saved."]

-
@@ -12,7 +10,7 @@
- + @T["The content to detail your site's privacy policy."]
\ No newline at end of file diff --git a/src/OrchardCoreContrib.Gdpr/Views/Home/Privacy.cshtml b/src/OrchardCoreContrib.Gdpr/Views/Home/Privacy.cshtml index 9b5e370b..21ec399c 100644 --- a/src/OrchardCoreContrib.Gdpr/Views/Home/Privacy.cshtml +++ b/src/OrchardCoreContrib.Gdpr/Views/Home/Privacy.cshtml @@ -2,4 +2,6 @@ @model GdprSettings

Privacy Policy

-

@Model.Detail

\ No newline at end of file +
+ @Html.Raw(@Model.Detail) +
\ No newline at end of file From c3db681817fed5bb6b59bdec29ac620a3acb6714 Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Mon, 5 Jan 2026 11:40:57 +0300 Subject: [PATCH 2/3] Add README.md to OCC.Gdpr module --- src/OrchardCoreContrib.Gdpr/README.md | 37 +++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/OrchardCoreContrib.Gdpr/README.md diff --git a/src/OrchardCoreContrib.Gdpr/README.md b/src/OrchardCoreContrib.Gdpr/README.md new file mode 100644 index 00000000..977e1abb --- /dev/null +++ b/src/OrchardCoreContrib.Gdpr/README.md @@ -0,0 +1,37 @@ +# GDPR Module + +This module supports EU General Data Protection Regulation. + +## Version + +1.4.0 + +## Category + +Security + +## Dependencies + +This module has no dependencies. + +## Features + +| | | +|------------------|--------------------------------------------------------| +| **Name** | Elm Diagnostics (`OrchardCoreContrib.Gdpr`) | +| **Description** | Supports EU General Data Protection Regulation. | +| **Dependencies** | | + +## NuGet Packages + +| Name | Version | +|-------------------------------------------------------------------------------------------|--------------| +| [`OrchardCoreContrib.Gdpr`](https://www.nuget.org/packages/OrchardCoreContrib.Gdpr/1.0.0) | 1.0.0 | + +## Get Started + +1. Install the [`OrchardCoreContrib.Gdpr`](https://www.nuget.org/packages/OrchardCoreContrib.Gdpr/) NuGet package to your Orchard Core host project. +2. Go to the admin site +3. Select **Configuration -> Features** menu. +4. Enable the `GDPR` feature. +5. Go to **Configuration > Settings > GDPR** to configure the GDPR settings, if you want to customize the message and privacy content. From 675eac54d1598fa02b51831b052dbb56fcfcc75d Mon Sep 17 00:00:00 2001 From: Hisham Bin Ateya Date: Mon, 5 Jan 2026 11:43:08 +0300 Subject: [PATCH 3/3] Update README.md --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 1b967ad6..2132c9e2 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,7 @@ The `OrchardCoreContrib.Modules` repository consists of the following modules: | [Data Localization Module](src/OrchardCoreContrib.DataLocalization/README.md) | `OrchardCoreContrib.DataLocalization` | [![NuGet](https://img.shields.io/nuget/v/OrchardCoreContrib.DataLocalization.svg)](https://www.nuget.org/packages/OrchardCoreContrib.DataLocalization) | | [Elm Diagnostics Module](src/OrchardCoreContrib.Diagnostics.Elm/README.md) | `OrchardCoreContrib.Diagnostics.Elm` | [![NuGet](https://img.shields.io/nuget/v/OrchardCoreContrib.Diagnostics.Elm.svg)](https://www.nuget.org/packages/OrchardCoreContrib.Diagnostics.Elm) | | [Gmail Module](src/OrchardCoreContrib.Email.Gmail/README.md) | `OrchardCoreContrib.Email.Gmail` | [![NuGet](https://img.shields.io/nuget/v/OrchardCoreContrib.Email.Gmail.svg)](https://www.nuget.org/packages/OrchardCoreContrib.Email.Gmail) | +| [GDPR Module](src/OrchardCoreContrib.Gdpr/README.md) | `OrchardCoreContrib.Gdpr` | [![NuGet](https://img.shields.io/nuget/v/OrchardCoreContrib.Gdpr.svg)](https://www.nuget.org/packages/OrchardCoreContrib.Gdpr) | | [Hotmail Module](src/OrchardCoreContrib.Email.Hotmail/README.md) | `OrchardCoreContrib.Email.Hotmail` | [![NuGet](https://img.shields.io/nuget/v/OrchardCoreContrib.Email.Hotmail.svg)](https://www.nuget.org/packages/OrchardCoreContrib.Email.Hotmail) | | [SendGrid Module](src/OrchardCoreContrib.Email.SendGrid/README.md) | `OrchardCoreContrib.Email.SendGrid` | [![NuGet](https://img.shields.io/nuget/v/OrchardCoreContrib.Email.SendGrid.svg)](https://www.nuget.org/packages/OrchardCoreContrib.Email.SendGrid) | | [Yahoo Module](src/OrchardCoreContrib.Email.Yahoo/README.md) | `OrchardCoreContrib.Email.Yahoo` | [![NuGet](https://img.shields.io/nuget/v/OrchardCoreContrib.Email.Yahoo.svg)](https://www.nuget.org/packages/OrchardCoreContrib.Email.Yahoo) |