diff --git a/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs b/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs
index 8204b13c0..bd6f3ce39 100644
--- a/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs
+++ b/src/Altinn.App.Core/Infrastructure/Clients/Pdf/PdfGeneratorClient.cs
@@ -51,13 +51,27 @@ IHttpContextAccessor httpContextAccessor
///
public async Task GeneratePdf(Uri uri, CancellationToken ct)
+ {
+ return await GeneratePdf(uri, null, ct);
+ }
+
+ ///
+ public async Task GeneratePdf(Uri uri, string? footerContent, CancellationToken ct)
{
bool hasWaitForSelector = !string.IsNullOrWhiteSpace(_pdfGeneratorSettings.WaitForSelector);
PdfGeneratorRequest generatorRequest =
new()
{
Url = uri.AbsoluteUri,
- WaitFor = hasWaitForSelector ? _pdfGeneratorSettings.WaitForSelector : _pdfGeneratorSettings.WaitForTime
+ WaitFor = hasWaitForSelector
+ ? _pdfGeneratorSettings.WaitForSelector
+ : _pdfGeneratorSettings.WaitForTime,
+ Options =
+ {
+ HeaderTemplate = "",
+ FooterTemplate = footerContent ?? "",
+ DisplayHeaderFooter = footerContent != null,
+ },
};
generatorRequest.Cookies.Add(
diff --git a/src/Altinn.App.Core/Internal/Pdf/IPdfGeneratorClient.cs b/src/Altinn.App.Core/Internal/Pdf/IPdfGeneratorClient.cs
index 5ebbcf8a8..2412a6317 100644
--- a/src/Altinn.App.Core/Internal/Pdf/IPdfGeneratorClient.cs
+++ b/src/Altinn.App.Core/Internal/Pdf/IPdfGeneratorClient.cs
@@ -10,4 +10,10 @@ public interface IPdfGeneratorClient
///
/// A stream with the binary content of the generated PDF
Task GeneratePdf(Uri uri, CancellationToken ct);
+
+ ///
+ /// Generates a PDF.
+ ///
+ /// A stream with the binary content of the generated PDF with a footer
+ Task GeneratePdf(Uri uri, string? footerContent, CancellationToken ct);
}
diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfGeneratorSettings.cs b/src/Altinn.App.Core/Internal/Pdf/PdfGeneratorSettings.cs
index 5da9f96ae..a20c79439 100644
--- a/src/Altinn.App.Core/Internal/Pdf/PdfGeneratorSettings.cs
+++ b/src/Altinn.App.Core/Internal/Pdf/PdfGeneratorSettings.cs
@@ -24,4 +24,9 @@ public class PdfGeneratorSettings
///
/// This will be ignored if has been assigned a value.
public int WaitForTime { get; set; } = 5000;
+
+ ///
+ /// Shows a footer on each page in the PDF with the date, altinn-referance, page number and total pages.
+ ///
+ public bool DisplayFooter { get; set; }
}
diff --git a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs
index e8ac114e2..4c1319acc 100644
--- a/src/Altinn.App.Core/Internal/Pdf/PdfService.cs
+++ b/src/Altinn.App.Core/Internal/Pdf/PdfService.cs
@@ -1,3 +1,4 @@
+using System.Globalization;
using System.Security.Claims;
using Altinn.App.Core.Configuration;
using Altinn.App.Core.Extensions;
@@ -11,6 +12,7 @@
using Altinn.Platform.Profile.Models;
using Altinn.Platform.Storage.Interface.Models;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
@@ -28,6 +30,7 @@ public class PdfService : IPdfService
private readonly IPdfGeneratorClient _pdfGeneratorClient;
private readonly PdfGeneratorSettings _pdfGeneratorSettings;
+ private readonly ILogger _logger;
private readonly GeneralSettings _generalSettings;
private readonly Telemetry? _telemetry;
private const string PdfElementType = "ref-data-as-pdf";
@@ -43,6 +46,7 @@ public class PdfService : IPdfService
/// PDF generator client for the experimental PDF generator service
/// PDF generator related settings.
/// The app general settings.
+ /// The logger.
/// Telemetry for metrics and traces.
public PdfService(
IAppResources appResources,
@@ -52,6 +56,7 @@ public PdfService(
IPdfGeneratorClient pdfGeneratorClient,
IOptions pdfGeneratorSettings,
IOptions generalSettings,
+ ILogger logger,
Telemetry? telemetry = null
)
{
@@ -62,6 +67,7 @@ public PdfService(
_pdfGeneratorClient = pdfGeneratorClient;
_pdfGeneratorSettings = pdfGeneratorSettings.Value;
_generalSettings = generalSettings.Value;
+ _logger = logger;
_telemetry = telemetry;
}
@@ -113,7 +119,10 @@ CancellationToken ct
Uri uri = BuildUri(baseUrl, pagePath, language);
- Stream pdfContent = await _pdfGeneratorClient.GeneratePdf(uri, ct);
+ bool displayFooter = _pdfGeneratorSettings.DisplayFooter;
+ string? footerContent = displayFooter ? GetFooterContent(instance) : null;
+
+ Stream pdfContent = await _pdfGeneratorClient.GeneratePdf(uri, footerContent, ct);
return pdfContent;
}
@@ -225,4 +234,43 @@ private static string GetValidFileName(string fileName)
fileName = Uri.EscapeDataString(fileName.AsFileName(false));
return fileName;
}
+
+ private string GetFooterContent(Instance instance)
+ {
+ TimeZoneInfo timeZone = TimeZoneInfo.Utc;
+ try
+ {
+ // attempt to set timezone to norwegian
+ timeZone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Oslo");
+ }
+ catch (TimeZoneNotFoundException e)
+ {
+ _logger.LogWarning($"Could not find timezone Europe/Oslo. Defaulting to UTC. {e.Message}");
+ }
+
+ DateTimeOffset now = TimeZoneInfo.ConvertTime(DateTimeOffset.UtcNow, timeZone);
+
+ string dateGenerated = now.ToString("G", new CultureInfo("nb-NO"));
+ string altinnReferenceId = instance.Id.Split("/")[1].Split("-")[4];
+
+ string footerTemplate =
+ $@"";
+ return footerTemplate;
+ }
}
diff --git a/src/Altinn.App.Core/Models/Pdf/PdfGeneratorRequestOptions.cs b/src/Altinn.App.Core/Models/Pdf/PdfGeneratorRequestOptions.cs
index 184078fe7..ddb51abec 100644
--- a/src/Altinn.App.Core/Models/Pdf/PdfGeneratorRequestOptions.cs
+++ b/src/Altinn.App.Core/Models/Pdf/PdfGeneratorRequestOptions.cs
@@ -10,6 +10,21 @@ internal class PdfGeneratorRequestOptions
///
public bool DisplayHeaderFooter { get; set; } = false;
+ ///
+ /// HTML template for the print header. Should be valid HTML with the following classes used to inject values into them:
+ /// * `date` formatted print date
+ /// * `title` document title
+ /// * `url` document location
+ /// * `pageNumber` current page number
+ /// * `totalPages` total pages in the document
+ ///
+ public string HeaderTemplate { get; set; } = string.Empty;
+
+ ///
+ /// HTML template for the print footer. Has the same constraints and support for special classes as HeaderTemplate.
+ ///
+ public string FooterTemplate { get; set; } = string.Empty;
+
///
/// Indicate wheter the background should be included.
///
diff --git a/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs
index 01f8e5ded..c4a32508f 100644
--- a/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs
+++ b/test/Altinn.App.Api.Tests/Controllers/InstancesController_PostNewInstance.cs
@@ -269,7 +269,9 @@ public async Task InstationAllowedByOrg_Returns_Ok_For_User_When_Copying_Simplif
{
var pdfMock = new Mock(MockBehavior.Strict);
using var pdfReturnStream = new MemoryStream();
- pdfMock.Setup(p => p.GeneratePdf(It.IsAny(), It.IsAny())).ReturnsAsync(pdfReturnStream);
+ pdfMock
+ .Setup(p => p.GeneratePdf(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(pdfReturnStream);
// Setup test data
string org = "tdd";
diff --git a/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs
index 6755f9302..030d5a09f 100644
--- a/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs
+++ b/test/Altinn.App.Api.Tests/Controllers/PdfControllerTests.cs
@@ -10,10 +10,12 @@
using Altinn.App.Core.Internal.Pdf;
using Altinn.App.Core.Internal.Profile;
using Altinn.Platform.Storage.Interface.Models;
+using Castle.Core.Logging;
using FluentAssertions;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Moq;
using Moq.Protected;
@@ -42,6 +44,8 @@ public class PdfControllerTests
new() { }
);
+ private readonly Mock> _logger = new();
+
public PdfControllerTests()
{
_instanceClient
@@ -92,7 +96,8 @@ public async Task Request_In_Prod_Should_Be_Blocked()
_profile.Object,
pdfGeneratorClient,
_pdfGeneratorSettingsOptions,
- generalSettingsOptions
+ generalSettingsOptions,
+ _logger.Object
);
var pdfController = new PdfController(
_instanceClient.Object,
@@ -146,7 +151,8 @@ public async Task Request_In_Dev_Should_Generate()
_profile.Object,
pdfGeneratorClient,
_pdfGeneratorSettingsOptions,
- generalSettingsOptions
+ generalSettingsOptions,
+ _logger.Object
);
var pdfController = new PdfController(
_instanceClient.Object,
@@ -225,7 +231,8 @@ public async Task Request_In_Dev_Should_Include_Frontend_Version()
_profile.Object,
pdfGeneratorClient,
_pdfGeneratorSettingsOptions,
- generalSettingsOptions
+ generalSettingsOptions,
+ _logger.Object
);
var pdfController = new PdfController(
_instanceClient.Object,
@@ -306,7 +313,8 @@ public async Task Request_In_TT02_Should_Ignore_Frontend_Version()
_profile.Object,
pdfGeneratorClient,
_pdfGeneratorSettingsOptions,
- generalSettingsOptions
+ generalSettingsOptions,
+ _logger.Object
);
var pdfController = new PdfController(
_instanceClient.Object,
diff --git a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs
index 421bfc166..e0626e3f7 100644
--- a/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs
+++ b/test/Altinn.App.Api.Tests/Controllers/ProcessControllerTests.cs
@@ -341,7 +341,9 @@ public async Task RunProcessNext_NonErrorValidations_ReturnsOk()
);
var pdfMock = new Mock(MockBehavior.Strict);
using var pdfReturnStream = new MemoryStream();
- pdfMock.Setup(p => p.GeneratePdf(It.IsAny(), It.IsAny())).ReturnsAsync(pdfReturnStream);
+ pdfMock
+ .Setup(p => p.GeneratePdf(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(pdfReturnStream);
OverrideServicesForThisTest = (services) =>
{
services.AddSingleton(dataValidator.Object);
@@ -366,7 +368,9 @@ public async Task RunCompleteTask_GoesToEndEvent()
{
var pdfMock = new Mock(MockBehavior.Strict);
using var pdfReturnStream = new MemoryStream();
- pdfMock.Setup(p => p.GeneratePdf(It.IsAny(), It.IsAny())).ReturnsAsync(pdfReturnStream);
+ pdfMock
+ .Setup(p => p.GeneratePdf(It.IsAny(), It.IsAny(), It.IsAny()))
+ .ReturnsAsync(pdfReturnStream);
OverrideServicesForThisTest = (services) =>
{
services.AddSingleton(pdfMock.Object);
diff --git a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs
index 6cc21dcaf..5dcdfee76 100644
--- a/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs
+++ b/test/Altinn.App.Core.Tests/Internal/Pdf/PdfServiceTests.cs
@@ -14,8 +14,10 @@
using Altinn.Platform.Profile.Models;
using Altinn.Platform.Storage.Interface.Models;
using AltinnCore.Authentication.Constants;
+using Castle.Core.Logging;
using FluentAssertions;
using Microsoft.AspNetCore.Http;
+using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Primitives;
using Moq;
@@ -43,6 +45,8 @@ public class PdfServiceTests
private readonly Mock _userTokenProvider;
+ private readonly Mock> _logger = new();
+
public PdfServiceTests()
{
var resource = new TextResource()
@@ -134,7 +138,9 @@ public async Task GenerateAndStorePdf()
{
// Arrange
TelemetrySink telemetrySink = new();
- _pdfGeneratorClient.Setup(s => s.GeneratePdf(It.IsAny(), It.IsAny()));
+ _pdfGeneratorClient.Setup(s =>
+ s.GeneratePdf(It.IsAny(), It.IsAny(), It.IsAny())
+ );
_generalSettingsOptions.Value.ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}";
var target = SetupPdfService(
@@ -164,6 +170,7 @@ public async Task GenerateAndStorePdf()
&& u.AbsoluteUri.Contains(instance.AppId)
&& u.AbsoluteUri.Contains(instance.Id)
),
+ It.Is(s => s == null),
It.IsAny()
),
Times.Once
@@ -189,7 +196,9 @@ public async Task GenerateAndStorePdf()
public async Task GenerateAndStorePdf_with_generatedFrom()
{
// Arrange
- _pdfGeneratorClient.Setup(s => s.GeneratePdf(It.IsAny(), It.IsAny()));
+ _pdfGeneratorClient.Setup(s =>
+ s.GeneratePdf(It.IsAny(), It.IsAny(), It.IsAny())
+ );
_generalSettingsOptions.Value.ExternalAppBaseUrl = "https://{org}.apps.{hostName}/{org}/{app}";
@@ -228,6 +237,7 @@ public async Task GenerateAndStorePdf_with_generatedFrom()
&& u.AbsoluteUri.Contains(instance.AppId)
&& u.AbsoluteUri.Contains(instance.Id)
),
+ It.Is(s => s == null),
It.IsAny()
),
Times.Once
@@ -394,6 +404,7 @@ private PdfService SetupPdfService(
pdfGeneratorClient?.Object ?? _pdfGeneratorClient.Object,
pdfGeneratorSettingsOptions ?? _pdfGeneratorSettingsOptions,
generalSettingsOptions ?? _generalSettingsOptions,
+ _logger.Object,
telemetrySink?.Object
);
}