Browse Source

Merge pull request #16174 from abpframework/EngincanV/cms-comment

Cms: Allow/Disallow External URLs in Comments
pull/16189/head
Yunus Emre Kalkan 3 years ago
committed by GitHub
parent
commit
3f4fd31303
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      docs/en/Modules/Cms-Kit/Comments.md
  2. 11
      modules/cms-kit/host/Volo.CmsKit.Web.Unified/CmsKitWebUnifiedModule.cs
  3. 5
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Comments/CmsKitCommentOptions.cs
  4. 3
      modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json
  5. 2
      modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Comments/CreateCommentWithParametersInput.cs
  6. 60
      modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Comments/CommentPublicAppService.cs
  7. 2
      modules/cms-kit/src/Volo.CmsKit.Public.Web/CmsKitPublicWebAutoMapperProfile.cs
  8. 4
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicCommentsController.cs
  9. 2
      modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs
  10. 21
      modules/cms-kit/test/Volo.CmsKit.Application.Tests/CmsKitApplicationTestModule.cs
  11. 33
      modules/cms-kit/test/Volo.CmsKit.Application.Tests/Comments/CommentPublicAppService_Tests.cs

13
docs/en/Modules/Cms-Kit/Comments.md

@ -19,6 +19,16 @@ Configure<CmsKitCommentOptions>(options =>
{
options.EntityTypes.Add(new CommentEntityTypeDefinition("Product"));
options.IsRecaptchaEnabled = true; //false by default
options.AllowedExternalUrls = new Dictionary<string, List<string>>
{
{
"quote",
new List<string>
{
"https://abp.io/"
}
}
};
});
```
@ -28,6 +38,7 @@ Configure<CmsKitCommentOptions>(options =>
- `EntityTypes`: List of defined entity types(`CmsKitCommentOptions`) in the comment system.
- `IsRecaptchaEnabled`: This flag enables or disables the reCaptcha for the comment system. You can set it as **true** if you want to use reCaptcha in your comment system.
- `AllowedExternalUrls`: Indicates the allowed external URLs by entity types, which can be included in a comment. If it's specified for a certain entity type, then only the specified external URLs are allowed in the comments.
`CommentEntityTypeDefinition` properties:
@ -46,7 +57,7 @@ The comment system provides a commenting [widget](../../UI/AspNetCore/Widgets.md
})
```
`entityType` was explained in the previous section. `entityId` should be the unique id of the product, in this example. If you have a Product entity, you can use its Id here. `referralLinks` is an optional parameter. You can use this parameter to add values (such as "nofollow", "noreferrer", or any other values) to the [rel attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel) of links.
`entityType` was explained in the previous section. `entityId` should be the unique id of the product, in this example. If you have a Product entity, you can use its Id here. `referralLinks` is an optional parameter. You can use this parameter to add values (such as "nofollow", "noreferrer", or any other values) to the [rel attributes](https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes/rel) of links.
## User Interface

11
modules/cms-kit/host/Volo.CmsKit.Web.Unified/CmsKitWebUnifiedModule.cs

@ -1,3 +1,4 @@
using System.Collections.Generic;
using System.IO;
using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
@ -168,6 +169,16 @@ public class CmsKitWebUnifiedModule : AbpModule
{
options.EntityTypes.Add(new CommentEntityTypeDefinition("quote"));
options.IsRecaptchaEnabled = true;
options.AllowedExternalUrls = new Dictionary<string, List<string>>
{
{
"quote",
new List<string>
{
"https://abp.io/"
}
}
};
});
Configure<CmsKitMediaOptions>(options =>

5
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Comments/CmsKitCommentOptions.cs

@ -13,4 +13,9 @@ public class CmsKitCommentOptions
/// Default: false
/// </summary>
public bool IsRecaptchaEnabled { get; set; }
/// <summary>
/// Indicates the allowed external URLs by entity types, which can be included in a comment.
/// </summary>
public Dictionary<string, List<string>> AllowedExternalUrls { get; set; } = new();
}

3
modules/cms-kit/src/Volo.CmsKit.Domain.Shared/Volo/CmsKit/Localization/Resources/en.json

@ -216,6 +216,7 @@
"CaptchaCode": "Captcha code",
"CommentTextRequired": "Comment is required",
"CaptchaCodeErrorMessage" : "The answer you entered for the CAPTCHA was not correct. Please try again",
"CaptchaCodeMissingMessage": "The captcha code is missing!"
"CaptchaCodeMissingMessage": "The captcha code is missing!",
"UnAllowedExternalUrlMessage": "You included an unallowed external URL. Please try again without the external URL."
}
}

2
modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Comments/CreateCommentWithParameteresInput.cs → modules/cms-kit/src/Volo.CmsKit.Public.Application.Contracts/Volo/CmsKit/Public/Comments/CreateCommentWithParametersInput.cs

@ -6,7 +6,7 @@ using Volo.CmsKit.Comments;
namespace Volo.CmsKit.Public.Comments;
[Serializable]
public class CreateCommentWithParameteresInput
public class CreateCommentWithParametersInput
{
[Required]
[DynamicStringLength(typeof(CommentConsts), nameof(CommentConsts.MaxTextLength))]

60
modules/cms-kit/src/Volo.CmsKit.Public.Application/Volo/CmsKit/Public/Comments/CommentPublicAppService.cs

@ -1,9 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Security;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
using Volo.Abp;
using Volo.Abp.Application.Dtos;
using Volo.Abp.Authorization;
using Volo.Abp.Data;
@ -23,24 +25,27 @@ namespace Volo.CmsKit.Public.Comments;
[RequiresGlobalFeature(typeof(CommentsFeature))]
public class CommentPublicAppService : CmsKitPublicAppServiceBase, ICommentPublicAppService
{
protected string RegexMarkdownUrlPattern = @"\[[^\]]*\]\((?<url>.*?)\)(?![^\x60]*\x60)";
protected ICommentRepository CommentRepository { get; }
protected ICmsUserLookupService CmsUserLookupService { get; }
public IDistributedEventBus DistributedEventBus { get; }
protected CommentManager CommentManager { get; }
protected IAuthorizationService AuthorizationService { get; }
protected CmsKitCommentOptions CmsCommentOptions { get; }
public CommentPublicAppService(
ICommentRepository commentRepository,
ICmsUserLookupService cmsUserLookupService,
IDistributedEventBus distributedEventBus,
CommentManager commentManager,
IAuthorizationService authorizationService)
IOptionsSnapshot<CmsKitCommentOptions> cmsCommentOptions)
{
CommentRepository = commentRepository;
CmsUserLookupService = cmsUserLookupService;
DistributedEventBus = distributedEventBus;
CommentManager = commentManager;
AuthorizationService = authorizationService;
CmsCommentOptions = cmsCommentOptions.Value;
}
public virtual async Task<ListResultDto<CommentWithDetailsDto>> GetListAsync(string entityType, string entityId)
@ -56,6 +61,8 @@ public class CommentPublicAppService : CmsKitPublicAppServiceBase, ICommentPubli
[Authorize]
public virtual async Task<CommentDto> CreateAsync(string entityType, string entityId, CreateCommentInput input)
{
CheckExternalUrls(entityType, input.Text);
var user = await CmsUserLookupService.GetByIdAsync(CurrentUser.GetId());
if (input.RepliedCommentId.HasValue)
@ -87,11 +94,12 @@ public class CommentPublicAppService : CmsKitPublicAppServiceBase, ICommentPubli
public virtual async Task<CommentDto> UpdateAsync(Guid id, UpdateCommentInput input)
{
var comment = await CommentRepository.GetAsync(id);
if (comment.CreatorId != CurrentUser.GetId())
{
throw new AbpAuthorizationException();
}
CheckExternalUrls(comment.EntityType, input.Text);
comment.SetText(input.Text);
comment.SetConcurrencyStampIfNotNull(input.ConcurrencyStamp);
@ -148,4 +156,46 @@ public class CommentPublicAppService : CmsKitPublicAppServiceBase, ICommentPubli
{
return ObjectMapper.Map<CmsUser, CmsUserDto>(comments.Single(c => c.Comment.Id == commentId).Author);
}
private void CheckExternalUrls(string entityType, string text)
{
if (!CmsCommentOptions.AllowedExternalUrls.TryGetValue(entityType, out var allowedExternalUrls))
{
return;
}
var matches = Regex.Matches(text, RegexMarkdownUrlPattern,
RegexOptions.Compiled | RegexOptions.IgnoreCase);
foreach (Match match in matches)
{
if (!match.Success || match.Groups.Count < 2)
{
continue;
}
var url = NormalizeUrl(match.Groups[1].Value);
if (!IsExternalUrl(url))
{
continue;
}
if (!allowedExternalUrls.Any(allowedExternalUrl =>
url.Contains(NormalizeUrl(allowedExternalUrl), StringComparison.OrdinalIgnoreCase)))
{
throw new UserFriendlyException(L["UnAllowedExternalUrlMessage"]);
}
}
}
private static bool IsExternalUrl(string url)
{
return url.StartsWith("https", StringComparison.InvariantCultureIgnoreCase) ||
url.StartsWith("http", StringComparison.InvariantCultureIgnoreCase);
}
private static string NormalizeUrl(string url)
{
return url.Replace("www.", "").RemovePostFix("/");
}
}

2
modules/cms-kit/src/Volo.CmsKit.Public.Web/CmsKitPublicWebAutoMapperProfile.cs

@ -8,6 +8,6 @@ public class CmsKitPublicWebAutoMapperProfile : Profile
{
public CmsKitPublicWebAutoMapperProfile()
{
CreateMap<CreateCommentWithParameteresInput, CreateCommentInput>();
CreateMap<CreateCommentWithParametersInput, CreateCommentInput>();
}
}

4
modules/cms-kit/src/Volo.CmsKit.Public.Web/Controllers/CmsKitPublicCommentsController.cs

@ -29,14 +29,14 @@ public class CmsKitPublicCommentsController : AbpController
}
[HttpPost]
public async Task ValidateAsync([FromBody] CreateCommentWithParameteresInput input)
public async Task ValidateAsync([FromBody] CreateCommentWithParametersInput input)
{
if (CmsKitCommentOptions.IsRecaptchaEnabled && input.CaptchaToken.HasValue)
{
SimpleMathsCaptchaGenerator.Validate(input.CaptchaToken.Value, input.CaptchaAnswer);
}
var dto = ObjectMapper.Map<CreateCommentWithParameteresInput, CreateCommentInput> (input);
var dto = ObjectMapper.Map<CreateCommentWithParametersInput, CreateCommentInput> (input);
await CommentPublicAppService.CreateAsync(input.EntityType, input.EntityId, dto);
}
}

2
modules/cms-kit/src/Volo.CmsKit.Public.Web/Pages/CmsKit/Shared/Components/Commenting/CommentingViewComponent.cs

@ -121,7 +121,7 @@ public class CommentingViewComponent : AbpViewComponent
public string EntityType { get; set; }
public string EntityId { get; set; }
public IEnumerable<string> ReferralLinks { get; set; }
public string LoginUrl { get; set; }

21
modules/cms-kit/test/Volo.CmsKit.Application.Tests/CmsKitApplicationTestModule.cs

@ -1,4 +1,6 @@
using Volo.Abp.Modularity;
using System.Collections.Generic;
using Volo.Abp.Modularity;
using Volo.CmsKit.Comments;
namespace Volo.CmsKit;
@ -8,5 +10,20 @@ namespace Volo.CmsKit;
)]
public class CmsKitApplicationTestModule : AbpModule
{
public override void ConfigureServices(ServiceConfigurationContext context)
{
Configure<CmsKitCommentOptions>(options =>
{
options.AllowedExternalUrls = new Dictionary<string, List<string>>
{
{
"EntityName1",
new List<string>
{
"https://abp.io/"
}
}
};
});
}
}

33
modules/cms-kit/test/Volo.CmsKit.Application.Tests/Comments/CommentPublicAppService_Tests.cs

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using NSubstitute;
using Shouldly;
using Volo.Abp;
using Volo.Abp.Users;
using Volo.CmsKit.Public.Comments;
using Xunit;
@ -62,6 +63,23 @@ public class CommentPublicAppService_Tests : CmsKitApplicationTestBase
});
}
[Fact]
public async Task CreateAsync_ShouldThrowUserFriendlyException_If_Url_UnAllowed()
{
_currentUser.Id.Returns(_cmsKitTestData.User2Id);
await Should.ThrowAsync<UserFriendlyException>(async () =>
await _commentAppService.CreateAsync(
_cmsKitTestData.EntityType1,
_cmsKitTestData.EntityId1,
new CreateCommentInput
{
RepliedCommentId = null,
Text = "[ABP Community](https://community.abp.io/)", //not allowed URL
}
));
}
[Fact]
public async Task UpdateAsync()
{
@ -80,6 +98,21 @@ public class CommentPublicAppService_Tests : CmsKitApplicationTestBase
comment.Text.ShouldBe("I'm Updated");
});
}
[Fact]
public async Task UpdateAsync_ShouldThrowUserFriendlyException_If_Url_UnAllowed()
{
_currentUser.Id.Returns(_cmsKitTestData.User1Id);
await Should.ThrowAsync<UserFriendlyException>(async () =>
await _commentAppService.UpdateAsync(
_cmsKitTestData.CommentWithChildId,
new UpdateCommentInput
{
Text = "[ABP Community - Update](https://community.abp.io/)", //not allowed URL
}
));
}
[Fact]
public async Task DeleteAsync()

Loading…
Cancel
Save