Merge branch 'main' of https://github.com/umbraco/Umbraco-CMS
This commit is contained in:
@@ -40,7 +40,7 @@ public class TextStringValueConverter : PropertyValueConverterBase, IDeliveryApi
|
|||||||
var sourceString = source.ToString();
|
var sourceString = source.ToString();
|
||||||
|
|
||||||
// ensures string is parsed for {localLink} and URLs are resolved correctly
|
// ensures string is parsed for {localLink} and URLs are resolved correctly
|
||||||
sourceString = _linkParser.EnsureInternalLinks(sourceString!, preview);
|
sourceString = _linkParser.EnsureInternalLinks(sourceString!);
|
||||||
sourceString = _urlParser.EnsureUrls(sourceString);
|
sourceString = _urlParser.EnsureUrls(sourceString);
|
||||||
|
|
||||||
return sourceString;
|
return sourceString;
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||||
using Umbraco.Cms.Core.Routing;
|
using Umbraco.Cms.Core.Routing;
|
||||||
|
|
||||||
namespace Umbraco.Cms.Core.Templates;
|
namespace Umbraco.Cms.Core.Templates;
|
||||||
@@ -45,17 +46,18 @@ public sealed class HtmlLocalLinkParser
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses the string looking for the {localLink} syntax and updates them to their correct links.
|
/// Parses the string looking for the {localLink} syntax and updates them to their correct links.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="text"></param>
|
[Obsolete("This method overload is no longer used in Umbraco and delegates to the overload without the preview parameter. Scheduled for removal in Umbraco 18.")]
|
||||||
/// <param name="preview"></param>
|
|
||||||
/// <returns></returns>
|
|
||||||
public string EnsureInternalLinks(string text, bool preview) => EnsureInternalLinks(text);
|
public string EnsureInternalLinks(string text, bool preview) => EnsureInternalLinks(text);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Parses the string looking for the {localLink} syntax and updates them to their correct links.
|
/// Parses the string looking for the {localLink} syntax and updates them to their correct links.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="text"></param>
|
public string EnsureInternalLinks(string text) => EnsureInternalLinks(text, UrlMode.Default);
|
||||||
/// <returns></returns>
|
|
||||||
public string EnsureInternalLinks(string text)
|
/// <summary>
|
||||||
|
/// Parses the string looking for the {localLink} syntax and updates them to their correct links.
|
||||||
|
/// </summary>
|
||||||
|
public string EnsureInternalLinks(string text, UrlMode urlMode)
|
||||||
{
|
{
|
||||||
foreach (LocalLinkTag tagData in FindLocalLinkIds(text))
|
foreach (LocalLinkTag tagData in FindLocalLinkIds(text))
|
||||||
{
|
{
|
||||||
@@ -63,8 +65,8 @@ public sealed class HtmlLocalLinkParser
|
|||||||
{
|
{
|
||||||
var newLink = tagData.Udi?.EntityType switch
|
var newLink = tagData.Udi?.EntityType switch
|
||||||
{
|
{
|
||||||
Constants.UdiEntityType.Document => _publishedUrlProvider.GetUrl(tagData.Udi.Guid),
|
Constants.UdiEntityType.Document => _publishedUrlProvider.GetUrl(tagData.Udi.Guid, urlMode),
|
||||||
Constants.UdiEntityType.Media => _publishedUrlProvider.GetMediaUrl(tagData.Udi.Guid),
|
Constants.UdiEntityType.Media => _publishedUrlProvider.GetMediaUrl(tagData.Udi.Guid, urlMode),
|
||||||
_ => string.Empty,
|
_ => string.Empty,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,7 +75,7 @@ public sealed class HtmlLocalLinkParser
|
|||||||
}
|
}
|
||||||
else if (tagData.IntId.HasValue)
|
else if (tagData.IntId.HasValue)
|
||||||
{
|
{
|
||||||
var newLink = _publishedUrlProvider.GetUrl(tagData.IntId.Value);
|
var newLink = _publishedUrlProvider.GetUrl(tagData.IntId.Value, urlMode);
|
||||||
text = text.Replace(tagData.TagHref, newLink);
|
text = text.Replace(tagData.TagHref, newLink);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -671,6 +671,15 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0
|
|||||||
|
|
||||||
protected override void PersistDeletedItem(IUser entity)
|
protected override void PersistDeletedItem(IUser entity)
|
||||||
{
|
{
|
||||||
|
// Clear user group caches for any user groups associated with the deleted user.
|
||||||
|
// We need to do this because the count of the number of users in the user group is cached
|
||||||
|
// along with the user group, and if we've made changes to the user groups assigned to the user,
|
||||||
|
// the count for the groups need to be refreshed.
|
||||||
|
foreach (IReadOnlyUserGroup group in entity.Groups)
|
||||||
|
{
|
||||||
|
ClearRepositoryCacheForUserGroup(group.Id);
|
||||||
|
}
|
||||||
|
|
||||||
IEnumerable<string> deletes = GetDeleteClauses();
|
IEnumerable<string> deletes = GetDeleteClauses();
|
||||||
foreach (var delete in deletes)
|
foreach (var delete in deletes)
|
||||||
{
|
{
|
||||||
@@ -713,8 +722,8 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0
|
|||||||
if (entity.IsPropertyDirty("Groups"))
|
if (entity.IsPropertyDirty("Groups"))
|
||||||
{
|
{
|
||||||
// lookup all assigned
|
// lookup all assigned
|
||||||
List<UserGroupDto>? assigned = entity.Groups == null || entity.Groups.Any() == false
|
List<UserGroupDto>? assigned = entity.Groups.Any() is false
|
||||||
? new List<UserGroupDto>()
|
? []
|
||||||
: Database.Fetch<UserGroupDto>(
|
: Database.Fetch<UserGroupDto>(
|
||||||
"SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)",
|
"SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)",
|
||||||
new { aliases = entity.Groups.Select(x => x.Alias) });
|
new { aliases = entity.Groups.Select(x => x.Alias) });
|
||||||
@@ -724,6 +733,15 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0
|
|||||||
var dto = new User2UserGroupDto { UserGroupId = groupDto.Id, UserId = entity.Id };
|
var dto = new User2UserGroupDto { UserGroupId = groupDto.Id, UserId = entity.Id };
|
||||||
Database.Insert(dto);
|
Database.Insert(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clear user group caches for the user groups associated with the new user.
|
||||||
|
// We need to do this because the count of the number of users in the user group is cached
|
||||||
|
// along with the user group, and if we've made changes to the user groups assigned to the user,
|
||||||
|
// the count for the groups need to be refreshed.
|
||||||
|
foreach (IReadOnlyUserGroup group in entity.Groups)
|
||||||
|
{
|
||||||
|
ClearRepositoryCacheForUserGroup(group.Id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.ResetDirtyProperties();
|
entity.ResetDirtyProperties();
|
||||||
@@ -836,27 +854,66 @@ SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0
|
|||||||
|
|
||||||
if (entity.IsPropertyDirty("Groups"))
|
if (entity.IsPropertyDirty("Groups"))
|
||||||
{
|
{
|
||||||
//lookup all assigned
|
// Get all user groups Ids currently assigned to the user.
|
||||||
List<UserGroupDto>? assigned = entity.Groups == null || entity.Groups.Any() == false
|
var existingUserGroupIds = Database.Fetch<User2UserGroupDto>(
|
||||||
? new List<UserGroupDto>()
|
"WHERE UserId = @UserId",
|
||||||
: Database.Fetch<UserGroupDto>(
|
new { UserId = entity.Id })
|
||||||
"SELECT * FROM umbracoUserGroup WHERE userGroupAlias IN (@aliases)",
|
.Select(x => x.UserGroupId)
|
||||||
new { aliases = entity.Groups.Select(x => x.Alias) });
|
.ToList();
|
||||||
|
|
||||||
//first delete all
|
// Get the user groups Ids that need to be removed and added.
|
||||||
// TODO: We could do this a nicer way instead of "Nuke and Pave"
|
var userGroupsIdsToRemove = existingUserGroupIds
|
||||||
Database.Delete<User2UserGroupDto>("WHERE UserId = @UserId", new { UserId = entity.Id });
|
.Except(entity.Groups.Select(x => x.Id))
|
||||||
|
.ToList();
|
||||||
|
var userGroupIdsToAdd = entity.Groups
|
||||||
|
.Select(x => x.Id)
|
||||||
|
.Except(existingUserGroupIds)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
foreach (UserGroupDto? groupDto in assigned)
|
// Remove user groups that are no longer assigned to the user.
|
||||||
|
if (userGroupsIdsToRemove.Count > 0)
|
||||||
{
|
{
|
||||||
var dto = new User2UserGroupDto { UserGroupId = groupDto.Id, UserId = entity.Id };
|
Database.Delete<User2UserGroupDto>(
|
||||||
Database.Insert(dto);
|
"WHERE UserId = @UserId AND UserGroupId IN (@userGroupIds)",
|
||||||
|
new { UserId = entity.Id, userGroupIds = userGroupsIdsToRemove });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add user groups that are newly assigned to the user.
|
||||||
|
if (userGroupIdsToAdd.Count > 0)
|
||||||
|
{
|
||||||
|
IEnumerable<User2UserGroupDto> user2UserGroupDtos = userGroupIdsToAdd
|
||||||
|
.Select(userGroupId => new User2UserGroupDto
|
||||||
|
{
|
||||||
|
UserGroupId = userGroupId,
|
||||||
|
UserId = entity.Id,
|
||||||
|
});
|
||||||
|
Database.InsertBulk(user2UserGroupDtos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear user group caches for any user group that have been removed or added.
|
||||||
|
// We need to do this because the count of the number of users in the user group is cached
|
||||||
|
// along with the user group, and if we've made changes to the user groups assigned to the user,
|
||||||
|
// the count for the groups need to be refreshed.
|
||||||
|
var userGroupIdsToRefresh = userGroupsIdsToRemove
|
||||||
|
.Union(userGroupIdsToAdd)
|
||||||
|
.ToList();
|
||||||
|
foreach (int userGroupIdToRefresh in userGroupIdsToRefresh)
|
||||||
|
{
|
||||||
|
ClearRepositoryCacheForUserGroup(userGroupIdToRefresh);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entity.ResetDirtyProperties();
|
entity.ResetDirtyProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ClearRepositoryCacheForUserGroup(int id)
|
||||||
|
{
|
||||||
|
IAppPolicyCache userGroupCache = AppCaches.IsolatedCaches.GetOrCreate<IUserGroup>();
|
||||||
|
|
||||||
|
string cacheKey = RepositoryCacheKeys.GetKey<IUserGroup, int>(id);
|
||||||
|
userGroupCache.Clear(cacheKey);
|
||||||
|
}
|
||||||
|
|
||||||
private void AddingOrUpdateStartNodes(IEntity entity, IEnumerable<UserStartNodeDto> current,
|
private void AddingOrUpdateStartNodes(IEntity entity, IEnumerable<UserStartNodeDto> current,
|
||||||
UserStartNodeDto.StartNodeTypeValue startNodeType, int[]? entityStartIds)
|
UserStartNodeDto.StartNodeTypeValue startNodeType, int[]? entityStartIds)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ public class MarkdownEditorValueConverter : PropertyValueConverterBase, IDeliver
|
|||||||
var sourceString = source.ToString()!;
|
var sourceString = source.ToString()!;
|
||||||
|
|
||||||
// ensures string is parsed for {localLink} and URLs are resolved correctly
|
// ensures string is parsed for {localLink} and URLs are resolved correctly
|
||||||
sourceString = _localLinkParser.EnsureInternalLinks(sourceString, preview);
|
sourceString = _localLinkParser.EnsureInternalLinks(sourceString);
|
||||||
sourceString = _urlParser.EnsureUrls(sourceString);
|
sourceString = _urlParser.EnsureUrls(sourceString);
|
||||||
|
|
||||||
return sourceString;
|
return sourceString;
|
||||||
|
|||||||
@@ -135,7 +135,7 @@ public class RteBlockRenderingValueConverter : SimpleRichTextValueConverter, IDe
|
|||||||
var sourceString = intermediateValue.Markup;
|
var sourceString = intermediateValue.Markup;
|
||||||
|
|
||||||
// ensures string is parsed for {localLink} and URLs and media are resolved correctly
|
// ensures string is parsed for {localLink} and URLs and media are resolved correctly
|
||||||
sourceString = _linkParser.EnsureInternalLinks(sourceString, preview);
|
sourceString = _linkParser.EnsureInternalLinks(sourceString);
|
||||||
sourceString = _urlParser.EnsureUrls(sourceString);
|
sourceString = _urlParser.EnsureUrls(sourceString);
|
||||||
sourceString = _imageSourceParser.EnsureImageSources(sourceString);
|
sourceString = _imageSourceParser.EnsureImageSources(sourceString);
|
||||||
|
|
||||||
|
|||||||
@@ -266,6 +266,15 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
|
|||||||
max-width: var(--umb-rte-max-width, 100%);
|
max-width: var(--umb-rte-max-width, 100%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:host(:hover) {
|
||||||
|
--umb-tiptap-edge-border-color: var(--uui-color-border-standalone);
|
||||||
|
}
|
||||||
|
|
||||||
|
:host(:focus),
|
||||||
|
:host(:focus-within) {
|
||||||
|
--umb-tiptap-edge-border-color: var(--uui-color-border-emphasis);
|
||||||
|
}
|
||||||
|
|
||||||
:host([readonly]) {
|
:host([readonly]) {
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
|
|
||||||
@@ -335,6 +344,12 @@ export class UmbInputTiptapElement extends UmbFormControlMixin<string, typeof Um
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#editor,
|
||||||
|
umb-tiptap-toolbar,
|
||||||
|
umb-tiptap-statusbar {
|
||||||
|
transition: border-color 120ms ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
umb-tiptap-toolbar + #editor {
|
umb-tiptap-toolbar + #editor {
|
||||||
border-top: 0;
|
border-top: 0;
|
||||||
border-top-left-radius: 0;
|
border-top-left-radius: 0;
|
||||||
|
|||||||
@@ -398,4 +398,35 @@ internal sealed partial class UserServiceCrudTests
|
|||||||
Assert.IsNotNull(updatedUser.StartMediaIds);
|
Assert.IsNotNull(updatedUser.StartMediaIds);
|
||||||
Assert.IsEmpty(updatedUser.StartMediaIds);
|
Assert.IsEmpty(updatedUser.StartMediaIds);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[TestCase(false, false)]
|
||||||
|
[TestCase(true, true)]
|
||||||
|
public async Task Cannot_Remove_Admin_Group_From_Only_Admin_User(bool createAdditionalAdminUser, bool expectSuccess)
|
||||||
|
{
|
||||||
|
var userService = CreateUserService(securitySettings: new SecuritySettings { UsernameIsEmail = false });
|
||||||
|
|
||||||
|
if (createAdditionalAdminUser)
|
||||||
|
{
|
||||||
|
var (updateModel, _) = await CreateUserForUpdate(userService);
|
||||||
|
updateModel.UserGroupKeys = new HashSet<Guid> { Constants.Security.AdminGroupKey };
|
||||||
|
var updateResult = await userService.UpdateAsync(Constants.Security.SuperUserKey, updateModel);
|
||||||
|
Assert.IsTrue(updateResult.Success);
|
||||||
|
}
|
||||||
|
|
||||||
|
var adminUser = await userService.GetAsync(Constants.Security.SuperUserKey);
|
||||||
|
var adminUserUpdateModel = await MapUserToUpdateModel(adminUser);
|
||||||
|
adminUserUpdateModel.Email = "admin@test.com";
|
||||||
|
adminUserUpdateModel.UserGroupKeys = new HashSet<Guid> { Constants.Security.EditorGroupKey };
|
||||||
|
var adminUserUpdateResult = await userService.UpdateAsync(Constants.Security.SuperUserKey, adminUserUpdateModel);
|
||||||
|
|
||||||
|
if (expectSuccess)
|
||||||
|
{
|
||||||
|
Assert.IsTrue(adminUserUpdateResult.Success);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Assert.IsFalse(adminUserUpdateResult.Success);
|
||||||
|
Assert.AreEqual(UserOperationStatus.AdminUserGroupMustNotBeEmpty, adminUserUpdateResult.Status);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1019,6 +1019,42 @@ internal sealed class UserServiceTests : UmbracoIntegrationTest
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public async Task Can_Assign_And_Get_Groups_For_User()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var (user, userGroup1) = await CreateTestUserAndGroup();
|
||||||
|
var userGroup2 = await CreateTestUserGroup("testGroup2", "Test Group 2");
|
||||||
|
|
||||||
|
// Act & Assert
|
||||||
|
user = UserService.GetByUsername(user.Username);
|
||||||
|
|
||||||
|
Assert.IsNotNull(user);
|
||||||
|
Assert.AreEqual(1, user.Groups.Count());
|
||||||
|
Assert.AreEqual(userGroup1.Alias, user.Groups.First().Alias);
|
||||||
|
|
||||||
|
// - add second group
|
||||||
|
user.AddGroup(userGroup2);
|
||||||
|
UserService.Save(user);
|
||||||
|
user = UserService.GetByUsername(user.Username);
|
||||||
|
Assert.AreEqual(2, user.Groups.Count());
|
||||||
|
|
||||||
|
// - remove first group
|
||||||
|
user.RemoveGroup(userGroup1.Alias);
|
||||||
|
UserService.Save(user);
|
||||||
|
user = UserService.GetByUsername(user.Username);
|
||||||
|
Assert.AreEqual(1, user.Groups.Count());
|
||||||
|
Assert.AreEqual(userGroup2.Alias, user.Groups.First().Alias);
|
||||||
|
|
||||||
|
// - remove second group and add first
|
||||||
|
user.RemoveGroup(userGroup2.Alias);
|
||||||
|
user.AddGroup(userGroup1.ToReadOnlyGroup());
|
||||||
|
UserService.Save(user);
|
||||||
|
user = UserService.GetByUsername(user.Username);
|
||||||
|
Assert.AreEqual(1, user.Groups.Count());
|
||||||
|
Assert.AreEqual(userGroup1.Alias, user.Groups.First().Alias);
|
||||||
|
}
|
||||||
|
|
||||||
[TestCase(UserKind.Default, UserClientCredentialsOperationStatus.InvalidUser)]
|
[TestCase(UserKind.Default, UserClientCredentialsOperationStatus.InvalidUser)]
|
||||||
[TestCase(UserKind.Api, UserClientCredentialsOperationStatus.Success)]
|
[TestCase(UserKind.Api, UserClientCredentialsOperationStatus.Success)]
|
||||||
public async Task Can_Assign_ClientId_To_Api_User(UserKind userKind, UserClientCredentialsOperationStatus expectedResult)
|
public async Task Can_Assign_ClientId_To_Api_User(UserKind userKind, UserClientCredentialsOperationStatus expectedResult)
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ using Umbraco.Cms.Core.Models.PublishedContent;
|
|||||||
using Umbraco.Cms.Core.Routing;
|
using Umbraco.Cms.Core.Routing;
|
||||||
using Umbraco.Cms.Core.Services.Navigation;
|
using Umbraco.Cms.Core.Services.Navigation;
|
||||||
using Umbraco.Cms.Core.Templates;
|
using Umbraco.Cms.Core.Templates;
|
||||||
|
using Umbraco.Cms.Core.Web;
|
||||||
using Umbraco.Cms.Tests.Common;
|
using Umbraco.Cms.Tests.Common;
|
||||||
using Umbraco.Cms.Tests.UnitTests.TestHelpers.Objects;
|
using Umbraco.Cms.Tests.UnitTests.TestHelpers.Objects;
|
||||||
|
|
||||||
@@ -216,18 +217,204 @@ public class HtmlLocalLinkParserTests
|
|||||||
|
|
||||||
var umbracoContextAccessor = new TestUmbracoContextAccessor();
|
var umbracoContextAccessor = new TestUmbracoContextAccessor();
|
||||||
|
|
||||||
|
var umbracoContextFactory = TestUmbracoContextFactory.Create(
|
||||||
|
umbracoContextAccessor: umbracoContextAccessor);
|
||||||
|
|
||||||
|
using (var reference = umbracoContextFactory.EnsureUmbracoContext())
|
||||||
|
{
|
||||||
|
var contentCache = Mock.Get(reference.UmbracoContext.Content);
|
||||||
|
contentCache.Setup(x => x.GetById(It.IsAny<int>())).Returns(publishedContent.Object);
|
||||||
|
contentCache.Setup(x => x.GetById(It.IsAny<Guid>())).Returns(publishedContent.Object);
|
||||||
|
|
||||||
|
var mediaCache = Mock.Get(reference.UmbracoContext.Media);
|
||||||
|
mediaCache.Setup(x => x.GetById(It.IsAny<int>())).Returns(media.Object);
|
||||||
|
mediaCache.Setup(x => x.GetById(It.IsAny<Guid>())).Returns(media.Object);
|
||||||
|
|
||||||
|
var publishedUrlProvider = CreatePublishedUrlProvider(
|
||||||
|
contentUrlProvider,
|
||||||
|
mediaUrlProvider,
|
||||||
|
umbracoContextAccessor);
|
||||||
|
|
||||||
|
var linkParser = new HtmlLocalLinkParser(publishedUrlProvider);
|
||||||
|
|
||||||
|
var output = linkParser.EnsureInternalLinks(input);
|
||||||
|
|
||||||
|
Assert.AreEqual(result, output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Test]
|
||||||
|
public void ParseLocalLinks_WithUrlMode_RespectsUrlMode()
|
||||||
|
{
|
||||||
|
// Arrange
|
||||||
|
var input = "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world";
|
||||||
|
|
||||||
|
// Setup content URL provider that returns different URLs based on UrlMode
|
||||||
|
var contentUrlProvider = new Mock<IUrlProvider>();
|
||||||
|
contentUrlProvider
|
||||||
|
.Setup(x => x.GetUrl(
|
||||||
|
It.IsAny<IPublishedContent>(),
|
||||||
|
UrlMode.Relative,
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<Uri>()))
|
||||||
|
.Returns(UrlInfo.Url("/relative-url"));
|
||||||
|
contentUrlProvider
|
||||||
|
.Setup(x => x.GetUrl(
|
||||||
|
It.IsAny<IPublishedContent>(),
|
||||||
|
UrlMode.Absolute,
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<Uri>()))
|
||||||
|
.Returns(UrlInfo.Url("http://example.com/absolute-url"));
|
||||||
|
|
||||||
|
var contentType = new PublishedContentType(
|
||||||
|
Guid.NewGuid(),
|
||||||
|
666,
|
||||||
|
"alias",
|
||||||
|
PublishedItemType.Content,
|
||||||
|
Enumerable.Empty<string>(),
|
||||||
|
Enumerable.Empty<PublishedPropertyType>(),
|
||||||
|
ContentVariation.Nothing);
|
||||||
|
var publishedContent = new Mock<IPublishedContent>();
|
||||||
|
publishedContent.Setup(x => x.Id).Returns(1234);
|
||||||
|
publishedContent.Setup(x => x.ContentType).Returns(contentType);
|
||||||
|
|
||||||
|
var umbracoContextAccessor = new TestUmbracoContextAccessor();
|
||||||
var umbracoContextFactory = TestUmbracoContextFactory.Create(
|
var umbracoContextFactory = TestUmbracoContextFactory.Create(
|
||||||
umbracoContextAccessor: umbracoContextAccessor);
|
umbracoContextAccessor: umbracoContextAccessor);
|
||||||
|
|
||||||
var webRoutingSettings = new WebRoutingSettings();
|
var webRoutingSettings = new WebRoutingSettings();
|
||||||
|
|
||||||
var navigationQueryService = new Mock<IDocumentNavigationQueryService>();
|
var publishedUrlProvider = CreatePublishedUrlProvider(
|
||||||
// Guid? parentKey = null;
|
contentUrlProvider,
|
||||||
// navigationQueryService.Setup(x => x.TryGetParentKey(It.IsAny<Guid>(), out parentKey)).Returns(true);
|
new Mock<IMediaUrlProvider>(),
|
||||||
IEnumerable<Guid> ancestorKeys = [];
|
umbracoContextAccessor);
|
||||||
navigationQueryService.Setup(x => x.TryGetAncestorsKeys(It.IsAny<Guid>(), out ancestorKeys)).Returns(true);
|
|
||||||
|
|
||||||
var publishedContentStatusFilteringService = new Mock<IPublishedContentStatusFilteringService>();
|
using (var reference = umbracoContextFactory.EnsureUmbracoContext())
|
||||||
|
{
|
||||||
|
var contentCache = Mock.Get(reference.UmbracoContext.Content);
|
||||||
|
contentCache.Setup(x => x.GetById(It.IsAny<Guid>())).Returns(publishedContent.Object);
|
||||||
|
|
||||||
|
var linkParser = new HtmlLocalLinkParser(publishedUrlProvider);
|
||||||
|
|
||||||
|
// Act
|
||||||
|
var relativeOutput = linkParser.EnsureInternalLinks(input, UrlMode.Relative);
|
||||||
|
var absoluteOutput = linkParser.EnsureInternalLinks(input, UrlMode.Absolute);
|
||||||
|
|
||||||
|
// Assert
|
||||||
|
Assert.AreEqual("hello href=\"/relative-url\" world", relativeOutput);
|
||||||
|
Assert.AreEqual("hello href=\"http://example.com/absolute-url\" world", absoluteOutput);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[TestCase(UrlMode.Default, "hello href=\"{localLink:1234}\" world ", "hello href=\"/relative-url\" world ")]
|
||||||
|
[TestCase(UrlMode.Relative, "hello href=\"{localLink:1234}\" world ", "hello href=\"/relative-url\" world ")]
|
||||||
|
[TestCase(UrlMode.Absolute, "hello href=\"{localLink:1234}\" world ", "hello href=\"https://example.com/absolute-url\" world ")]
|
||||||
|
[TestCase(UrlMode.Auto, "hello href=\"{localLink:1234}\" world ", "hello href=\"/relative-url\" world ")]
|
||||||
|
[TestCase(UrlMode.Default, "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/relative-url\" world ")]
|
||||||
|
[TestCase(UrlMode.Relative, "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/relative-url\" world ")]
|
||||||
|
[TestCase(UrlMode.Absolute, "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"https://example.com/absolute-url\" world ")]
|
||||||
|
[TestCase(UrlMode.Auto, "hello href=\"{localLink:umb://document/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/relative-url\" world ")]
|
||||||
|
[TestCase(UrlMode.Default, "hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/media/relative/image.jpg\" world ")]
|
||||||
|
[TestCase(UrlMode.Relative, "hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/media/relative/image.jpg\" world ")]
|
||||||
|
[TestCase(UrlMode.Absolute, "hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"https://example.com/media/absolute/image.jpg\" world ")]
|
||||||
|
[TestCase(UrlMode.Auto, "hello href=\"{localLink:umb://media/9931BDE0AAC34BABB838909A7B47570E}\" world ", "hello href=\"/media/relative/image.jpg\" world ")]
|
||||||
|
public void ParseLocalLinks_WithVariousUrlModes_ReturnsCorrectUrls(UrlMode urlMode, string input, string expectedResult)
|
||||||
|
{
|
||||||
|
// Setup content URL provider that returns different URLs based on UrlMode
|
||||||
|
var contentUrlProvider = new Mock<IUrlProvider>();
|
||||||
|
contentUrlProvider
|
||||||
|
.Setup(x => x.GetUrl(
|
||||||
|
It.IsAny<IPublishedContent>(),
|
||||||
|
UrlMode.Default,
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<Uri>()))
|
||||||
|
.Returns(UrlInfo.Url("/relative-url"));
|
||||||
|
contentUrlProvider
|
||||||
|
.Setup(x => x.GetUrl(
|
||||||
|
It.IsAny<IPublishedContent>(),
|
||||||
|
UrlMode.Relative,
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<Uri>()))
|
||||||
|
.Returns(UrlInfo.Url("/relative-url"));
|
||||||
|
contentUrlProvider
|
||||||
|
.Setup(x => x.GetUrl(
|
||||||
|
It.IsAny<IPublishedContent>(),
|
||||||
|
UrlMode.Absolute,
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<Uri>()))
|
||||||
|
.Returns(UrlInfo.Url("https://example.com/absolute-url"));
|
||||||
|
contentUrlProvider
|
||||||
|
.Setup(x => x.GetUrl(
|
||||||
|
It.IsAny<IPublishedContent>(),
|
||||||
|
UrlMode.Auto,
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<Uri>()))
|
||||||
|
.Returns(UrlInfo.Url("/relative-url"));
|
||||||
|
|
||||||
|
var contentType = new PublishedContentType(
|
||||||
|
Guid.NewGuid(),
|
||||||
|
666,
|
||||||
|
"alias",
|
||||||
|
PublishedItemType.Content,
|
||||||
|
Enumerable.Empty<string>(),
|
||||||
|
Enumerable.Empty<PublishedPropertyType>(),
|
||||||
|
ContentVariation.Nothing);
|
||||||
|
var publishedContent = new Mock<IPublishedContent>();
|
||||||
|
publishedContent.Setup(x => x.Id).Returns(1234);
|
||||||
|
publishedContent.Setup(x => x.ContentType).Returns(contentType);
|
||||||
|
|
||||||
|
// Setup media URL provider that returns different URLs based on UrlMode
|
||||||
|
var mediaUrlProvider = new Mock<IMediaUrlProvider>();
|
||||||
|
mediaUrlProvider.Setup(x => x.GetMediaUrl(
|
||||||
|
It.IsAny<IPublishedContent>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
UrlMode.Default,
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<Uri>()))
|
||||||
|
.Returns(UrlInfo.Url("/media/relative/image.jpg"));
|
||||||
|
mediaUrlProvider.Setup(x => x.GetMediaUrl(
|
||||||
|
It.IsAny<IPublishedContent>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
UrlMode.Relative,
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<Uri>()))
|
||||||
|
.Returns(UrlInfo.Url("/media/relative/image.jpg"));
|
||||||
|
mediaUrlProvider.Setup(x => x.GetMediaUrl(
|
||||||
|
It.IsAny<IPublishedContent>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
UrlMode.Absolute,
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<Uri>()))
|
||||||
|
.Returns(UrlInfo.Url("https://example.com/media/absolute/image.jpg"));
|
||||||
|
mediaUrlProvider.Setup(x => x.GetMediaUrl(
|
||||||
|
It.IsAny<IPublishedContent>(),
|
||||||
|
It.IsAny<string>(),
|
||||||
|
UrlMode.Auto,
|
||||||
|
It.IsAny<string>(),
|
||||||
|
It.IsAny<Uri>()))
|
||||||
|
.Returns(UrlInfo.Url("/media/relative/image.jpg"));
|
||||||
|
|
||||||
|
var mediaType = new PublishedContentType(
|
||||||
|
Guid.NewGuid(),
|
||||||
|
777,
|
||||||
|
"image",
|
||||||
|
PublishedItemType.Media,
|
||||||
|
Enumerable.Empty<string>(),
|
||||||
|
Enumerable.Empty<PublishedPropertyType>(),
|
||||||
|
ContentVariation.Nothing);
|
||||||
|
var media = new Mock<IPublishedContent>();
|
||||||
|
media.Setup(x => x.ContentType).Returns(mediaType);
|
||||||
|
|
||||||
|
var umbracoContextAccessor = new TestUmbracoContextAccessor();
|
||||||
|
var umbracoContextFactory = TestUmbracoContextFactory.Create(
|
||||||
|
umbracoContextAccessor: umbracoContextAccessor);
|
||||||
|
|
||||||
|
var webRoutingSettings = new WebRoutingSettings();
|
||||||
|
|
||||||
|
var publishedUrlProvider = CreatePublishedUrlProvider(
|
||||||
|
contentUrlProvider,
|
||||||
|
mediaUrlProvider,
|
||||||
|
umbracoContextAccessor);
|
||||||
|
|
||||||
using (var reference = umbracoContextFactory.EnsureUmbracoContext())
|
using (var reference = umbracoContextFactory.EnsureUmbracoContext())
|
||||||
{
|
{
|
||||||
@@ -239,25 +426,35 @@ public class HtmlLocalLinkParserTests
|
|||||||
mediaCache.Setup(x => x.GetById(It.IsAny<int>())).Returns(media.Object);
|
mediaCache.Setup(x => x.GetById(It.IsAny<int>())).Returns(media.Object);
|
||||||
mediaCache.Setup(x => x.GetById(It.IsAny<Guid>())).Returns(media.Object);
|
mediaCache.Setup(x => x.GetById(It.IsAny<Guid>())).Returns(media.Object);
|
||||||
|
|
||||||
var publishStatusQueryService = new Mock<IPublishStatusQueryService>();
|
|
||||||
publishStatusQueryService
|
|
||||||
.Setup(x => x.IsDocumentPublished(It.IsAny<Guid>(), It.IsAny<string>()))
|
|
||||||
.Returns(true);
|
|
||||||
|
|
||||||
var publishedUrlProvider = new UrlProvider(
|
|
||||||
umbracoContextAccessor,
|
|
||||||
Options.Create(webRoutingSettings),
|
|
||||||
new UrlProviderCollection(() => new[] { contentUrlProvider.Object }),
|
|
||||||
new MediaUrlProviderCollection(() => new[] { mediaUrlProvider.Object }),
|
|
||||||
Mock.Of<IVariationContextAccessor>(),
|
|
||||||
navigationQueryService.Object,
|
|
||||||
publishedContentStatusFilteringService.Object);
|
|
||||||
|
|
||||||
var linkParser = new HtmlLocalLinkParser(publishedUrlProvider);
|
var linkParser = new HtmlLocalLinkParser(publishedUrlProvider);
|
||||||
|
|
||||||
var output = linkParser.EnsureInternalLinks(input);
|
var output = linkParser.EnsureInternalLinks(input, urlMode);
|
||||||
|
|
||||||
Assert.AreEqual(result, output);
|
Assert.AreEqual(expectedResult, output);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static UrlProvider CreatePublishedUrlProvider(
|
||||||
|
Mock<IUrlProvider> contentUrlProvider,
|
||||||
|
Mock<IMediaUrlProvider> mediaUrlProvider,
|
||||||
|
TestUmbracoContextAccessor umbracoContextAccessor)
|
||||||
|
{
|
||||||
|
var navigationQueryService = new Mock<IDocumentNavigationQueryService>();
|
||||||
|
IEnumerable<Guid> ancestorKeys = [];
|
||||||
|
navigationQueryService.Setup(x => x.TryGetAncestorsKeys(It.IsAny<Guid>(), out ancestorKeys)).Returns(true);
|
||||||
|
|
||||||
|
var publishStatusQueryService = new Mock<IPublishStatusQueryService>();
|
||||||
|
publishStatusQueryService
|
||||||
|
.Setup(x => x.IsDocumentPublished(It.IsAny<Guid>(), It.IsAny<string>()))
|
||||||
|
.Returns(true);
|
||||||
|
|
||||||
|
return new UrlProvider(
|
||||||
|
umbracoContextAccessor,
|
||||||
|
Options.Create(new WebRoutingSettings()),
|
||||||
|
new UrlProviderCollection(() => new[] { contentUrlProvider.Object }),
|
||||||
|
new MediaUrlProviderCollection(() => new[] { mediaUrlProvider.Object }),
|
||||||
|
Mock.Of<IVariationContextAccessor>(),
|
||||||
|
navigationQueryService.Object,
|
||||||
|
new Mock<IPublishedContentStatusFilteringService>().Object);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user