Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -128,10 +128,13 @@ public void Assign(int contentId, int propertyTypeId, IEnumerable<ITag> tags, bo
var group = SqlSyntax.GetQuotedColumnName("group");

// insert tags
// - Note we are checking in the subquery for the existence of the tag, so we don't insert duplicates, using a case-insensitive comparison (the
// LOWER keyword is consistent across SQLite and SQLServer). This ensures consistent behavior across databases as by default, SQLServer will
// perform a case-insensitive comparison, while SQLite will not.
var sql1 = $@"INSERT INTO cmsTags (tag, {group}, languageId)
SELECT tagSet.tag, tagSet.{group}, tagSet.languageId
FROM {tagSetSql}
LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTags.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1))
LEFT OUTER JOIN cmsTags ON (LOWER(tagSet.tag) = LOWER(cmsTags.tag) AND LOWER(tagSet.{group}) = LOWER(cmsTags.{group}) AND COALESCE(tagSet.languageId, -1) = COALESCE(cmsTags.languageId, -1))
WHERE cmsTags.id IS NULL";

Database.Execute(sql1);
Expand All @@ -142,7 +145,7 @@ LEFT OUTER JOIN cmsTags ON (tagSet.tag = cmsTags.tag AND tagSet.{group} = cmsTag
FROM (
SELECT t.Id
FROM {tagSetSql}
INNER JOIN cmsTags as t ON (tagSet.tag = t.tag AND tagSet.{group} = t.{group} AND COALESCE(tagSet.languageId, -1) = COALESCE(t.languageId, -1))
INNER JOIN cmsTags as t ON (LOWER(tagSet.tag) = LOWER(t.tag) AND LOWER(tagSet.{group}) = LOWER(t.{group}) AND COALESCE(tagSet.languageId, -1) = COALESCE(t.languageId, -1))
) AS tagSet2
LEFT OUTER JOIN cmsTagRelationship r ON (tagSet2.id = r.tagId AND r.nodeId = {contentId} AND r.propertyTypeID = {propertyTypeId})
WHERE r.tagId IS NULL";
Expand Down Expand Up @@ -245,14 +248,18 @@ private class TagComparer : IEqualityComparer<ITag>
{
public bool Equals(ITag? x, ITag? y) =>
ReferenceEquals(x, y) // takes care of both being null
|| (x != null && y != null && x.Text == y.Text && x.Group == y.Group && x.LanguageId == y.LanguageId);
|| (x != null &&
y != null &&
string.Equals(x.Text, y.Text, StringComparison.OrdinalIgnoreCase) &&
string.Equals(x.Group, y.Group, StringComparison.OrdinalIgnoreCase) &&
x.LanguageId == y.LanguageId);

public int GetHashCode(ITag obj)
{
unchecked
{
var h = obj.Text.GetHashCode();
h = (h * 397) ^ obj.Group.GetHashCode();
var h = StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Text);
h = (h * 397) ^ StringComparer.OrdinalIgnoreCase.GetHashCode(obj.Group);
h = (h * 397) ^ (obj.LanguageId?.GetHashCode() ?? 0);
return h;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.

using System.Linq;
using Microsoft.Extensions.Logging;
using NUnit.Framework;
using Umbraco.Cms.Core.Cache;
Expand Down Expand Up @@ -1047,6 +1046,98 @@ public void Can_Get_Tagged_Entities_For_Tag()
}
}

[Test]
public void Can_Create_Tag_Relations_With_Mixed_Casing_For_Tag()
{
var provider = ScopeProvider;
using (var scope = ScopeProvider.CreateScope())
{
(IContentType contentType, IContent content1, IContent content2) = CreateContentForCreateTagTests();

var repository = CreateRepository(provider);

// Note two tags are applied, but they differ only in case for the tag.
Tag[] tags1 = { new() { Text = "tag1", Group = "test" }, new() { Text = "Tag1", Group = "test" } };
repository.Assign(
content1.Id,
contentType.PropertyTypes.First().Id,
tags1,
false);

// Note the casing is different from the tag in tags1, but both should be considered equivalent.
Tag[] tags2 = { new() { Text = "TAG1", Group = "test" } };
repository.Assign(
content2.Id,
contentType.PropertyTypes.First().Id,
tags2,
false);

// Only one tag should have been saved.
var tagCount = scope.Database.ExecuteScalar<int>(
"SELECT COUNT(*) FROM cmsTags WHERE [group] = 'test'");
Assert.AreEqual(1, tagCount);

// Both content items should be found as tagged by the tag, even though one was assigned with the tag differing in case.
Assert.AreEqual(2, repository.GetTaggedEntitiesByTag(TaggableObjectTypes.Content, "tag1").Count());
}
}

[Test]
public void Can_Create_Tag_Relations_With_Mixed_Casing_For_Group()
{
var provider = ScopeProvider;
using (var scope = ScopeProvider.CreateScope())
{
(IContentType contentType, IContent content1, IContent content2) = CreateContentForCreateTagTests();

var repository = CreateRepository(provider);

// Note two tags are applied, but they differ only in case for the group.
Tag[] tags1 = { new() { Text = "tag1", Group = "group1" }, new() { Text = "tag1", Group = "Group1" } };
repository.Assign(
content1.Id,
contentType.PropertyTypes.First().Id,
tags1,
false);

// Note the casing is different from the group in tags1, but both should be considered equivalent.
Tag[] tags2 = { new() { Text = "tag1", Group = "GROUP1" } };
repository.Assign(
content2.Id,
contentType.PropertyTypes.First().Id,
tags2,
false);

// Only one tag/group should have been saved.
var tagCount = scope.Database.ExecuteScalar<int>(
"SELECT COUNT(*) FROM cmsTags WHERE [tag] = 'tag1'");
Assert.AreEqual(1, tagCount);

var groupCount = scope.Database.ExecuteScalar<int>(
"SELECT COUNT(*) FROM cmsTags WHERE [group] = 'group1'");
Assert.AreEqual(1, groupCount);

// Both content items should be found as tagged by the tag, even though one was assigned with the group differing in case.
Assert.AreEqual(2, repository.GetTaggedEntitiesByTagGroup(TaggableObjectTypes.Content, "group1").Count());
}
}

private (IContentType ContentType, IContent Content1, IContent Content2) CreateContentForCreateTagTests()
{
var template = TemplateBuilder.CreateTextPageTemplate();
FileService.SaveTemplate(template);

var contentType = ContentTypeBuilder.CreateSimpleContentType("test", "Test", defaultTemplateId: template.Id);
ContentTypeRepository.Save(contentType);

var content1 = ContentBuilder.CreateSimpleContent(contentType);
var content2 = ContentBuilder.CreateSimpleContent(contentType);
DocumentRepository.Save(content1);
DocumentRepository.Save(content2);

return (contentType, content1, content2);
}

private TagRepository CreateRepository(IScopeProvider provider) =>
new((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger<TagRepository>());
}
Loading