Merge branch 'v17/dev'
This commit is contained in:
+16
-45
@@ -8,67 +8,38 @@ using Umbraco.Cms.Core.Security;
|
||||
namespace Umbraco.Cms.Api.Management.Controllers.RedirectUrlManagement;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for setting the redirect URL tracking status.
|
||||
/// Controller for setting the redirect URL tracking status. Retained for backwards compatibility only;
|
||||
/// the endpoint no longer modifies any configuration.
|
||||
/// </summary>
|
||||
[ApiVersion("1.0")]
|
||||
[Obsolete("This controller is deprecated and no longer modifies the configuration. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
|
||||
public class SetStatusRedirectUrlManagementController : RedirectUrlManagementControllerBase
|
||||
{
|
||||
private readonly IBackOfficeSecurityAccessor _backOfficeSecurityAccessor;
|
||||
private readonly IConfigManipulator _configManipulator;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SetStatusRedirectUrlManagementController"/> class.
|
||||
/// </summary>
|
||||
/// <param name="backOfficeSecurityAccessor">The back office security accessor.</param>
|
||||
/// <param name="configManipulator">The configuration manipulator.</param>
|
||||
/// <param name="backOfficeSecurityAccessor">Ignored. Retained for binary compatibility.</param>
|
||||
/// <param name="configManipulator">Ignored. Retained for binary compatibility.</param>
|
||||
public SetStatusRedirectUrlManagementController(
|
||||
#pragma warning disable IDE0060 // Remove unused parameter
|
||||
IBackOfficeSecurityAccessor backOfficeSecurityAccessor,
|
||||
IConfigManipulator configManipulator)
|
||||
#pragma warning restore IDE0060 // Remove unused parameter
|
||||
{
|
||||
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
|
||||
_configManipulator = configManipulator;
|
||||
}
|
||||
|
||||
// TODO: Consider if we should even allow this, or only allow using the appsettings
|
||||
// We generally don't want to edit the appsettings from our code.
|
||||
// But maybe there is a valid use case for doing it on the fly.
|
||||
/// <summary>
|
||||
/// Sets the redirect URL tracking status.
|
||||
/// Deprecated. Returns an OK response without modifying any configuration. To toggle redirect URL tracking,
|
||||
/// set the <c>Umbraco:CMS:WebRouting:DisableRedirectUrlTracking</c> configuration key instead.
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">The cancellation token for the HTTP request.</param>
|
||||
/// <param name="status">The redirect status to set.</param>
|
||||
/// <returns>An OK result if successful.</returns>
|
||||
/// <param name="status">The redirect status (ignored).</param>
|
||||
/// <returns>An OK result.</returns>
|
||||
[HttpPost("status")]
|
||||
[EndpointSummary("Sets the redirect URL tracking status.")]
|
||||
[EndpointDescription("Updates the redirect URL tracking configuration according to the provided status.")]
|
||||
[EndpointSummary("Deprecated. No longer changes the redirect URL tracking status.")]
|
||||
[EndpointDescription("This endpoint is deprecated and no longer modifies the configuration. To toggle redirect URL tracking, set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead.")]
|
||||
[MapToApiVersion("1.0")]
|
||||
public async Task<IActionResult> SetStatus(CancellationToken cancellationToken, [FromQuery] RedirectStatus status)
|
||||
{
|
||||
// TODO: uncomment this when auth is implemented.
|
||||
// var userIsAdmin = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.IsAdmin();
|
||||
// if (userIsAdmin is null or false)
|
||||
// {
|
||||
// return Unauthorized();
|
||||
// }
|
||||
|
||||
var enable = status switch
|
||||
{
|
||||
RedirectStatus.Enabled => true,
|
||||
RedirectStatus.Disabled => false,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(status), status, "Unknown redirect status")
|
||||
};
|
||||
|
||||
// For now I'm not gonna change this to limit breaking, but it's weird to have a "disabled" switch,
|
||||
// since you're essentially negating the boolean from the get go,
|
||||
// it's much easier to reason with enabled = false == disabled.
|
||||
await _configManipulator.SaveDisableRedirectUrlTrackingAsync(!enable);
|
||||
|
||||
// Taken from the existing implementation in RedirectUrlManagementController
|
||||
// TODO this is ridiculous, but we need to ensure the configuration is reloaded, before this request is ended.
|
||||
// otherwise we can read the old value in GetEnableState.
|
||||
// The value is equal to JsonConfigurationSource.ReloadDelay
|
||||
Thread.Sleep(250);
|
||||
|
||||
return Ok();
|
||||
}
|
||||
[Obsolete("This endpoint is deprecated and no longer modifies the configuration. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
|
||||
public Task<IActionResult> SetStatus(CancellationToken cancellationToken, [FromQuery] RedirectStatus status)
|
||||
=> Task.FromResult<IActionResult>(Ok());
|
||||
}
|
||||
|
||||
+2
-2
@@ -28924,8 +28924,8 @@
|
||||
"tags": [
|
||||
"Redirect Management"
|
||||
],
|
||||
"summary": "Sets the redirect URL tracking status.",
|
||||
"description": "Updates the redirect URL tracking configuration according to the provided status.",
|
||||
"summary": "Deprecated. No longer changes the redirect URL tracking status.",
|
||||
"description": "This endpoint is deprecated and no longer modifies the configuration. To toggle redirect URL tracking, set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead.",
|
||||
"operationId": "PostRedirectManagementStatus",
|
||||
"parameters": [
|
||||
{
|
||||
|
||||
@@ -36,6 +36,7 @@ public interface IConfigManipulator
|
||||
/// </summary>
|
||||
/// <param name="disable">The value to save.</param>
|
||||
/// <returns></returns>
|
||||
[Obsolete("This method is no longer used by Umbraco. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
|
||||
Task SaveDisableRedirectUrlTrackingAsync(bool disable);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -104,6 +104,7 @@ internal sealed class JsonConfigManipulator : IConfigManipulator
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
[Obsolete("This method is no longer used by Umbraco. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead. Scheduled for removal in Umbraco 19.")]
|
||||
public async Task SaveDisableRedirectUrlTrackingAsync(bool disable)
|
||||
=> await CreateOrUpdateConfigValueAsync(DisableRedirectUrlTrackingPath, disable);
|
||||
|
||||
|
||||
@@ -47,30 +47,21 @@ public class LogViewerRepository : LogViewerRepositoryBase
|
||||
|
||||
var filesForCurrentDay = Directory.GetFiles(_loggingConfiguration.LogDirectory, filesToFind);
|
||||
|
||||
// Foreach file we find - open it
|
||||
// Foreach file we find - open it. Any failure reading a single file (open error,
|
||||
// unrecoverable parse error, etc.) should not prevent the remaining files for the
|
||||
// day or date range from being read.
|
||||
foreach (var filePath in filesForCurrentDay)
|
||||
{
|
||||
// Open log file & add contents to the log collection
|
||||
// Which we then use LINQ to page over
|
||||
using (var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
|
||||
try
|
||||
{
|
||||
using (var stream = new StreamReader(fs))
|
||||
{
|
||||
var reader = new LogEventReader(stream);
|
||||
while (TryRead(reader, out LogEvent? evt))
|
||||
{
|
||||
// We may get a null if log line is malformed
|
||||
if (evt == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (logFilter.TakeLogEvent(evt))
|
||||
{
|
||||
logs.Add(evt);
|
||||
}
|
||||
}
|
||||
}
|
||||
ReadLogFile(filePath, logFilter, logs);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
ex,
|
||||
"Skipped log file {FilePath} after a file-level error; the file may be inaccessible or unreadable.",
|
||||
filePath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -88,6 +79,63 @@ public class LogViewerRepository : LogViewerRepositoryBase
|
||||
}).ToArray();
|
||||
}
|
||||
|
||||
private void ReadLogFile(string filePath, ILogFilter logFilter, List<LogEvent> logs)
|
||||
{
|
||||
using var fs = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite);
|
||||
using var stream = new StreamReader(fs);
|
||||
var reader = new LogEventReader(stream);
|
||||
|
||||
var errorCount = 0;
|
||||
Exception? firstError = null;
|
||||
|
||||
while (true)
|
||||
{
|
||||
LogEvent? evt;
|
||||
try
|
||||
{
|
||||
if (!reader.TryRead(out evt))
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is Newtonsoft.Json.JsonException or InvalidDataException)
|
||||
{
|
||||
// Serilog.Formatting.Compact.Reader uses Newtonsoft.Json internally and surfaces
|
||||
// its exceptions (Umbraco's own serialization is on System.Text.Json, but that
|
||||
// doesn't apply here — we have to catch what the reader actually throws).
|
||||
// JsonException covers parse failures (e.g. an unterminated string in a truncated
|
||||
// entry); InvalidDataException covers structurally-valid JSON that isn't a valid
|
||||
// Serilog Compact event. Either way the offending line has been consumed from the
|
||||
// underlying StreamReader and the next TryRead call advances. Anything else
|
||||
// (IOException, decoder failures, etc.) is propagated to the file-level catch in
|
||||
// GetLogs so we don't risk a tight loop or silently swallow a more serious failure.
|
||||
errorCount++;
|
||||
firstError ??= ex;
|
||||
continue;
|
||||
}
|
||||
|
||||
// LogEventReader may return true with a null event for a benign skip.
|
||||
if (evt is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (logFilter.TakeLogEvent(evt))
|
||||
{
|
||||
logs.Add(evt);
|
||||
}
|
||||
}
|
||||
|
||||
if (errorCount > 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
firstError,
|
||||
"Encountered {ErrorCount} unreadable line(s) while reading log file {FilePath}. The file may contain partially-written or corrupt entries; affected lines were skipped.",
|
||||
errorCount,
|
||||
filePath);
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyDictionary<string, string?> MapLogMessageProperties(IReadOnlyDictionary<string, LogEventPropertyValue>? properties)
|
||||
{
|
||||
var result = new Dictionary<string, string?>();
|
||||
@@ -121,21 +169,4 @@ public class LogViewerRepository : LogViewerRepositoryBase
|
||||
}
|
||||
|
||||
private static string GetSearchPattern(DateTime day) => $"*{day:yyyyMMdd}*.json";
|
||||
|
||||
private bool TryRead(LogEventReader reader, out LogEvent? evt)
|
||||
{
|
||||
try
|
||||
{
|
||||
return reader.TryRead(out evt);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// As we are reading/streaming one line at a time in the JSON file
|
||||
// Thus we can not report the line number, as it will always be 1
|
||||
_logger.LogError(ex, "Unable to parse a line in the JSON log file");
|
||||
|
||||
evt = null;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2026,6 +2026,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'تعطيل تتبع URL',
|
||||
enableUrlTracker: 'تمكين تتبع URL',
|
||||
urlTrackerEnabled: 'ممكّن',
|
||||
urlTrackerDisabled: 'معطّل',
|
||||
culture: 'الثقافة',
|
||||
originalUrl: 'URL الأصلي',
|
||||
redirectedTo: 'تم إعادة التوجيه إلى',
|
||||
|
||||
@@ -1895,6 +1895,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'Onemogući URL tragač',
|
||||
enableUrlTracker: 'Omogući URL tragač',
|
||||
urlTrackerEnabled: 'Omogućen',
|
||||
urlTrackerDisabled: 'Onemogućen',
|
||||
originalUrl: 'Originalni URL',
|
||||
redirectedTo: 'Preusmjeri na',
|
||||
redirectUrlManagement: 'Preusmjeravanje URL-ova',
|
||||
|
||||
@@ -1731,6 +1731,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'Zakázat sledování URL',
|
||||
enableUrlTracker: 'Povolit sledování URL',
|
||||
urlTrackerEnabled: 'Povoleno',
|
||||
urlTrackerDisabled: 'Zakázáno',
|
||||
culture: 'Jazyk',
|
||||
originalUrl: 'Originální URL',
|
||||
redirectedTo: 'Přesměrováno na',
|
||||
|
||||
@@ -2075,6 +2075,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'Analluogi olinydd URL',
|
||||
enableUrlTracker: 'Galluogi olinydd URL',
|
||||
urlTrackerEnabled: "Wedi'i alluogi",
|
||||
urlTrackerDisabled: "Wedi'i analluogi",
|
||||
culture: 'Diwylliant',
|
||||
originalUrl: 'URL gwreiddiol',
|
||||
redirectedTo: 'Ailgyfeirwyd I',
|
||||
|
||||
@@ -2295,6 +2295,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'Slå URL tracker fra',
|
||||
enableUrlTracker: 'Slå URL tracker til',
|
||||
urlTrackerEnabled: 'Aktiveret',
|
||||
urlTrackerDisabled: 'Deaktiveret',
|
||||
culture: 'Kultur',
|
||||
originalUrl: 'Original URL',
|
||||
redirectedTo: 'Viderestillet til',
|
||||
|
||||
@@ -1943,6 +1943,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'URL-Änderungsaufzeichnung abschalten',
|
||||
enableUrlTracker: 'URL-Änderungsaufzeichnung einschalten',
|
||||
urlTrackerEnabled: 'Aktiviert',
|
||||
urlTrackerDisabled: 'Deaktiviert',
|
||||
culture: 'Kultur',
|
||||
originalUrl: 'Original URL',
|
||||
redirectedTo: 'Weiterleiten zu',
|
||||
|
||||
@@ -2484,7 +2484,13 @@ export default {
|
||||
},
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'Disable URL tracker',
|
||||
disableUrlTrackerInstruction:
|
||||
'Redirect URL tracking is configured through application settings. To disable tracking, set the following configuration key to true:',
|
||||
enableUrlTracker: 'Enable URL tracker',
|
||||
enableUrlTrackerInstruction:
|
||||
'Redirect URL tracking is configured through application settings. To enable tracking, set the following configuration key to false:',
|
||||
urlTrackerEnabled: 'Enabled',
|
||||
urlTrackerDisabled: 'Disabled',
|
||||
originalUrl: 'Original URL',
|
||||
redirectedTo: 'Redirected To',
|
||||
redirectUrlManagement: 'Redirect URL Management',
|
||||
|
||||
@@ -1432,6 +1432,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'Desactivar URL tracker',
|
||||
enableUrlTracker: 'Activar URL tracker',
|
||||
urlTrackerEnabled: 'Activado',
|
||||
urlTrackerDisabled: 'Desactivado',
|
||||
originalUrl: 'URL Original',
|
||||
redirectedTo: 'Redirigido a To',
|
||||
noRedirects: 'No se ha creado ninguna redirección',
|
||||
|
||||
@@ -1784,6 +1784,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'Désactiver URL tracker',
|
||||
enableUrlTracker: 'Activer URL tracker',
|
||||
urlTrackerEnabled: 'Activé',
|
||||
urlTrackerDisabled: 'Désactivé',
|
||||
culture: 'Culture',
|
||||
originalUrl: 'URL original',
|
||||
redirectedTo: 'Redirigé Vers',
|
||||
|
||||
@@ -1955,6 +1955,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'Onemogući URL praćenje',
|
||||
enableUrlTracker: 'Omogući URL praćenje',
|
||||
urlTrackerEnabled: 'Omogućeno',
|
||||
urlTrackerDisabled: 'Onemogućeno',
|
||||
originalUrl: 'Originalni URL',
|
||||
redirectedTo: 'Preusmjerno na',
|
||||
redirectUrlManagement: 'Preusmjeravanje URL-ova',
|
||||
|
||||
@@ -1993,6 +1993,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'Disabilita tracciamento degli URL',
|
||||
enableUrlTracker: 'Abilita tracciamento degli URL',
|
||||
urlTrackerEnabled: 'Abilitato',
|
||||
urlTrackerDisabled: 'Disabilitato',
|
||||
culture: 'Cultura',
|
||||
originalUrl: 'URL originale',
|
||||
redirectedTo: 'Reindirizzato a',
|
||||
|
||||
@@ -1877,6 +1877,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'URL tracker uitschakelen',
|
||||
enableUrlTracker: 'URL tracker inschakelen',
|
||||
urlTrackerEnabled: 'Ingeschakeld',
|
||||
urlTrackerDisabled: 'Uitgeschakeld',
|
||||
culture: 'Cultuur',
|
||||
originalUrl: 'Originele URL',
|
||||
redirectedTo: 'Doorgestuurd naar',
|
||||
|
||||
@@ -1277,6 +1277,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'Wyłącz śledzenie URL',
|
||||
enableUrlTracker: 'Włącz śledzenie URL',
|
||||
urlTrackerEnabled: 'Włączone',
|
||||
urlTrackerDisabled: 'Wyłączone',
|
||||
originalUrl: 'Oryginalny URL',
|
||||
redirectedTo: 'Przekierowane do',
|
||||
noRedirects: 'Nie stworzono żadnych przekierowań',
|
||||
|
||||
@@ -2287,6 +2287,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'Desativar Monitorizador de URLs',
|
||||
enableUrlTracker: 'Ativar Monitorizador de URLs',
|
||||
urlTrackerEnabled: 'Ativado',
|
||||
urlTrackerDisabled: 'Desativado',
|
||||
originalUrl: 'URL Original',
|
||||
redirectedTo: 'Redirecionado Para',
|
||||
redirectUrlManagement: 'Gestão de URL de Redirecionamento',
|
||||
|
||||
@@ -984,6 +984,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'Остановить отслеживание URL',
|
||||
enableUrlTracker: 'Запустить отслеживание URL',
|
||||
urlTrackerEnabled: 'Включено',
|
||||
urlTrackerDisabled: 'Отключено',
|
||||
originalUrl: 'Первоначальный URL',
|
||||
redirectedTo: 'Перенаправлен в',
|
||||
noRedirects: 'На данный момент нет ни одного перенаправления',
|
||||
|
||||
@@ -1726,6 +1726,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'URL izleyiciyi devre dışı bırakın',
|
||||
enableUrlTracker: 'URL izleyiciyi etkinleştir',
|
||||
urlTrackerEnabled: 'Etkin',
|
||||
urlTrackerDisabled: 'Devre Dışı',
|
||||
originalUrl: 'Orijinal URL',
|
||||
redirectedTo: 'Yönlendirildi',
|
||||
redirectUrlManagement: 'URL Yönetimini Yeniden Yönlendir',
|
||||
|
||||
@@ -982,6 +982,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'Зупинити відстеження URL',
|
||||
enableUrlTracker: 'Запустити відстеження URL',
|
||||
urlTrackerEnabled: 'Увімкнено',
|
||||
urlTrackerDisabled: 'Вимкнено',
|
||||
originalUrl: 'Початковий URL',
|
||||
redirectedTo: 'Перенаправлено в',
|
||||
noRedirects: 'На даний момент немає жодного перенаправлення',
|
||||
|
||||
@@ -2291,6 +2291,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: 'Tắt theo dõi URL',
|
||||
enableUrlTracker: 'Bật theo dõi URL',
|
||||
urlTrackerEnabled: 'Đã bật',
|
||||
urlTrackerDisabled: 'Đã tắt',
|
||||
originalUrl: 'URL gốc',
|
||||
redirectedTo: 'Chuyển hướng đến',
|
||||
redirectUrlManagement: 'Quản lý URL chuyển hướng',
|
||||
|
||||
@@ -1038,6 +1038,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: '停止網址追蹤器',
|
||||
enableUrlTracker: '啟動網址追蹤器',
|
||||
urlTrackerEnabled: '已啟用',
|
||||
urlTrackerDisabled: '已停用',
|
||||
originalUrl: '原本網址',
|
||||
redirectedTo: '轉址成',
|
||||
noRedirects: '沒有任何轉址',
|
||||
|
||||
@@ -1050,6 +1050,8 @@ export default {
|
||||
redirectUrls: {
|
||||
disableUrlTracker: '禁用 URL 跟踪程序',
|
||||
enableUrlTracker: '启用 URL 跟踪程序',
|
||||
urlTrackerEnabled: '已启用',
|
||||
urlTrackerDisabled: '已禁用',
|
||||
originalUrl: '原始网址',
|
||||
redirectedTo: '已重定向至',
|
||||
noRedirects: '未进行重定向',
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -5203,9 +5203,9 @@ export class RedirectManagementService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the redirect URL tracking status.
|
||||
* Deprecated. No longer changes the redirect URL tracking status.
|
||||
*
|
||||
* Updates the redirect URL tracking configuration according to the provided status.
|
||||
* This endpoint is deprecated and no longer modifies the configuration. To toggle redirect URL tracking, set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead.
|
||||
*/
|
||||
public static postRedirectManagementStatus<ThrowOnError extends boolean = true>(options?: Options<PostRedirectManagementStatusData, ThrowOnError>) {
|
||||
return (options?.client ?? client).post<PostRedirectManagementStatusResponses, PostRedirectManagementStatusErrors, ThrowOnError>({
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+42
-33
@@ -9,7 +9,7 @@ import {
|
||||
state,
|
||||
when,
|
||||
} from '@umbraco-cms/backoffice/external/lit';
|
||||
import { umbConfirmModal } from '@umbraco-cms/backoffice/modal';
|
||||
import { umbConfirmModal, umbInfoModal } from '@umbraco-cms/backoffice/modal';
|
||||
import { UmbDocumentRedirectManagementRepository } from '@umbraco-cms/backoffice/document';
|
||||
import { UmbLitElement } from '@umbraco-cms/backoffice/lit-element';
|
||||
import { UmbTextStyles } from '@umbraco-cms/backoffice/style';
|
||||
@@ -113,26 +113,19 @@ export class UmbDashboardRedirectManagementElement extends UmbLitElement {
|
||||
this.#getRedirectData(this._search.value);
|
||||
}
|
||||
|
||||
async #onRequestTrackerToggle() {
|
||||
if (!this._trackerEnabled) {
|
||||
this.#trackerToggle();
|
||||
return;
|
||||
}
|
||||
|
||||
await umbConfirmModal(this, {
|
||||
headline: '#redirectUrls_disableUrlTracker',
|
||||
content: '#redirectUrls_confirmDisable',
|
||||
color: 'danger',
|
||||
confirmLabel: '#actions_disable',
|
||||
});
|
||||
|
||||
this.#trackerToggle();
|
||||
}
|
||||
|
||||
async #trackerToggle() {
|
||||
const { error } = await this.#repository.setStatus(!this._trackerEnabled);
|
||||
if (error) return;
|
||||
this._trackerEnabled = !this._trackerEnabled;
|
||||
async #showTrackerInfo() {
|
||||
const isEnabled = this._trackerEnabled;
|
||||
await umbInfoModal(this, {
|
||||
headline: isEnabled ? '#redirectUrls_disableUrlTracker' : '#redirectUrls_enableUrlTracker',
|
||||
content: html`
|
||||
<p>
|
||||
${this.localize.term(
|
||||
isEnabled ? 'redirectUrls_disableUrlTrackerInstruction' : 'redirectUrls_enableUrlTrackerInstruction',
|
||||
)}
|
||||
</p>
|
||||
<p><code>Umbraco:CMS:WebRouting:DisableRedirectUrlTracking</code></p>
|
||||
`,
|
||||
}).catch(() => undefined);
|
||||
}
|
||||
|
||||
override render() {
|
||||
@@ -153,20 +146,24 @@ export class UmbDashboardRedirectManagementElement extends UmbLitElement {
|
||||
@click=${this.#onSearch}
|
||||
.state=${this._buttonState}></uui-button>
|
||||
</div>
|
||||
<uui-button
|
||||
look="outline"
|
||||
label=${this.localize.term('redirectUrls_disableUrlTracker')}
|
||||
@click=${this.#onRequestTrackerToggle}></uui-button>
|
||||
`,
|
||||
() => html`
|
||||
<div></div>
|
||||
<uui-button
|
||||
color="positive"
|
||||
look="outline"
|
||||
label=${this.localize.term('redirectUrls_enableUrlTracker')}
|
||||
@click=${this.#onRequestTrackerToggle}></uui-button>
|
||||
`,
|
||||
() => html`<div></div>`,
|
||||
)}
|
||||
<uui-button
|
||||
id="tracker-status"
|
||||
compact
|
||||
label=${this.localize.term(
|
||||
this._trackerEnabled ? 'redirectUrls_urlTrackerEnabled' : 'redirectUrls_urlTrackerDisabled',
|
||||
)}
|
||||
@click=${this.#showTrackerInfo}>
|
||||
<uui-tag color="default">
|
||||
<uui-icon name="icon-info"></uui-icon>
|
||||
<umb-localize
|
||||
key=${this._trackerEnabled
|
||||
? 'redirectUrls_urlTrackerEnabled'
|
||||
: 'redirectUrls_urlTrackerDisabled'}></umb-localize>
|
||||
</uui-tag>
|
||||
</uui-button>
|
||||
</div>
|
||||
${when(
|
||||
this._redirectData?.length,
|
||||
@@ -287,6 +284,18 @@ export class UmbDashboardRedirectManagementElement extends UmbLitElement {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
#tracker-status {
|
||||
--uui-button-background-color: transparent;
|
||||
--uui-button-background-color-hover: transparent;
|
||||
}
|
||||
|
||||
uui-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--uui-size-1);
|
||||
text-wrap: nowrap;
|
||||
}
|
||||
|
||||
#search-wrapper {
|
||||
display: flex;
|
||||
gap: var(--uui-size-4);
|
||||
|
||||
+11
@@ -2,6 +2,7 @@ import type { UmbDocumentRedirectFilterArgs } from './types.js';
|
||||
import { UmbDocumentRedirectManagementServerDataSource } from './document-redirect-management.server.data-source.js';
|
||||
import { UmbControllerBase } from '@umbraco-cms/backoffice/class-api';
|
||||
import type { UmbApi } from '@umbraco-cms/backoffice/extension-api';
|
||||
import { UmbDeprecation } from '@umbraco-cms/backoffice/utils';
|
||||
|
||||
/**
|
||||
* Repository for managing document redirect URLs.
|
||||
@@ -25,8 +26,18 @@ export class UmbDocumentRedirectManagementRepository extends UmbControllerBase i
|
||||
* @param {boolean} enabled - Whether the tracker should be enabled.
|
||||
* @returns {*}
|
||||
* @memberof UmbDocumentRedirectManagementRepository
|
||||
* @deprecated Deprecated since v17. The backend endpoint is now a no-op; set the
|
||||
* `Umbraco:CMS:WebRouting:DisableRedirectUrlTracking` configuration key instead.
|
||||
* Scheduled for removal in Umbraco 19.
|
||||
*/
|
||||
async setStatus(enabled: boolean) {
|
||||
new UmbDeprecation({
|
||||
deprecated: 'UmbDocumentRedirectManagementRepository.setStatus()',
|
||||
removeInVersion: '19.0.0',
|
||||
solution:
|
||||
'The backend endpoint is now a no-op. Set the Umbraco:CMS:WebRouting:DisableRedirectUrlTracking configuration key instead.',
|
||||
}).warn();
|
||||
|
||||
return this.#dataSource.setStatus(enabled);
|
||||
}
|
||||
|
||||
|
||||
+3
@@ -48,6 +48,9 @@ export class UmbDocumentRedirectManagementServerDataSource {
|
||||
* @param {boolean} enabled - Whether the tracker should be enabled.
|
||||
* @returns {*}
|
||||
* @memberof UmbDocumentRedirectManagementServerDataSource
|
||||
* @deprecated Deprecated since v17. The backend endpoint is now a no-op; set the
|
||||
* `Umbraco:CMS:WebRouting:DisableRedirectUrlTracking` configuration key instead.
|
||||
* Scheduled for removal in Umbraco 19.
|
||||
*/
|
||||
async setStatus(enabled: boolean) {
|
||||
const status = enabled ? RedirectStatusModel.ENABLED : RedirectStatusModel.DISABLED;
|
||||
|
||||
@@ -12,51 +12,77 @@ internal sealed class LogViewerServiceTests : UmbracoIntegrationTest
|
||||
{
|
||||
private ILogViewerService LogViewerService => GetRequiredService<ILogViewerService>();
|
||||
|
||||
private const string LogfileName = "UmbracoTraceLog.INTEGRATIONTEST.20230707.json";
|
||||
// A pre-recorded log file copied from TestData/TestLogs/ into the configured log directory in
|
||||
// OneTimeSetUp. Contains 362 valid Compact JSON entries from a real Umbraco test run and is the
|
||||
// basis for the "Can_*" assertions below.
|
||||
private const string SampleLogfileName = "UmbracoTraceLog.INTEGRATIONTEST.20230707.json";
|
||||
private readonly DateTime _sampleStartDate = new(2023, 7, 7);
|
||||
private readonly DateTime _sampleEndDate = new(2023, 7, 8);
|
||||
|
||||
private string _newLogfilePath;
|
||||
// A second log file written in OneTimeSetUp that deliberately contains an unterminated JSON
|
||||
// entry between two valid entries, to exercise corrupt-line recovery in LogViewerRepository.
|
||||
// The date is deliberately outside the sample file's range so the exact-count assertions on
|
||||
// the sample file are not influenced by these entries.
|
||||
private const string CorruptLogfileName = "UmbracoTraceLog.WITHCORRUPTLINE.20240109.json";
|
||||
private readonly DateTime _corruptStartDate = new(2024, 1, 9);
|
||||
private readonly DateTime _corruptEndDate = new(2024, 1, 10);
|
||||
|
||||
private readonly DateTime _startDate = new(2023, 7, 7);
|
||||
private readonly DateTime _endDate = new(2023, 7, 8);
|
||||
private const string CorruptFileValidLineBefore =
|
||||
"""{"@t":"2024-01-09T09:00:00.0000000Z","@mt":"First valid entry","SourceContext":"Test","ProcessId":1,"ProcessName":"Test","ThreadId":1,"MachineName":"TEST","Log4NetLevel":"INFO "}""";
|
||||
|
||||
// Truncated mid-string — mirrors the failure mode from https://github.com/umbraco/Umbraco-CMS/issues/22820
|
||||
// (JsonReaderException: Unterminated string).
|
||||
private const string CorruptFileTruncatedLine =
|
||||
"""{"@t":"2024-01-09T09:00:01.0000000Z","@mt":"Truncated entry""";
|
||||
|
||||
private const string CorruptFileValidLineAfter =
|
||||
"""{"@t":"2024-01-09T09:00:02.0000000Z","@mt":"Second valid entry","SourceContext":"Test","ProcessId":1,"ProcessName":"Test","ThreadId":1,"MachineName":"TEST","Log4NetLevel":"INFO "}""";
|
||||
|
||||
private string _sampleLogfilePath;
|
||||
private string _corruptLogfilePath;
|
||||
|
||||
[OneTimeSetUp]
|
||||
public void Setup()
|
||||
{
|
||||
// Create an example JSON log file to check results
|
||||
// As a one time setup for all tets in this class/fixture
|
||||
var testRoot = TestContext.CurrentContext.TestDirectory.Split("bin")[0];
|
||||
var ioHelper = TestHelper.IOHelper;
|
||||
var hostingEnv = TestHelper.GetHostingEnvironment();
|
||||
|
||||
var loggingConfiguration = TestHelper.GetLoggingConfiguration(hostingEnv);
|
||||
|
||||
var exampleLogfilePath = Path.Combine(testRoot, "TestData", "TestLogs", LogfileName);
|
||||
|
||||
string newLogfileDirPath = loggingConfiguration.LogDirectory;
|
||||
_newLogfilePath = Path.Combine(newLogfileDirPath, LogfileName);
|
||||
|
||||
// Create/ensure Directory exists
|
||||
var newLogfileDirPath = loggingConfiguration.LogDirectory;
|
||||
ioHelper.EnsurePathExists(newLogfileDirPath);
|
||||
|
||||
// Copy the sample files
|
||||
File.Copy(exampleLogfilePath, _newLogfilePath, true);
|
||||
// Sample log file (good content, 362 entries).
|
||||
var sampleLogfileSource = Path.Combine(testRoot, "TestData", "TestLogs", SampleLogfileName);
|
||||
_sampleLogfilePath = Path.Combine(newLogfileDirPath, SampleLogfileName);
|
||||
File.Copy(sampleLogfileSource, _sampleLogfilePath, true);
|
||||
|
||||
// Corrupt log file (two valid entries around a truncated one).
|
||||
_corruptLogfilePath = Path.Combine(newLogfileDirPath, CorruptLogfileName);
|
||||
var corruptContent = string.Join(
|
||||
Environment.NewLine,
|
||||
new[] { CorruptFileValidLineBefore, CorruptFileTruncatedLine, CorruptFileValidLineAfter }) + Environment.NewLine;
|
||||
File.WriteAllText(_corruptLogfilePath, corruptContent);
|
||||
}
|
||||
|
||||
[OneTimeTearDown]
|
||||
public void TearDown()
|
||||
{
|
||||
// Cleanup & delete the example log & search files off disk
|
||||
// Once all tests in this class/fixture have run
|
||||
if (File.Exists(_newLogfilePath))
|
||||
if (File.Exists(_sampleLogfilePath))
|
||||
{
|
||||
File.Delete(_newLogfilePath);
|
||||
File.Delete(_sampleLogfilePath);
|
||||
}
|
||||
|
||||
if (File.Exists(_corruptLogfilePath))
|
||||
{
|
||||
File.Delete(_corruptLogfilePath);
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_View_Logs()
|
||||
{
|
||||
var attempt = await LogViewerService.CanViewLogsAsync(_startDate, _endDate);
|
||||
var attempt = await LogViewerService.CanViewLogsAsync(_sampleStartDate, _sampleEndDate);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
@@ -68,7 +94,7 @@ internal sealed class LogViewerServiceTests : UmbracoIntegrationTest
|
||||
[Test]
|
||||
public async Task Can_Get_Logs()
|
||||
{
|
||||
var attempt = await LogViewerService.GetPagedLogsAsync(_startDate, _endDate, 0, int.MaxValue);
|
||||
var attempt = await LogViewerService.GetPagedLogsAsync(_sampleStartDate, _sampleEndDate, 0, int.MaxValue);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
@@ -84,8 +110,8 @@ internal sealed class LogViewerServiceTests : UmbracoIntegrationTest
|
||||
public async Task Can_Get_Logs_By_Filter_Expression()
|
||||
{
|
||||
var attempt = await LogViewerService.GetPagedLogsAsync(
|
||||
_startDate,
|
||||
_endDate,
|
||||
_sampleStartDate,
|
||||
_sampleEndDate,
|
||||
0,
|
||||
int.MaxValue,
|
||||
filterExpression: "@Level='Error'");
|
||||
@@ -104,7 +130,7 @@ internal sealed class LogViewerServiceTests : UmbracoIntegrationTest
|
||||
public async Task Can_Get_Logs_By_Log_Levels()
|
||||
{
|
||||
var attempt =
|
||||
await LogViewerService.GetPagedLogsAsync(_startDate, _endDate, 0, int.MaxValue, logLevels: new[] {"Error"});
|
||||
await LogViewerService.GetPagedLogsAsync(_sampleStartDate, _sampleEndDate, 0, int.MaxValue, logLevels: new[] {"Error"});
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
@@ -119,7 +145,7 @@ internal sealed class LogViewerServiceTests : UmbracoIntegrationTest
|
||||
[Test]
|
||||
public async Task Can_Get_Log_Count()
|
||||
{
|
||||
var attempt = await LogViewerService.GetLogLevelCountsAsync(_startDate, _endDate);
|
||||
var attempt = await LogViewerService.GetLogLevelCountsAsync(_sampleStartDate, _sampleEndDate);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
@@ -137,7 +163,7 @@ internal sealed class LogViewerServiceTests : UmbracoIntegrationTest
|
||||
[Test]
|
||||
public async Task Can_Get_Message_Templates()
|
||||
{
|
||||
var attempt = await LogViewerService.GetMessageTemplatesAsync(_startDate, _endDate, 0, int.MaxValue);
|
||||
var attempt = await LogViewerService.GetMessageTemplatesAsync(_sampleStartDate, _sampleEndDate, 0, int.MaxValue);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
@@ -214,4 +240,42 @@ internal sealed class LogViewerServiceTests : UmbracoIntegrationTest
|
||||
Assert.AreEqual(savedAttempt.Result, deleteAttempt.Result);
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that a log file containing an unterminated JSON entry between two valid entries
|
||||
/// does not prevent the log viewer from returning the surrounding valid entries.
|
||||
/// Regression coverage for https://github.com/umbraco/Umbraco-CMS/issues/22820.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Reads_Valid_Lines_Either_Side_Of_Corrupt_Line()
|
||||
{
|
||||
var attempt = await LogViewerService.GetPagedLogsAsync(_corruptStartDate, _corruptEndDate, 0, int.MaxValue);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsTrue(attempt.Success);
|
||||
Assert.AreEqual(LogViewerOperationStatus.Success, attempt.Status);
|
||||
Assert.IsNotNull(attempt.Result);
|
||||
Assert.AreEqual(2, attempt.Result.Total, "Expected the two valid lines either side of the corrupt line to be returned.");
|
||||
Assert.That(attempt.Result.Items.Select(x => x.RenderedMessage), Is.EquivalentTo(new[] { "First valid entry", "Second valid entry" }));
|
||||
});
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verifies that the level-counts request returns successfully and tallies the surrounding valid
|
||||
/// entries in the presence of an unterminated JSON entry in the source file.
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Get_Log_Count_Succeeds_With_Corrupt_Line()
|
||||
{
|
||||
var attempt = await LogViewerService.GetLogLevelCountsAsync(_corruptStartDate, _corruptEndDate);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.IsTrue(attempt.Success);
|
||||
Assert.AreEqual(LogViewerOperationStatus.Success, attempt.Status);
|
||||
Assert.IsNotNull(attempt.Result);
|
||||
Assert.AreEqual(2, attempt.Result.Information);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user