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

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,6 @@ public static IUmbracoBuilder AddConfiguration(this IUmbracoBuilder builder)
builder.Services.AddSingleton<IValidateOptions<RequestHandlerSettings>, RequestHandlerSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<UnattendedSettings>, UnattendedSettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<SecuritySettings>, SecuritySettingsValidator>();
builder.Services.AddSingleton<IValidateOptions<SystemDateMigrationSettings>, SystemDateMigrationSettingsValidator>();

// Register configuration sections.
builder
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
using NPoco;
using Umbraco.Cms.Infrastructure.Scoping;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using NPoco;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Infrastructure.Scoping;
using Umbraco.Extensions;

namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_17_0_0;

Expand All @@ -11,6 +12,8 @@
/// </summary>
public class MigrateSystemDatesToUtc : UnscopedMigrationBase
{
private static readonly string[] UtcIdentifiers = ["Coordinated Universal Time", "UTC"];

private readonly IScopeProvider _scopeProvider;
private readonly TimeProvider _timeProvider;
private readonly IOptions<SystemDateMigrationSettings> _migrationSettings;
Expand Down Expand Up @@ -49,96 +52,153 @@
return;
}

// If the local server timezone is not set, we detect it.
var timeZoneName = _migrationSettings.Value.LocalServerTimeZone;
if (string.IsNullOrWhiteSpace(timeZoneName))
// Offsets and Windows name are lazy loaded as they are not always needed.
// This also allows using timezones that exist in the SQL Database but not on the local server.
(string TimeZoneName, Lazy<TimeSpan> TimeZoneOffset, Lazy<string> WindowsTimeZoneName) timeZone;

var configuredTimeZoneName = _migrationSettings.Value.LocalServerTimeZone;
if (configuredTimeZoneName.IsNullOrWhiteSpace() is false)
{
timeZoneName = _timeProvider.LocalTimeZone.Id;
_logger.LogInformation("Migrating system dates to UTC using the detected local server timezone: {TimeZoneName}.", timeZoneName);
timeZone = (
configuredTimeZoneName,
new Lazy<TimeSpan>(() => TimeZoneInfo.FindSystemTimeZoneById(configuredTimeZoneName).BaseUtcOffset),
new Lazy<string>(() => configuredTimeZoneName));

_logger.LogInformation(
"Migrating system dates to UTC using the configured timezone: {TimeZoneName}.",
timeZone.TimeZoneName);
}
else
{
_logger.LogInformation("Migrating system dates to UTC using the configured local server timezone: {TimeZoneName}.", timeZoneName);
// If the local server timezone is not configured, we detect it.
TimeZoneInfo timeZoneInfo = _timeProvider.LocalTimeZone;

timeZone = (
timeZoneInfo.Id,
new Lazy<TimeSpan>(() => timeZoneInfo.BaseUtcOffset),
new Lazy<string>(() => GetWindowsTimeZoneId(timeZoneInfo)));

_logger.LogInformation(
"Migrating system dates to UTC using the detected local server timezone: {TimeZoneName}.",
timeZone.TimeZoneName);
}

// If the local server timezone is UTC, skip the migration.
if (string.Equals(timeZoneName, "Coordinated Universal Time", StringComparison.OrdinalIgnoreCase))
if (UtcIdentifiers.Contains(timeZone.TimeZoneName, StringComparer.OrdinalIgnoreCase))
{
_logger.LogInformation("Skipping migration {MigrationName} as the local server timezone is UTC.", nameof(MigrateSystemDatesToUtc));
_logger.LogInformation(
"Skipping migration {MigrationName} as the local server timezone is UTC.",
nameof(MigrateSystemDatesToUtc));
Context.Complete();
return;
}

TimeSpan timeZoneOffset = GetTimezoneOffset(timeZoneName);

using IScope scope = _scopeProvider.CreateScope();
using IDisposable notificationSuppression = scope.Notifications.Suppress();

MigrateDateColumn(scope, "cmsMember", "emailConfirmedDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "cmsMember", "lastLoginDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "cmsMember", "lastLockoutDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "cmsMember", "lastPasswordChangeDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoAccess", "createDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoAccess", "updateDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoAccessRule", "createDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoAccessRule", "updateDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoCreatedPackageSchema", "updateDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoContentVersion", "versionDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoContentVersionCleanupPolicy", "updated", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoContentVersionCultureVariation", "date", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoExternalLogin", "createDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoExternalLoginToken", "createDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoKeyValue", "updated", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoLog", "Datestamp", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoNode", "createDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoRelation", "datetime", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoServer", "registeredDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoServer", "lastNotifiedDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoUser", "createDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoUser", "updateDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoUser", "emailConfirmedDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoUser", "lastLockoutDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoUser", "lastPasswordChangeDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoUser", "lastLoginDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoUser", "invitedDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoUserGroup", "createDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "umbracoUserGroup", "updateDate", timeZoneName, timeZoneOffset);
MigrateDateColumn(scope, "cmsMember", "emailConfirmedDate", timeZone);
MigrateDateColumn(scope, "cmsMember", "lastLoginDate", timeZone);
MigrateDateColumn(scope, "cmsMember", "lastLockoutDate", timeZone);
MigrateDateColumn(scope, "cmsMember", "lastPasswordChangeDate", timeZone);
MigrateDateColumn(scope, "umbracoAccess", "createDate", timeZone);
MigrateDateColumn(scope, "umbracoAccess", "updateDate", timeZone);
MigrateDateColumn(scope, "umbracoAccessRule", "createDate", timeZone);
MigrateDateColumn(scope, "umbracoAccessRule", "updateDate", timeZone);
MigrateDateColumn(scope, "umbracoCreatedPackageSchema", "updateDate", timeZone);
MigrateDateColumn(scope, "umbracoContentVersion", "versionDate", timeZone);
MigrateDateColumn(scope, "umbracoContentVersionCleanupPolicy", "updated", timeZone);
MigrateDateColumn(scope, "umbracoContentVersionCultureVariation", "date", timeZone);
MigrateDateColumn(scope, "umbracoExternalLogin", "createDate", timeZone);
MigrateDateColumn(scope, "umbracoExternalLoginToken", "createDate", timeZone);
MigrateDateColumn(scope, "umbracoKeyValue", "updated", timeZone);
MigrateDateColumn(scope, "umbracoLog", "Datestamp", timeZone);
MigrateDateColumn(scope, "umbracoNode", "createDate", timeZone);
MigrateDateColumn(scope, "umbracoRelation", "datetime", timeZone);
MigrateDateColumn(scope, "umbracoServer", "registeredDate", timeZone);
MigrateDateColumn(scope, "umbracoServer", "lastNotifiedDate", timeZone);
MigrateDateColumn(scope, "umbracoUser", "createDate", timeZone);
MigrateDateColumn(scope, "umbracoUser", "updateDate", timeZone);
MigrateDateColumn(scope, "umbracoUser", "emailConfirmedDate", timeZone);
MigrateDateColumn(scope, "umbracoUser", "lastLockoutDate", timeZone);
MigrateDateColumn(scope, "umbracoUser", "lastPasswordChangeDate", timeZone);
MigrateDateColumn(scope, "umbracoUser", "lastLoginDate", timeZone);
MigrateDateColumn(scope, "umbracoUser", "invitedDate", timeZone);
MigrateDateColumn(scope, "umbracoUserGroup", "createDate", timeZone);
MigrateDateColumn(scope, "umbracoUserGroup", "updateDate", timeZone);

Check warning on line 127 in src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/MigrateSystemDatesToUtc.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v17/dev)

❌ New issue: Large Method

Migrate has 73 lines, threshold = 70. Large functions with many lines of code are generally harder to understand and lower the code health. Avoid adding more lines to this function.

scope.Complete();
Context.Complete();
}

private static TimeSpan GetTimezoneOffset(string timeZoneName)
private static string GetWindowsTimeZoneId(TimeZoneInfo timeZone)
{
if (timeZone.HasIanaId is false)
{
return timeZone.Id;
}

// We know the provided timezone name exists, as it's either detected or configured (and configuration has been validated).
=> TimeZoneInfo.FindSystemTimeZoneById(timeZoneName).BaseUtcOffset;
if (TimeZoneInfo.TryConvertIanaIdToWindowsId(timeZone.Id, out var windowsId) is false)
{
throw new InvalidOperationException(
$"Could not update system dates to UTC as it was not possible to convert the detected local time zone IANA id '{timeZone.Id}' to a Windows Id necessary for updates with SQL Server. Please manually configure the 'Umbraco:CMS:SystemDateMigration:LocalServerTimeZone' app setting with a valid Windows time zone name.");
}

private void MigrateDateColumn(IScope scope, string tableName, string columName, string timezoneName, TimeSpan timeZoneOffset)
{
var offsetInMinutes = -timeZoneOffset.TotalMinutes;
var offSetInMinutesString = offsetInMinutes > 0
? $"+{offsetInMinutes}"
: $"{offsetInMinutes}";
return windowsId;
}

Sql sql;
private void MigrateDateColumn(
IScope scope,
string tableName,
string columName,
(string Name, Lazy<TimeSpan> BaseOffset, Lazy<string> WindowsName) timeZone)
{
if (DatabaseType == DatabaseType.SQLite)
{
// SQLite does not support AT TIME ZONE, but we can use the offset to update the dates. It won't take account of daylight saving time, but
// given these are historical dates in expected non-production environments, that are unlikely to be necessary to be 100% accurate, this is acceptable.
sql = Sql($"UPDATE {tableName} SET {columName} = DATETIME({columName}, '{offSetInMinutesString} minutes')");
MigrateDateColumnSQLite(scope, tableName, columName, timeZone.Name, timeZone.BaseOffset.Value);
}
else
{
sql = Sql($"UPDATE {tableName} SET {columName} = {columName} AT TIME ZONE '{timezoneName}' AT TIME ZONE 'UTC'");
MigrateDateColumnSqlServer(scope, tableName, columName, timeZone.WindowsName.Value);
}
}

private void MigrateDateColumnSQLite(
IScope scope,
string tableName,
string columName,
string timezoneName,
TimeSpan timezoneOffset)
{
// SQLite does not support AT TIME ZONE, but we can use the offset to update the dates. It won't take account of daylight saving time, but
// given these are historical dates in expected non-production environments, that are unlikely to be necessary to be 100% accurate, this is acceptable.

var offsetInMinutes = -timezoneOffset.TotalMinutes;
var offsetInMinutesString = offsetInMinutes > 0
? $"+{offsetInMinutes}"
: $"{offsetInMinutes}";

Sql sql = Sql($"UPDATE {tableName} SET {columName} = DATETIME({columName}, '{offsetInMinutesString} minutes')");

scope.Database.Execute(sql);

_logger.LogInformation(
"Migrated {TableName}.{ColumnName} from local server timezone of {TimeZoneName} ({OffSetInMinutes} minutes) to UTC.",
"Migrated {TableName}.{ColumnName} from timezone {TimeZoneName} ({OffsetInMinutes}) to UTC.",
tableName,
columName,
timezoneName,
offSetInMinutesString);
offsetInMinutesString);
}

private void MigrateDateColumnSqlServer(IScope scope, string tableName, string columName, string timeZoneName)
{
Sql sql = Sql($"UPDATE {tableName} SET {columName} = {columName} AT TIME ZONE '{timeZoneName}' AT TIME ZONE 'UTC'");

scope.Database.Execute(sql);

_logger.LogInformation(
"Migrated {TableName}.{ColumnName} from timezone {TimeZoneName} to UTC.",
tableName,
columName,
timeZoneName);

Check notice on line 202 in src/Umbraco.Infrastructure/Migrations/Upgrade/V_17_0_0/MigrateSystemDatesToUtc.cs

View check run for this annotation

CodeScene Delta Analysis / CodeScene Cloud Delta Analysis (v17/dev)

✅ No longer an issue: Excess Number of Function Arguments

MigrateDateColumn is no longer above the threshold for number of arguments. This function has too many arguments, indicating a lack of encapsulation. Avoid adding more arguments.
}
}

This file was deleted.