diff --git a/League.Demo/Chromium-Win/README.md b/League.Demo/Chromium-Win/README.md index e2f5ecfc..69ff0ed3 100644 --- a/League.Demo/Chromium-Win/README.md +++ b/League.Demo/Chromium-Win/README.md @@ -1,4 +1,6 @@ -# Chromium v131.0.6733.0 +# Chromium 111.0.5545.0 +# Chromium 131.x has issues with rendering dashed or dotted lines + This folder contains the binaries of the Chromium web browser. It can be downloaded from here: diff --git a/League.Demo/Configuration/AppSettings.json b/League.Demo/Configuration/AppSettings.json index 1bacf81a..a1a53b59 100644 --- a/League.Demo/Configuration/AppSettings.json +++ b/League.Demo/Configuration/AppSettings.json @@ -14,7 +14,7 @@ }, "User": { "RequireUniqueEmail": true, - "AllowedUserNameCharacters": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789öäüÖÄÜß#-._" /* no @; if set to "", all characters are allowed! */ + "AllowedUserNameCharacters": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789öäüÖÄÜß#-._" }, "Password": { "RequireDigit": false, @@ -26,14 +26,14 @@ }, "Lockout": { "AllowedForNewUsers": true, - "DefaultLockoutTimeSpan": "0.00:05:00.0000", /* TimeSpan of 5 minutes */ + "DefaultLockoutTimeSpan": "0.00:05:00.0000", "MaxFailedAccessAttempts": 5 } }, "LeagueUserValidatorOptions": { "RequiredUsernameLength": 2 }, - "Chromium": { + "Browser": { "ExecutablePath": "Chromium-Win\\chrome.exe" } } \ No newline at end of file diff --git a/League.Tests/Caching/ReportSheetCacheTests.cs b/League.Tests/Caching/ReportSheetCacheTests.cs index f8aa60ff..5a7bbf4d 100644 --- a/League.Tests/Caching/ReportSheetCacheTests.cs +++ b/League.Tests/Caching/ReportSheetCacheTests.cs @@ -25,7 +25,7 @@ public ReportSheetCacheTests() { _webHostEnvironment = new HostingEnvironment { WebRootPath = Path.GetTempPath(), ContentRootPath - // Because we use the Chromium installation in the demo web app + // Because we use a Browser installation in the demo web app = DirectoryLocator.GetTargetProjectPath(typeof(League.WebApp.WebAppStartup)) }; @@ -34,10 +34,10 @@ public ReportSheetCacheTests() Identifier = "testorg" }; - var chromiumPath = new List> - { new("Chromium:ExecutablePath", "Chromium-Win\\chrome.exe") }; + var browserPath = new List> + { new("Browser:ExecutablePath", "Chromium-Win\\chrome.exe") }; - IServiceProvider services = UnitTestHelpers.GetReportSheetCacheServiceProvider(_tenantContext, _webHostEnvironment, chromiumPath); + IServiceProvider services = UnitTestHelpers.GetReportSheetCacheServiceProvider(_tenantContext, _webHostEnvironment, browserPath); _cache = services.GetRequiredService(); } diff --git a/League.Tests/TestComponents/UnitTestHelpers.cs b/League.Tests/TestComponents/UnitTestHelpers.cs index 0786b91b..eeaede32 100644 --- a/League.Tests/TestComponents/UnitTestHelpers.cs +++ b/League.Tests/TestComponents/UnitTestHelpers.cs @@ -136,7 +136,7 @@ public static ServiceProvider GetTextTemplatingServiceProvider(ITenantContext te .BuildServiceProvider(); } - public static ServiceProvider GetReportSheetCacheServiceProvider(ITenantContext tenantContext, IWebHostEnvironment webHostEnvironment, IEnumerable> chromiumPath) + public static ServiceProvider GetReportSheetCacheServiceProvider(ITenantContext tenantContext, IWebHostEnvironment webHostEnvironment, IEnumerable> browserPath) { return new ServiceCollection() .AddLogging(builder => @@ -151,7 +151,7 @@ public static ServiceProvider GetReportSheetCacheServiceProvider(ITenantContext .AddTransient(sp => { var c = new ConfigurationManager(); - c.AddInMemoryCollection(chromiumPath); + c.AddInMemoryCollection(browserPath); return c; }) .AddTransient(sp => tenantContext) diff --git a/League/Caching/HtmlToPdfConverter.cs b/League/Caching/HtmlToPdfConverter.cs new file mode 100644 index 00000000..be5cfdd9 --- /dev/null +++ b/League/Caching/HtmlToPdfConverter.cs @@ -0,0 +1,197 @@ +// +// Copyright Volleyball League Project maintainers and contributors. +// Licensed under the MIT license. +// + +namespace League.Caching; + +#pragma warning disable CA3003 // reason: False positive due to CancellationToken in GetPdfDataBrowser + +/// +/// The class to create PDF files from HTML content. +/// For converting HTML to PDF, it uses either a Browser command line or . +/// +public class HtmlToPdfConverter : IDisposable +{ + private readonly string _pathToBrowser; + private readonly string _tempFolder; + private readonly ILoggerFactory _loggerFactory; + private readonly ILogger _logger; + private bool _isDisposing; + + /// + /// Initializes a new instance of the class. + /// + /// The path to the Browser executable. + /// The folder where temporary files will be stored. + /// + public HtmlToPdfConverter(string pathToBrowser, string tempPath, ILoggerFactory loggerFactory) + { + _pathToBrowser = pathToBrowser; + EnsureTempFolder(tempPath); + _tempFolder = CreateTempPathFolder(tempPath); + _loggerFactory = loggerFactory; + _logger = loggerFactory.CreateLogger(); + UsePuppeteer = false; + } + + /// + /// Gets or sets a value indicating whether to use Puppeteer for generating the report sheet, + /// instead of Browser command line. + /// + public bool UsePuppeteer { get; set; } + + private void EnsureTempFolder(string tempFolder) + { + if (Directory.Exists(tempFolder)) return; + + Directory.CreateDirectory(tempFolder); + _logger.LogDebug("Temporary path '{TempFolder}' created", tempFolder); + } + + /// + /// Creates a PDF file from the specified HTML content. + /// + /// + /// + /// A of the PDF file. + public async Task GeneratePdfData(string html, CancellationToken cancellationToken) + { + var pdfData = UsePuppeteer + ? await GetPdfDataPuppeteer(html) + : await GetPdfDataBrowser(html, cancellationToken); + + return pdfData; + } + + private async Task GetPdfDataBrowser(string html, CancellationToken cancellationToken) + { + var tmpHtmlPath = await CreateHtmlFile(html, cancellationToken); + + try + { + var tmpPdfFile = await CreatePdfDataBrowser(tmpHtmlPath, cancellationToken); + + if (tmpPdfFile != null && File.Exists(tmpPdfFile)) + return await File.ReadAllBytesAsync(tmpPdfFile, cancellationToken); + + _logger.LogError("Error creating PDF file with Browser"); + return null; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error creating PDF file with Browser"); + return null; + } + } + + private async Task GetPdfDataPuppeteer(string html) + { + var options = new PuppeteerSharp.LaunchOptions + { + Headless = true, + Browser = PuppeteerSharp.SupportedBrowser.Chromium, + // Alternative: --use-cmd-decoder=validating + Args = new[] // Chromium-based browsers require using a sandboxed browser for PDF generation, unless sandbox is disabled + { "--no-sandbox", "--disable-gpu", "--disable-extensions", "--use-cmd-decoder=passthrough" }, + ExecutablePath = _pathToBrowser, + Timeout = 5000, + ProtocolTimeout = 10000 // default is 180,000 - used for page.PdfDataAsync + }; + // Use Puppeteer as a wrapper for the browser, which can generate PDF from HTML + // Start command line arguments set by Puppeteer v20: + // --allow-pre-commit-input --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-field-trial-config --disable-hang-monitor --disable-infobars --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-search-engine-choice-screen --disable-sync --enable-automation --enable-blink-features=IdleDetection --export-tagged-pdf --generate-pdf-document-outline --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --disable-features=Translate,AcceptCHFrame,MediaRouter,OptimizationHints,ProcessPerSiteUpToMainFrameThreshold --enable-features= --headless=new --hide-scrollbars --mute-audio about:blank --no-sandbox --disable-gpu --disable-extensions --use-cmd-decoder=passthrough --remote-debugging-port=0 --user-data-dir="C:\Users\xyz\AppData\Local\Temp\yk1fjkgt.phb" + await using var browser = await PuppeteerSharp.Puppeteer.LaunchAsync(options, _loggerFactory).ConfigureAwait(false); + await using var page = await browser.NewPageAsync().ConfigureAwait(false); + + await page.SetContentAsync(html); // Bootstrap 5 is loaded from CDN + await page.EvaluateExpressionHandleAsync("document.fonts.ready"); // Wait for fonts to be loaded. Omitting this might result in no text rendered in pdf. + + try + { + return await page.PdfDataAsync(new PuppeteerSharp.PdfOptions + { Scale = 1.0M, Format = PuppeteerSharp.Media.PaperFormat.A4 }).ConfigureAwait(false); + } + catch(Exception ex) + { + _logger.LogError(ex, "Error creating PDF file with Puppeteer"); + return null; + } + } + + private async Task CreatePdfDataBrowser(string htmlFile, CancellationToken cancellationToken) + { + // Temporary file for the PDF stream from the Browser + // Note: non-existing file is handled in MovePdfToCache + var pdfFile = Path.Combine(_tempFolder, Path.GetRandomFileName() + ".pdf"); + + // Run the Browser + // Command line switches overview: https://kapeli.com/cheat_sheets/Chromium_Command_Line_Switches.docset/Contents/Resources/Documents/index + // or better https://peter.sh/experiments/chromium-command-line-switches/ + var startInfo = new System.Diagnostics.ProcessStartInfo(_pathToBrowser, + $"--allow-pre-commit-input --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-sync --enable-automation --enable-blink-features=IdleDetection --enable-features=NetworkServiceInProcess2 --export-tagged-pdf --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --headless --hide-scrollbars --mute-audio --no-sandbox --disable-gpu --use-cmd-decoder=passthrough --no-margins --user-data-dir={_tempFolder} --no-pdf-header-footer --print-to-pdf={pdfFile} {htmlFile}") + { CreateNoWindow = true, UseShellExecute = false }; + var proc = System.Diagnostics.Process.Start(startInfo); + + if (proc == null) + { + _logger.LogError("Process '{PathToBrowser}' could not be started.", _pathToBrowser); + return pdfFile; + } + + var timeout = TimeSpan.FromMilliseconds(5000); + var processTask = proc.WaitForExitAsync(cancellationToken); + + await Task.WhenAny(processTask, Task.Delay(timeout, cancellationToken)); + + if (processTask.IsCompleted) return pdfFile; + + proc.Kill(true); + return null; + } + + private async Task CreateHtmlFile(string html, CancellationToken cancellationToken) + { + var htmlFile = Path.Combine(_tempFolder, Path.GetRandomFileName() + ".html"); // extension must be "html" + await File.WriteAllTextAsync(htmlFile, html, cancellationToken); + return new Uri(htmlFile).AbsoluteUri; + } + + private static string CreateTempPathFolder(string tempPath) + { + // Create child folder in TempPath + var tempFolder = Path.Combine(tempPath, Path.GetRandomFileName()); + if (!Directory.Exists(tempFolder)) Directory.CreateDirectory(tempFolder); + return tempFolder; + } + + private void DeleteTempPathFolder() + { + // Delete folder in TempPath + if (!Directory.Exists(_tempFolder)) return; + Directory.Delete(_tempFolder, true); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposing || !disposing) return; + _isDisposing = true; + + try + { + DeleteTempPathFolder(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error disposing {HtmlToPdfConverter}", nameof(HtmlToPdfConverter)); + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } +} + +#pragma warning restore CA3003 // reason: False positive due to CancellationToken in GetPdfDataBrowser diff --git a/League/Caching/ReportSheetCache.cs b/League/Caching/ReportSheetCache.cs index 9d19236a..b5cb8cc6 100644 --- a/League/Caching/ReportSheetCache.cs +++ b/League/Caching/ReportSheetCache.cs @@ -8,7 +8,6 @@ namespace League.Caching; -#pragma warning disable S2083 // reason: False positive due to CancellationToken in GetOrCreatePdf #pragma warning disable CA3003 // reason: False positive due to CancellationToken in GetOrCreatePdf /// @@ -18,7 +17,7 @@ public class ReportSheetCache { private readonly ITenantContext _tenantContext; private readonly IWebHostEnvironment _webHostEnvironment; - private readonly string _pathToChromium; + private readonly string _pathToBrowser; private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; @@ -44,7 +43,7 @@ public ReportSheetCache(ITenantContext tenantContext, IConfiguration configurati { _tenantContext = tenantContext; _webHostEnvironment = webHostEnvironment; - _pathToChromium = Path.Combine(webHostEnvironment.ContentRootPath, configuration["Chromium:ExecutablePath"] ?? string.Empty); + _pathToBrowser = Path.Combine(webHostEnvironment.ContentRootPath, configuration["Browser:ExecutablePath"] ?? string.Empty); _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger(); UsePuppeteer = false; @@ -52,18 +51,17 @@ public ReportSheetCache(ITenantContext tenantContext, IConfiguration configurati /// /// Gets or sets a value indicating whether to use Puppeteer for generating the report sheet, - /// instead of Chromium command line. + /// instead of Browser command line. /// public bool UsePuppeteer { get; set; } private void EnsureCacheFolder() { var cacheFolder = Path.Combine(_webHostEnvironment.WebRootPath, ReportSheetCacheFolder); - if (!Directory.Exists(cacheFolder)) - { - Directory.CreateDirectory(cacheFolder); - _logger.LogDebug("Cache folder '{CacheFolder}' created", cacheFolder); - } + if (Directory.Exists(cacheFolder)) return; + + Directory.CreateDirectory(cacheFolder); + _logger.LogDebug("Cache folder '{CacheFolder}' created", cacheFolder); } /// @@ -83,11 +81,19 @@ public async Task GetOrCreatePdf(MatchReportSheetRow data, string html, { _logger.LogDebug("Create new match report for tenant '{Tenant}', match '{MatchId}'", _tenantContext.Identifier, data.Id); - cacheFile = UsePuppeteer - ? await GetReportSheetPuppeteer(data.Id, html, cancellationToken) - : await GetReportSheetChromium(data.Id, html, cancellationToken); + using var converter = new HtmlToPdfConverter(_pathToBrowser, CreateTempPathFolder(), _loggerFactory) + { UsePuppeteer = UsePuppeteer }; + + var pdfData = await converter.GeneratePdfData(html, cancellationToken); - if (cacheFile == null) return Stream.Null; + if (pdfData != null) + { + await File.WriteAllBytesAsync(cacheFile, pdfData, cancellationToken); + return File.OpenRead(cacheFile); + } + + _logger.LogWarning("No PDF data created for match ID '{MatchId}'", data.Id); + return Stream.Null; } _logger.LogDebug("Read match report from cache for tenant '{Tenant}', match '{MatchId}'", _tenantContext.Identifier, data.Id); @@ -101,41 +107,6 @@ private static bool IsOutdated(string cacheFile, DateTime dataModifiedOn) return !fi.Exists || fi.LastWriteTimeUtc < dataModifiedOn; // Database dates are in UTC } - private async Task GetReportSheetChromium(long matchId, string html, CancellationToken cancellationToken) - { - // Create folder in TempPath - var tempFolder = CreateTempPathFolder(); - - // Temporary file with HTML content - extension must be ".html"! - var htmlUri = await CreateHtmlFile(html, tempFolder, cancellationToken); - - var pdfFile = await CreateReportSheetPdfChromium(tempFolder, htmlUri, cancellationToken); - - var cacheFile = MovePdfToCache(pdfFile, matchId); - - DeleteTempPathFolder(tempFolder); - - return cacheFile; - } - - private string? MovePdfToCache(string pdfFile, long matchId) - { - if (!File.Exists(pdfFile)) return null; - - var fullPath = GetPathToCacheFile(matchId); - try - { - // may throw UnauthorizedAccessException on production server - File.Move(pdfFile, fullPath, true); - } - catch - { - File.Copy(pdfFile, fullPath, true); - } - - return fullPath; - } - private string GetPathToCacheFile(long matchId) { var fileName = string.Format(ReportSheetFilenameTemplate, _tenantContext.Identifier, matchId, @@ -144,83 +115,6 @@ private string GetPathToCacheFile(long matchId) return Path.Combine(_webHostEnvironment.WebRootPath, ReportSheetCacheFolder, fileName); } - private async Task GetReportSheetPuppeteer(long matchId, string html, CancellationToken cancellationToken) - { - var options = new PuppeteerSharp.LaunchOptions - { - Headless = true, - Browser = PuppeteerSharp.SupportedBrowser.Chromium, - // Alternative: --use-cmd-decoder=validating - Args = new[] // Chromium requires using a sandboxed browser for PDF generation, unless sandbox is disabled - { "--no-sandbox", "--disable-gpu", "--disable-extensions", "--use-cmd-decoder=passthrough" }, - ExecutablePath = _pathToChromium, - Timeout = 5000, - ProtocolTimeout = 10000 // default is 180,000 - used for page.PdfDataAsync - }; - // Use Puppeteer as a wrapper for the browser, which can generate PDF from HTML - // Start command line arguments set by Puppeteer v20: - // --allow-pre-commit-input --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-field-trial-config --disable-hang-monitor --disable-infobars --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-search-engine-choice-screen --disable-sync --enable-automation --enable-blink-features=IdleDetection --export-tagged-pdf --generate-pdf-document-outline --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --disable-features=Translate,AcceptCHFrame,MediaRouter,OptimizationHints,ProcessPerSiteUpToMainFrameThreshold --enable-features= --headless=new --hide-scrollbars --mute-audio about:blank --no-sandbox --disable-gpu --disable-extensions --use-cmd-decoder=passthrough --remote-debugging-port=0 --user-data-dir="C:\Users\xyz\AppData\Local\Temp\yk1fjkgt.phb" - await using var browser = await PuppeteerSharp.Puppeteer.LaunchAsync(options, _loggerFactory).ConfigureAwait(false); - await using var page = await browser.NewPageAsync().ConfigureAwait(false); - - await page.SetContentAsync(html); // Bootstrap 5 is loaded from CDN - await page.EvaluateExpressionHandleAsync("document.fonts.ready"); // Wait for fonts to be loaded. Omitting this might result in no text rendered in pdf. - - var fullPath = GetPathToCacheFile(matchId); - try - { - var bytes = await page.PdfDataAsync(new PuppeteerSharp.PdfOptions - { Scale = 1.0M, Format = PuppeteerSharp.Media.PaperFormat.A4 }).ConfigureAwait(false); - - await File.WriteAllBytesAsync(fullPath, bytes, cancellationToken); - } - catch(Exception ex) - { - _logger.LogError(ex, "Error creating PDF file with Puppeteer for match ID '{MatchId}'", matchId); - await File.WriteAllBytesAsync(fullPath, Array.Empty(), cancellationToken); - } - - return fullPath; - } - - private async Task CreateReportSheetPdfChromium(string tempFolder, string htmlUri, CancellationToken cancellationToken) - { - // Temporary file for the PDF stream from Chromium - // Note: non-existing file is handled in MovePdfToCache - var pdfFile = Path.Combine(tempFolder, Path.GetRandomFileName() + ".pdf"); - - // Run Chromium - // Command line switches overview: https://kapeli.com/cheat_sheets/Chromium_Command_Line_Switches.docset/Contents/Resources/Documents/index - // or better https://peter.sh/experiments/chromium-command-line-switches/ - var startInfo = new System.Diagnostics.ProcessStartInfo(_pathToChromium, - $"--allow-pre-commit-input --disable-background-networking --disable-background-timer-throttling --disable-backgrounding-occluded-windows --disable-breakpad --disable-client-side-phishing-detection --disable-component-extensions-with-background-pages --disable-component-update --disable-default-apps --disable-dev-shm-usage --disable-extensions --disable-features=Translate,BackForwardCache,AcceptCHFrame,MediaRouter,OptimizationHints --disable-hang-monitor --disable-ipc-flooding-protection --disable-popup-blocking --disable-prompt-on-repost --disable-renderer-backgrounding --disable-sync --enable-automation --enable-blink-features=IdleDetection --enable-features=NetworkServiceInProcess2 --export-tagged-pdf --force-color-profile=srgb --metrics-recording-only --no-first-run --password-store=basic --use-mock-keychain --headless --hide-scrollbars --mute-audio --no-sandbox --disable-gpu --use-cmd-decoder=passthrough --no-margins --user-data-dir={tempFolder} --no-pdf-header-footer --print-to-pdf={pdfFile} {htmlUri}") - { CreateNoWindow = true, UseShellExecute = false }; - var proc = System.Diagnostics.Process.Start(startInfo); - - if (proc == null) - { - _logger.LogError("Process '{PathToChromium}' could not be started.", _pathToChromium); - return pdfFile; - } - - var timeout = TimeSpan.FromMilliseconds(5000); - var processTask = proc.WaitForExitAsync(cancellationToken); - - await Task.WhenAny(processTask, Task.Delay(timeout, cancellationToken)); - - if (processTask.IsCompleted) return pdfFile; - - proc.Kill(true); - throw new OperationCanceledException($"Chromium timed out after {timeout.TotalMilliseconds}ms."); - } - - private static async Task CreateHtmlFile(string html, string tempFolder, CancellationToken cancellationToken) - { - var htmlFile = Path.Combine(tempFolder, Path.GetRandomFileName() + ".html"); - await File.WriteAllTextAsync(htmlFile, html, cancellationToken); - return new Uri(htmlFile).AbsoluteUri; - } - private static string CreateTempPathFolder() { // Create folder in TempPath @@ -228,20 +122,6 @@ private static string CreateTempPathFolder() if (!Directory.Exists(tempFolder)) Directory.CreateDirectory(tempFolder); return tempFolder; } - - private static void DeleteTempPathFolder(string path) - { - // Delete folder in TempPath - if (!Directory.Exists(path)) return; - try - { - Directory.Delete(path, true); - } - catch - { - // Best effort when trying to remove - } - } } #pragma warning restore CA3003 -#pragma warning restore S2083 + diff --git a/League/Configuration/AppSettings.json b/League/Configuration/AppSettings.json index c92209a4..00573272 100644 --- a/League/Configuration/AppSettings.json +++ b/League/Configuration/AppSettings.json @@ -22,7 +22,7 @@ }, "User": { "RequireUniqueEmail": true, - "AllowedUserNameCharacters": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789öäüÖÄÜß#-._" /* no @; if set to "", all characters are allowed! */ + "AllowedUserNameCharacters": "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789öäüÖÄÜß#-._" }, "Password": { "RequireDigit": false, @@ -34,14 +34,14 @@ }, "Lockout": { "AllowedForNewUsers": true, - "DefaultLockoutTimeSpan": "0.00:05:00.0000", /* TimeSpan of 5 minutes */ + "DefaultLockoutTimeSpan": "0.00:05:00.0000", "MaxFailedAccessAttempts": 5 } }, "LeagueUserValidatorOptions": { "RequiredUsernameLength": 2 }, - "Chromium": { + "Browser": { "ExecutablePath": "Chromium-Win\\chrome.exe" } } \ No newline at end of file diff --git a/League/Controllers/Match.cs b/League/Controllers/Match.cs index 5f1f2cc4..f617566d 100644 --- a/League/Controllers/Match.cs +++ b/League/Controllers/Match.cs @@ -8,7 +8,6 @@ using League.MultiTenancy; using League.Routing; using League.Views; -using MailMergeLib.AspNet; using SD.LLBLGen.Pro.ORMSupportClasses; using SD.LLBLGen.Pro.QuerySpec; using TournamentManager.DAL.EntityClasses; @@ -707,12 +706,15 @@ private async Task GetEnterResultViewModel(MatchEntity mat /// if the match has not already been played. /// /// - /// The cache injected from services. + /// /// /// A match report sheet suitable for a printout, if the match has not already been played. [HttpGet("[action]/{id:long}")] - public async Task ReportSheet(long id, [FromServices] ReportSheetCache cache, CancellationToken cancellationToken) + public async Task ReportSheet(long id, IServiceProvider services, CancellationToken cancellationToken) { + if (!ModelState.IsValid) return BadRequest(ModelState); + var cache = services.GetRequiredService(); + MatchReportSheetRow? model = null; cache.UsePuppeteer = false; @@ -727,7 +729,6 @@ public async Task ReportSheet(long id, [FromServices] ReportSheet contentDisposition.SetHttpFileName($"{_localizer["Report Sheet"].Value}_{model.Id}.pdf"); Response.Headers[Microsoft.Net.Http.Headers.HeaderNames.ContentDisposition] = contentDisposition.ToString(); - var html = await _razorViewToStringRenderer.RenderViewToStringAsync( $"~/Views/{nameof(Match)}/{ViewNames.Match.ReportSheet}.cshtml", model); @@ -744,7 +745,7 @@ public async Task ReportSheet(long id, [FromServices] ReportSheet Response.Clear(); return View(ViewNames.Match.ReportSheet, model); } - + private void SendFixtureNotification(long matchId) { var smt = _sendMailTask.CreateNewInstance(); diff --git a/League/Views/Match/ReportSheet.cshtml b/League/Views/Match/ReportSheet.cshtml index b44cc70c..34faf8c2 100644 --- a/League/Views/Match/ReportSheet.cshtml +++ b/League/Views/Match/ReportSheet.cshtml @@ -49,13 +49,9 @@ @ViewData["Title"] - - - @if(numberOfSets <= 3) { -