Skip to content
Draft
Show file tree
Hide file tree
Changes from 2 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
65 changes: 65 additions & 0 deletions Dnn.CommunityForums/Controllers/ArchivedUrlController.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) by DNN Community
//
// DNN Community licenses this file to you under the MIT license.
//
// See the LICENSE file in the project root for more information.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

namespace DotNetNuke.Modules.ActiveForums.Controllers
{
using System.Linq;
using System.Security.Cryptography;
using System.Text;

internal class ArchivedUrlController : RepositoryControllerBase<DotNetNuke.Modules.ActiveForums.Entities.ArchivedURLInfo>
{
internal DotNetNuke.Modules.ActiveForums.Entities.ArchivedURLInfo FindByURL(int portalId, string url)
{
if (string.IsNullOrWhiteSpace(url))
{
return null;
}

var normalizedUrl = NormalizeUrl(url);
Comment thread
johnhenley marked this conversation as resolved.
Outdated
var normalizedUrlWithoutLeadingSlash = normalizedUrl.TrimStart('/');
Comment thread
johnhenley marked this conversation as resolved.
Outdated
var normalizedUrlHash = ComputeUrlHash(normalizedUrl);
var normalizedUrlWithoutLeadingSlashHash = ComputeUrlHash(normalizedUrlWithoutLeadingSlash);
Comment thread
johnhenley marked this conversation as resolved.
Outdated

return this.Find("WHERE PortalId = @0 AND (URL_Hash = @1 OR URL_Hash = @2)", portalId, normalizedUrlHash, normalizedUrlWithoutLeadingSlashHash)
.FirstOrDefault(a => IsUrlMatch(a?.Url, normalizedUrl, normalizedUrlWithoutLeadingSlash));
}

internal static byte[] ComputeUrlHash(string normalizedUrl)
{
using (var md5 = MD5.Create())
{
return md5.ComputeHash(Encoding.Unicode.GetBytes(normalizedUrl));
}
}

internal static bool IsUrlMatch(string archivedUrl, string normalizedUrl, string normalizedUrlWithoutLeadingSlash)
{
var normalizedArchivedUrl = NormalizeUrl(archivedUrl);
return normalizedArchivedUrl == normalizedUrl || normalizedArchivedUrl == normalizedUrlWithoutLeadingSlash;
}

internal static string NormalizeUrl(string url)
{
return string.IsNullOrWhiteSpace(url) ? string.Empty : url.Trim().ToLowerInvariant();
}
}
}

5 changes: 5 additions & 0 deletions Dnn.CommunityForums/DnnCommunityForums.dnn
Original file line number Diff line number Diff line change
Expand Up @@ -484,6 +484,11 @@
<name>09.07.00.SqlDataProvider</name>
<version>09.07.00</version>
</script>
<script type="Install">
<path>sql</path>
<name>09.08.00.1877.SqlDataProvider</name>
<version>09.08.00</version>
</script>
<script type="UnInstall">
<path>sql</path>
<name>Uninstall.SqlDataProvider</name>
Expand Down
69 changes: 69 additions & 0 deletions Dnn.CommunityForums/Entities/ArchivedURLInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// Copyright (c) by DNN Community
//
// DNN Community licenses this file to you under the MIT license.
//
// See the LICENSE file in the project root for more information.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

namespace DotNetNuke.Modules.ActiveForums.Entities
{
using DotNetNuke.ComponentModel.DataAnnotations;

/// <summary>
/// Represents an archived forum URL entry.
/// </summary>
[TableName("activeforums_ArchivedURLs")]
[PrimaryKey("Id", AutoIncrement = true)]
public class ArchivedURLInfo
{
/// <summary>
/// Gets or sets the archived URL identifier.
/// </summary>
public int Id { get; set; }

/// <summary>
/// Gets or sets the archived URL value.
/// </summary>
[ColumnName("URL")]
public string Url { get; set; }

/// <summary>
/// Gets or sets the portal identifier.
/// </summary>
public int PortalId { get; set; }

/// <summary>
/// Gets or sets the forum identifier.
/// </summary>
public int ForumId { get; set; }

/// <summary>
/// Gets or sets the topic identifier.
/// </summary>
public int TopicId { get; set; }

/// <summary>
/// Gets or sets the forum group identifier.
/// </summary>
public int ForumGroupId { get; set; }

/// <summary>
/// Gets or sets the MD5 hash for normalized URL lookups.
/// </summary>
[ColumnName("URL_Hash")]
public byte[] UrlHash { get; set; }
}
}
109 changes: 109 additions & 0 deletions Dnn.CommunityForums/sql/09.08.00.1877.SqlDataProvider
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
SET NOCOUNT ON
GO

/* issue 1877 - begin - modernize archived URLs */

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}activeforums_URL]') AND type in (N'U'))
AND NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}activeforums_ArchivedURLs]') AND type in (N'U'))
BEGIN
EXEC sp_rename '{databaseOwner}{objectQualifier}activeforums_URL', 'activeforums_ArchivedURLs'
END
GO

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}activeforums_ArchivedURLs]') AND type in (N'U'))
AND NOT EXISTS (SELECT * FROM sys.columns WHERE [name] = N'URL_Hash' AND [object_id] = OBJECT_ID(N'{databaseOwner}[{objectQualifier}activeforums_ArchivedURLs]'))
BEGIN
ALTER TABLE {databaseOwner}[{objectQualifier}activeforums_ArchivedURLs]
ADD URL_Hash AS CONVERT(binary(16), HASHBYTES('MD5', LOWER(LTRIM(RTRIM([URL]))))) PERSISTED
Comment thread
johnhenley marked this conversation as resolved.
Outdated
END
GO

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}activeforums_ArchivedURLs]') AND type in (N'U'))
AND NOT EXISTS (SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}activeforums_ArchivedURLs]') AND name = N'IX_{objectQualifier}activeforums_ArchivedURLs_URL_Hash')
BEGIN
CREATE NONCLUSTERED INDEX IX_{objectQualifier}activeforums_ArchivedURLs_URL_Hash
ON {databaseOwner}[{objectQualifier}activeforums_ArchivedURLs] (URL_Hash)
END
GO

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}activeforums_URL_Archive]') AND type in (N'P', N'PC'))
DROP PROCEDURE {databaseOwner}[{objectQualifier}activeforums_URL_Archive]
GO

CREATE PROCEDURE {databaseOwner}[{objectQualifier}activeforums_URL_Archive]
@PortalId int,
@ForumGroupId int,
@ForumId int,
@TopicId int,
@URL nvarchar(1000)
AS
DECLARE @NormalizedUrl nvarchar(1000)
DECLARE @UrlHash binary(16)

SET @NormalizedUrl = LOWER(LTRIM(RTRIM(@URL)))
SET @UrlHash = CONVERT(binary(16), HASHBYTES('MD5', @NormalizedUrl))

IF NOT EXISTS
(
SELECT 1
FROM {databaseOwner}[{objectQualifier}activeforums_ArchivedURLs]
WHERE PortalID = @PortalId
AND URL_Hash = @UrlHash
AND LTRIM(RTRIM(LOWER([URL]))) = @NormalizedUrl
)
BEGIN
INSERT INTO {databaseOwner}[{objectQualifier}activeforums_ArchivedURLs] (PortalId, ForumGroupId, ForumId, TopicId, URL)
VALUES (@PortalId, @ForumGroupId, @ForumId, @TopicId, @URL)
END
GO

IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}activeforums_FindByURL]') AND type in (N'P', N'PC'))
DROP PROCEDURE {databaseOwner}[{objectQualifier}activeforums_FindByURL]
GO

CREATE PROCEDURE {databaseOwner}[{objectQualifier}activeforums_FindByURL]
@PortalId int,
@URL nvarchar(1000)
AS
DECLARE @NormalizedUrl nvarchar(1000)
DECLARE @NormalizedUrlNoSlash nvarchar(1000)
DECLARE @UrlHash binary(16)
DECLARE @UrlHashNoSlash binary(16)

SET @NormalizedUrl = LOWER(LTRIM(RTRIM(@URL)))
SET @NormalizedUrlNoSlash = CASE WHEN LEFT(@NormalizedUrl, 1) = '/' THEN STUFF(@NormalizedUrl, 1, 1, '') ELSE @NormalizedUrl END
SET @UrlHash = CONVERT(binary(16), HASHBYTES('MD5', @NormalizedUrl))
SET @UrlHashNoSlash = CONVERT(binary(16), HASHBYTES('MD5', @NormalizedUrlNoSlash))

SELECT ForumId, TopicId
FROM {databaseOwner}[{objectQualifier}activeforums_ArchivedURLs]
WHERE PortalId = @PortalId
AND (URL_Hash = @UrlHash OR URL_Hash = @UrlHashNoSlash)
AND
(
LTRIM(RTRIM(LOWER([URL]))) = @NormalizedUrl
OR '/' + LTRIM(RTRIM(LOWER([URL]))) = @NormalizedUrl
OR LTRIM(RTRIM(LOWER([URL]))) = @NormalizedUrlNoSlash
OR '/' + LTRIM(RTRIM(LOWER([URL]))) = @NormalizedUrlNoSlash
)
GO

DECLARE @urlSearchDefinition nvarchar(max)
Comment thread
johnhenley marked this conversation as resolved.
Outdated
SELECT @urlSearchDefinition = [definition]
FROM sys.sql_modules
WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}activeforums_URL_Search]')

IF @urlSearchDefinition IS NOT NULL
AND @urlSearchDefinition LIKE '%activeforums_URL%'
BEGIN
SET @urlSearchDefinition = REPLACE(@urlSearchDefinition, 'CREATE PROCEDURE', 'ALTER PROCEDURE')
SET @urlSearchDefinition = REPLACE(@urlSearchDefinition, 'create procedure', 'alter procedure')
SET @urlSearchDefinition = REPLACE(@urlSearchDefinition, '{objectQualifier}activeforums_URL', '{objectQualifier}activeforums_ArchivedURLs')
EXEC sp_executesql @urlSearchDefinition
END
GO

/* issue 1877 - end - modernize archived URLs */

/* --------------------- */

9 changes: 8 additions & 1 deletion Dnn.CommunityForums/sql/Uninstall.SqlDataProvider
Original file line number Diff line number Diff line change
Expand Up @@ -683,6 +683,10 @@ IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'{databas
ALTER TABLE {databaseOwner}[{objectQualifier}activeforums_URL]
DROP CONSTRAINT [FK_{objectQualifier}activeforums_URL_Topics]
GO
IF EXISTS (SELECT * FROM sys.foreign_keys WHERE object_id = OBJECT_ID(N'{databaseOwner}[FK_{objectQualifier}activeforums_URL_Topics]') AND parent_object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}activeforums_ArchivedURLs]'))
ALTER TABLE {databaseOwner}[{objectQualifier}activeforums_ArchivedURLs]
DROP CONSTRAINT [FK_{objectQualifier}activeforums_URL_Topics]
GO

IF EXISTS (SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID(N'{databaseOwner}{objectQualifier}activeforums_Content') AND name = N'idx_{objectQualifier}activeforums_Content_ModuleId')
DROP INDEX [IX_{objectQualifier}activeforums_Content_ModuleId] ON {databaseOwner}{objectQualifier}activeforums_Content
Expand Down Expand Up @@ -834,6 +838,9 @@ GO
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}activeforums_URL]') AND type in (N'U'))
DROP TABLE {databaseOwner}[{objectQualifier}activeforums_URL]
GO
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}activeforums_ArchivedURLs]') AND type in (N'U'))
DROP TABLE {databaseOwner}[{objectQualifier}activeforums_ArchivedURLs]
GO
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'{databaseOwner}[{objectQualifier}activeforums_SettingsTable]') AND type in (N'FN', N'IF', N'TF', N'FS', N'FT'))
DROP FUNCTION {databaseOwner}[{objectQualifier}activeforums_SettingsTable]
GO
Expand Down Expand Up @@ -979,4 +986,4 @@ IF EXISTS (Select * From {databaseOwner}{objectQualifier}Schedule WHERE TypeFull
DELETE FROM {databaseOwner}{objectQualifier}Schedule WHERE TypeFullName = 'DotNetNuke.Modules.ActiveForums.Services.Badges.BadgeAwardQueue, DotNetNuke.Modules.ActiveForums'
GO

/* end 09.02.00 */
/* end 09.02.00 */
72 changes: 72 additions & 0 deletions Dnn.CommunityForumsTests/Controllers/ArchivedUrlControllerTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
// Copyright (c) by DNN Community
//
// DNN Community licenses this file to you under the MIT license.
//
// See the LICENSE file in the project root for more information.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
// documentation files (the "Software"), to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and
// to permit persons to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions
// of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED
// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF
// CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

namespace DotNetNuke.Modules.ActiveForumsTests.Controllers
{
using NUnit.Framework;

[TestFixture]
public class ArchivedUrlControllerTests
{
[Test]
public void NormalizeUrl_TrimsAndLowerCases()
{
// Arrange
const string input = " /Forums/Topic-One/ ";

// Act
var result = DotNetNuke.Modules.ActiveForums.Controllers.ArchivedUrlController.NormalizeUrl(input);

// Assert
Assert.That(result, Is.EqualTo("/forums/topic-one/"));
}

[Test]
public void IsUrlMatch_ReturnsTrue_WhenArchivedUrlIsEquivalentWithoutLeadingSlash()
{
// Arrange
const string archivedUrl = "forums/topic-one/";
const string normalizedUrl = "/forums/topic-one/";
const string normalizedUrlWithoutLeadingSlash = "forums/topic-one/";

// Act
var result = DotNetNuke.Modules.ActiveForums.Controllers.ArchivedUrlController.IsUrlMatch(archivedUrl, normalizedUrl, normalizedUrlWithoutLeadingSlash);

// Assert
Assert.That(result, Is.True);
}

[Test]
public void ComputeUrlHash_ReturnsSameHashForCaseVariantsAfterNormalization()
{
// Arrange
var first = DotNetNuke.Modules.ActiveForums.Controllers.ArchivedUrlController.NormalizeUrl("/FORUMS/Topic-One/");
var second = DotNetNuke.Modules.ActiveForums.Controllers.ArchivedUrlController.NormalizeUrl("/forums/topic-one/");

// Act
var firstHash = DotNetNuke.Modules.ActiveForums.Controllers.ArchivedUrlController.ComputeUrlHash(first);
var secondHash = DotNetNuke.Modules.ActiveForums.Controllers.ArchivedUrlController.ComputeUrlHash(second);

// Assert
Assert.That(firstHash, Is.EqualTo(secondHash));
}
}
}