diff --git a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Drivers/ReCaptchaForgotPasswordFormDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Drivers/ReCaptchaForgotPasswordFormDisplayDriver.cs new file mode 100644 index 00000000000..9e4f72f57ec --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Drivers/ReCaptchaForgotPasswordFormDisplayDriver.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.ReCaptcha.Configuration; +using OrchardCore.Settings; +using OrchardCore.Users.Models; + +namespace OrchardCore.ReCaptcha.Drivers; + +public class ReCaptchaForgotPasswordFormDisplayDriver : DisplayDriver +{ + private readonly ISiteService _siteService; + + public ReCaptchaForgotPasswordFormDisplayDriver(ISiteService siteService) + { + _siteService = siteService; + } + + public override async Task EditAsync(ForgotPasswordForm model, BuildEditorContext context) + { + var _reCaptchaSettings = (await _siteService.GetSiteSettingsAsync()).As(); + + if (!_reCaptchaSettings.IsValid()) + { + return null; + } + + return View("FormReCaptcha_Edit", model).Location("Content:after"); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Drivers/ReCaptchaLoginFormDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Drivers/ReCaptchaLoginFormDisplayDriver.cs index 8b2432b7941..735c269b6b6 100644 --- a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Drivers/ReCaptchaLoginFormDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Drivers/ReCaptchaLoginFormDisplayDriver.cs @@ -30,6 +30,6 @@ public override async Task EditAsync(LoginForm model, BuildEdito return null; } - return View("LoginFormReCaptcha_Edit", model).Location("Content:after"); + return View("FormReCaptcha_Edit", model).Location("Content:after"); } } diff --git a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/ReCaptchaLoginFilter.cs b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/ReCaptchaLoginFilter.cs index 411d0e19e1f..2793e29ba8e 100644 --- a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/ReCaptchaLoginFilter.cs +++ b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/ReCaptchaLoginFilter.cs @@ -49,9 +49,6 @@ public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultE var layout = await _layoutAccessor.GetLayoutAsync(); - var afterForgotPasswordZone = layout.Zones["AfterForgotPassword"]; - await afterForgotPasswordZone.AddAsync(await _shapeFactory.CreateAsync("ReCaptcha")); - var afterRegisterZone = layout.Zones["AfterRegister"]; await afterRegisterZone.AddAsync(await _shapeFactory.CreateAsync("ReCaptcha")); diff --git a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Startup.cs b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Startup.cs index b1d37986d92..9a0ebfb1a33 100644 --- a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Startup.cs @@ -39,7 +39,7 @@ public override void ConfigureServices(IServiceCollection services) } [Feature("OrchardCore.ReCaptcha.Users")] - public class StartupUsers : StartupBase + public class UsersStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) { @@ -53,4 +53,14 @@ public override void ConfigureServices(IServiceCollection services) }); } } + + [Feature("OrchardCore.ReCaptcha.Users")] + [RequireFeatures("OrchardCore.Users.ResetPassword")] + public class UsersResetPasswordStartup : StartupBase + { + public override void ConfigureServices(IServiceCollection services) + { + services.AddScoped, ReCaptchaForgotPasswordFormDisplayDriver>(); + } + } } diff --git a/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Views/LoginFormReCaptcha.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.ReCaptcha/Views/FormReCaptcha.Edit.cshtml similarity index 100% rename from src/OrchardCore.Modules/OrchardCore.ReCaptcha/Views/LoginFormReCaptcha.Edit.cshtml rename to src/OrchardCore.Modules/OrchardCore.ReCaptcha/Views/FormReCaptcha.Edit.cshtml diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ResetPasswordController.cs b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ResetPasswordController.cs index 31cb64985a1..796bc532693 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ResetPasswordController.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Controllers/ResetPasswordController.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using System.Text; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; @@ -7,7 +8,11 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Logging; +using OrchardCore.DisplayManagement; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.Environment.Shell; using OrchardCore.Modules; +using OrchardCore.Mvc.Core.Utilities; using OrchardCore.Settings; using OrchardCore.Users.Events; using OrchardCore.Users.Models; @@ -19,31 +24,41 @@ namespace OrchardCore.Users.Controllers [Feature("OrchardCore.Users.ResetPassword")] public class ResetPasswordController : Controller { + private static readonly string _controllerName = typeof(ResetPasswordController).ControllerName(); + private readonly IUserService _userService; private readonly UserManager _userManager; private readonly ISiteService _siteService; private readonly IEnumerable _passwordRecoveryFormEvents; private readonly ILogger _logger; + private readonly IUpdateModelAccessor _updateModelAccessor; + private readonly IDisplayManager _displayManager; + private readonly IShellFeaturesManager _shellFeaturesManager; + protected readonly IStringLocalizer S; public ResetPasswordController( IUserService userService, UserManager userManager, ISiteService siteService, - IStringLocalizer stringLocalizer, ILogger logger, - IEnumerable passwordRecoveryFormEvents) + IUpdateModelAccessor updateModelAccessor, + IDisplayManager displayManager, + IShellFeaturesManager shellFeaturesManager, + IEnumerable passwordRecoveryFormEvents, + IStringLocalizer stringLocalizer) { _userService = userService; _userManager = userManager; _siteService = siteService; - - S = stringLocalizer; _logger = logger; + _updateModelAccessor = updateModelAccessor; + _displayManager = displayManager; + _shellFeaturesManager = shellFeaturesManager; _passwordRecoveryFormEvents = passwordRecoveryFormEvents; + S = stringLocalizer; } - [HttpGet] [AllowAnonymous] public async Task ForgotPassword() { @@ -52,56 +67,63 @@ public async Task ForgotPassword() return NotFound(); } - return View(); + var formShape = await _displayManager.BuildEditorAsync(_updateModelAccessor.ModelUpdater, false, string.Empty, string.Empty); + + return View(formShape); } [HttpPost] [AllowAnonymous] - public async Task ForgotPassword(ForgotPasswordViewModel model) + [ActionName(nameof(ForgotPassword))] + public async Task ForgotPasswordPOST() { if (!(await _siteService.GetSiteSettingsAsync()).As().AllowResetPassword) { return NotFound(); } + var model = new ForgotPasswordForm(); + + var formShape = await _displayManager.UpdateEditorAsync(model, _updateModelAccessor.ModelUpdater, false, string.Empty, string.Empty); + await _passwordRecoveryFormEvents.InvokeAsync((e, modelState) => e.RecoveringPasswordAsync((key, message) => modelState.AddModelError(key, message)), ModelState, _logger); - if (TryValidateModel(model) && ModelState.IsValid) + if (ModelState.IsValid) { - var user = await _userService.GetForgotPasswordUserAsync(model.Email) as User; - if (user == null || ( - (await _siteService.GetSiteSettingsAsync()).As().UsersMustValidateEmail - && !await _userManager.IsEmailConfirmedAsync(user)) - ) + var user = await _userService.GetForgotPasswordUserAsync(model.Identifier) as User; + if (user == null || await MustValidateEmailAsync(user)) { // returns to confirmation page anyway: we don't want to let scrapers know if a username or an email exist - return RedirectToLocal(Url.Action("ForgotPasswordConfirmation", "ResetPassword")); + return RedirectToAction(nameof(ForgotPasswordConfirmation)); } user.ResetToken = Convert.ToBase64String(Encoding.UTF8.GetBytes(user.ResetToken)); - var resetPasswordUrl = Url.Action("ResetPassword", "ResetPassword", new { code = user.ResetToken }, HttpContext.Request.Scheme); + var resetPasswordUrl = Url.Action(nameof(ResetPassword), _controllerName, new { code = user.ResetToken }, HttpContext.Request.Scheme); + // send email with callback link - await this.SendEmailAsync(user.Email, S["Reset your password"], new LostPasswordViewModel() { User = user, LostPasswordUrl = resetPasswordUrl }); + await this.SendEmailAsync(user.Email, S["Reset your password"], new LostPasswordViewModel() + { + User = user, + LostPasswordUrl = resetPasswordUrl + }); var context = new PasswordRecoveryContext(user); await _passwordRecoveryFormEvents.InvokeAsync((handler, context) => handler.PasswordRecoveredAsync(context), context, _logger); - return RedirectToLocal(Url.Action("ForgotPasswordConfirmation", "ResetPassword")); + return RedirectToAction(nameof(ForgotPasswordConfirmation)); } // If we got this far, something failed, redisplay form - return View(model); + return View(formShape); } - [HttpGet] [AllowAnonymous] public IActionResult ForgotPasswordConfirmation() { return View(); } - [HttpGet] [AllowAnonymous] public async Task ResetPassword(string code = null) { @@ -132,28 +154,31 @@ public async Task ResetPassword(ResetPasswordViewModel model) { if (await _userService.ResetPasswordAsync(model.Email, Encoding.UTF8.GetString(Convert.FromBase64String(model.ResetToken)), model.NewPassword, (key, message) => ModelState.AddModelError(key == "Password" ? nameof(ResetPasswordViewModel.NewPassword) : key, message))) { - return RedirectToLocal(Url.Action("ResetPasswordConfirmation", "ResetPassword")); + return RedirectToAction(nameof(ForgotPasswordConfirmation)); } } return View(model); } - [HttpGet] [AllowAnonymous] public IActionResult ResetPasswordConfirmation() { return View(); } - private RedirectResult RedirectToLocal(string returnUrl) + private async Task MustValidateEmailAsync(User user) { - if (Url.IsLocalUrl(returnUrl)) + var registrationFeatureIsAvailable = (await _shellFeaturesManager.GetAvailableFeaturesAsync()) + .Any(feature => feature.Id == "OrchardCore.Users.Registration"); + + if (!registrationFeatureIsAvailable) { - return Redirect(returnUrl); + return false; } - return Redirect("~/"); + return (await _siteService.GetSiteSettingsAsync()).As().UsersMustValidateEmail + && !await _userManager.IsEmailConfirmedAsync(user); } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ForgotPasswordFormDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ForgotPasswordFormDisplayDriver.cs new file mode 100644 index 00000000000..f239c73355c --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/ForgotPasswordFormDisplayDriver.cs @@ -0,0 +1,30 @@ +using System.Threading.Tasks; +using OrchardCore.DisplayManagement.Handlers; +using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Views; +using OrchardCore.Users.Models; +using OrchardCore.Users.ViewModels; + +namespace OrchardCore.Users.Drivers; + +public class ForgotPasswordFormDisplayDriver : DisplayDriver +{ + public override IDisplayResult Edit(ForgotPasswordForm model) + { + return Initialize("ForgotPasswordFormIdentifier_Edit", vm => + { + vm.Identifier = model.Identifier; + }).Location("Content"); + } + + public override async Task UpdateAsync(ForgotPasswordForm model, IUpdateModel updater) + { + var viewModel = new ForgotPasswordViewModel(); + + await updater.TryUpdateModelAsync(viewModel, Prefix); + + model.Identifier = viewModel.Identifier; + + return Edit(model); + } +} diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/LoginFormDisplayDriver.cs b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/LoginFormDisplayDriver.cs index b7997dc2588..6bec75e7e83 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Drivers/LoginFormDisplayDriver.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Drivers/LoginFormDisplayDriver.cs @@ -2,7 +2,6 @@ using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.ModelBinding; using OrchardCore.DisplayManagement.Views; -using OrchardCore.Entities; using OrchardCore.Users.Models; using OrchardCore.Users.ViewModels; @@ -29,8 +28,6 @@ public override async Task UpdateAsync(LoginForm model, IUpdateM model.Password = viewModel.Password; model.RememberMe = viewModel.RememberMe; - model.Put(viewModel); - return Edit(model); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs index b878ff8587d..6bb24e747dd 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/Startup.cs @@ -75,6 +75,7 @@ public override void Configure(IApplicationBuilder builder, IEndpointRouteBuilde pattern: _userOptions.LoginPath, defaults: new { controller = _accountControllerName, action = nameof(AccountController.Login) } ); + routes.MapAreaControllerRoute( name: "ChangePassword", areaName: UserConstants.Features.Users, @@ -440,7 +441,9 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped, ResetPasswordSettingsDisplayDriver>(); + services.AddScoped, ForgotPasswordLoginFormDisplayDriver>(); + services.AddScoped, ForgotPasswordFormDisplayDriver>(); } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/ForgotPasswordViewModel.cs b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/ForgotPasswordViewModel.cs index eca1cc288ee..dd19302c0a8 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/ForgotPasswordViewModel.cs +++ b/src/OrchardCore.Modules/OrchardCore.Users/ViewModels/ForgotPasswordViewModel.cs @@ -1,11 +1,15 @@ +using System; using System.ComponentModel.DataAnnotations; namespace OrchardCore.Users.ViewModels { public class ForgotPasswordViewModel { - [Required(ErrorMessage = "Email is required.")] + [Obsolete("Email property is not longer used and will be removed in future releases. Instead use Identifier.")] [Email.EmailAddress(ErrorMessage = "Invalid Email.")] public string Email { get; set; } + + [Required(ErrorMessage = "Username or Email is required.")] + public string Identifier { get; set; } } } diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ForgotPasswordForm.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ForgotPasswordForm.Edit.cshtml new file mode 100644 index 00000000000..feea36c860f --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ForgotPasswordForm.Edit.cshtml @@ -0,0 +1,21 @@ + +
+
+

@T["Forgot password?"]

+
+ +
+ +
+ + @if (Model.Content != null) + { + @await DisplayAsync(Model.Content) + } + +
+ +
+
+
+
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ForgotPasswordFormIdentifier.Edit.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ForgotPasswordFormIdentifier.Edit.cshtml new file mode 100644 index 00000000000..7e22422d977 --- /dev/null +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ForgotPasswordFormIdentifier.Edit.cshtml @@ -0,0 +1,6 @@ +@model ForgotPasswordViewModel + +
+ + +
diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ResetPassword/ForgotPassword.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ResetPassword/ForgotPassword.cshtml index e43fd8891bb..642dde2c57f 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/ResetPassword/ForgotPassword.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ResetPassword/ForgotPassword.cshtml @@ -1,28 +1,5 @@ -@model OrchardCore.Users.ViewModels.ForgotPasswordViewModel - @{ ViewLayout = "Layout__Login"; } -

@T["Forgot password?"]

-

@T["Please check your email to reset your password."]

-
-
-
-
-
-
- -
- -
-
- @await RenderSectionAsync("AfterForgotPassword", required: false) -
-
- -
-
-
-
-
+@await DisplayAsync(Model) diff --git a/src/OrchardCore.Modules/OrchardCore.Users/Views/ResetPassword/ForgotPasswordConfirmation.cshtml b/src/OrchardCore.Modules/OrchardCore.Users/Views/ResetPassword/ForgotPasswordConfirmation.cshtml index 5baf462afa4..015f9f8acb7 100644 --- a/src/OrchardCore.Modules/OrchardCore.Users/Views/ResetPassword/ForgotPasswordConfirmation.cshtml +++ b/src/OrchardCore.Modules/OrchardCore.Users/Views/ResetPassword/ForgotPasswordConfirmation.cshtml @@ -2,5 +2,5 @@ ViewLayout = "Layout__Login"; } -

@T["Forgot Password confirmation"]

+

@T["Forgot Password Confirmation"]

@T["Please check your email to reset your password."]

diff --git a/src/OrchardCore/OrchardCore.Users.Core/Models/ForgotPasswordForm.cs b/src/OrchardCore/OrchardCore.Users.Core/Models/ForgotPasswordForm.cs new file mode 100644 index 00000000000..cfe629664bb --- /dev/null +++ b/src/OrchardCore/OrchardCore.Users.Core/Models/ForgotPasswordForm.cs @@ -0,0 +1,8 @@ +using OrchardCore.Entities; + +namespace OrchardCore.Users.Models; + +public class ForgotPasswordForm : Entity +{ + public string Identifier { get; set; } +} diff --git a/src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs b/src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs index 99329d57086..9905d02354b 100644 --- a/src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs +++ b/src/OrchardCore/OrchardCore.Users.Core/Services/UserService.cs @@ -20,12 +20,13 @@ public class UserService : IUserService { private readonly SignInManager _signInManager; private readonly UserManager _userManager; - private readonly IOptions _identityOptions; + private readonly IdentityOptions _identityOptions; private readonly IEnumerable _passwordRecoveryFormEvents; - protected readonly IStringLocalizer S; private readonly ISiteService _siteService; private readonly ILogger _logger; + protected readonly IStringLocalizer S; + public UserService( SignInManager signInManager, UserManager userManager, @@ -37,7 +38,7 @@ public UserService( { _signInManager = signInManager; _userManager = userManager; - _identityOptions = identityOptions; + _identityOptions = identityOptions.Value; _passwordRecoveryFormEvents = passwordRecoveryFormEvents; S = stringLocalizer; _siteService = siteService; @@ -168,14 +169,17 @@ public async Task GetForgotPasswordUserAsync(string userIdentifier) return await Task.FromResult(null); } - var user = await _userManager.FindByEmailAsync(userIdentifier) as User; + var user = await GetUserAsync(userIdentifier); if (user == null) { return await Task.FromResult(null); } - user.ResetToken = await _userManager.GeneratePasswordResetTokenAsync(user); + if (user is User u) + { + u.ResetToken = await _userManager.GeneratePasswordResetTokenAsync(user); + } return user; } @@ -240,8 +244,17 @@ public Task CreatePrincipalAsync(IUser user) return _signInManager.CreateUserPrincipalAsync(user); } - public async Task GetUserAsync(string userName) => - (await _userManager.FindByNameAsync(userName)) ?? await _userManager.FindByEmailAsync(userName); + public async Task GetUserAsync(string userName) + { + var user = await _userManager.FindByNameAsync(userName); + + if (user is null && _identityOptions.User.RequireUniqueEmail) + { + user = await _userManager.FindByEmailAsync(userName); + } + + return user; + } public Task GetUserByUniqueIdAsync(string userIdentifier) => _userManager.FindByIdAsync(userIdentifier); @@ -265,10 +278,10 @@ public void ProcessValidationErrors(IEnumerable errors, User user reportError("Password", S["Passwords must have at least one non letter or digit character."]); break; case "PasswordTooShort": - reportError("Password", S["Passwords must be at least {0} characters.", _identityOptions.Value.Password.RequiredLength]); + reportError("Password", S["Passwords must be at least {0} characters.", _identityOptions.Password.RequiredLength]); break; case "PasswordRequiresUniqueChars": - reportError("Password", S["Passwords must contain at least {0} unique characters.", _identityOptions.Value.Password.RequiredUniqueChars]); + reportError("Password", S["Passwords must contain at least {0} unique characters.", _identityOptions.Password.RequiredUniqueChars]); break; // CurrentPassword. diff --git a/src/docs/releases/1.9.0.md b/src/docs/releases/1.9.0.md index 2ee46f8e1aa..d125efe569f 100644 --- a/src/docs/releases/1.9.0.md +++ b/src/docs/releases/1.9.0.md @@ -105,6 +105,34 @@ public class ForgotPasswordLoginFormDisplayDriver : DisplayDriver } ``` +The `ForgotPassword.cshtml` has undergone a significant revamp. The previous `AfterForgotPassword` zone, which allowed filters for injecting shapes, has been replaced. Now, you can inject shapes using drivers by implementing `IDisplayDriver`. For example, the 'Forgot Password?' link is injected using the following driver: + +```csharp +public class ForgotPasswordFormDisplayDriver : DisplayDriver +{ + public override IDisplayResult Edit(ForgotPasswordForm model) + { + return Initialize("ForgotPasswordFormIdentifier_Edit", vm => + { + vm.Identifier = model.Identifier; + }).Location("Content"); + } + + public override async Task UpdateAsync(ForgotPasswordForm model, IUpdateModel updater) + { + var viewModel = new ForgotPasswordViewModel(); + + await updater.TryUpdateModelAsync(viewModel, Prefix); + + model.Identifier = viewModel.Identifier; + + return Edit(model); + } +} +``` + +Previously, users were only able to reset their password through email when the "Reset Password" feature was enabled. However, we've enhanced this functionality to offer users the flexibility of resetting their password using either their email or username. Consequently, the `Email` property on the `ForgotPasswordViewModel` has been deprecated and should be replaced with the new Identifier property for password resets. + ### Media Indexing Previously, `.pdf` files were automatically indexed in the search providers (Elasticsearch, Lucene or Azure AI Search). Now, if you want to continue to index `.PDF` file you'll need to enable the `OrchardCore.Media.Indexing.Pdf` feature.