diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java index 21a547c..928d389 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRenderer.java @@ -16,6 +16,9 @@ import java.io.File; import java.io.IOException; import java.io.OutputStream; +import java.io.InputStream; +import java.nio.file.Path; +import java.nio.file.Paths; import java.util.Arrays; import java.util.Base64; import java.util.HashMap; @@ -48,12 +51,16 @@ import org.openmrs.module.reporting.report.renderer.RenderingException; import org.openmrs.module.reporting.report.renderer.ReportDesignRenderer; import org.openmrs.module.reporting.report.renderer.ReportRenderer; +import org.openmrs.util.OpenmrsClassLoader; import org.openmrs.util.OpenmrsUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; import org.w3c.dom.Document; import org.w3c.dom.Element; import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.IOUtils; /** * ReportRenderer that renders to a default XML format @@ -63,6 +70,10 @@ @Localized("patientdocuments.patientIdStickerXmlReportRenderer") public class PatientIdStickerXmlReportRenderer extends ReportDesignRenderer { + private static final Logger log = LoggerFactory.getLogger(PatientIdStickerXmlReportRenderer.class); + + private static final String DEFAULT_LOGO_CLASSPATH = "web/module/resources/openmrs_logo_white_large.png"; + private MessageSourceService mss; private InitializerService initializerService; @@ -116,10 +127,6 @@ protected String getStringValue(DataSetRow row, DataSetColumn column) { @Override public void render(ReportData results, String argument, OutputStream out) throws IOException, RenderingException { - render(results, argument, out, null); - } - - public void render(ReportData results, String argument, OutputStream out, byte[] defaultLogoBytes) throws IOException, RenderingException { DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance(); DocumentBuilder docBuilder; try { @@ -144,7 +151,7 @@ public void render(ReportData results, String argument, OutputStream out, byte[] Element templatePIDElement = createStickerTemplate(doc); // Handle header configuration - configureHeader(doc, templatePIDElement, defaultLogoBytes); + configureHeader(doc, templatePIDElement); // Process data set fields processDataSetFields(results, doc, templatePIDElement); @@ -217,11 +224,11 @@ private Element createStickerTemplate(Document doc) { return templatePIDElement; } - private void configureHeader(Document doc, Element templatePIDElement, byte[] defaultLogoBytes) { + private void configureHeader(Document doc, Element templatePIDElement) { Element header = doc.createElement("header"); - // Handle logo if configured + // Handle logo if configured String logoUrlPath = getInitializerService().getValueFromKey("report.patientIdSticker.logourl"); - configureLogo(doc, header, logoUrlPath, defaultLogoBytes); + configureLogo(doc, header, logoUrlPath); boolean useHeader = Boolean.TRUE.equals(getInitializerService().getBooleanFromKey("report.patientIdSticker.header")); if (useHeader) { @@ -243,51 +250,122 @@ private void configureHeader(Document doc, Element templatePIDElement, byte[] de templatePIDElement.appendChild(i18nStrings); } - /** - * Configures the logo for the sticker document. - * - * Logo resolution priority: - * 1. Custom logo from absolute filesystem path resolved under {@code OPENMRS_APPLICATION_DATA_DIRECTORY} - * 2. Default OpenMRS logo as base64 data URI - * - * @param doc The XML document - * @param header The header element to append the logo to - * @param logoUrlPath User-configured logo path (can be null, absolute, or relative) - * @throws RenderingException if no valid logo can be found - */ - private void configureLogo(Document doc, Element header, String logoUrlPath, byte[] defaultLogoBytes) { - String logoPath = ""; + /** + * Configures the logo for the sticker document. + * + * Loads a custom logo from {@code logoUrlPath} (relative to the {@code OPENMRS_APPLICATION_DATA_DIRECTORY}. + * If not found, falls back to the OpenMRS logo from the classpath. + * + * @param doc The XML document + * @param header The header element to append the logo to + * @param logoUrlPath User-configured logo path (must be relative to app data dir) + */ + private void configureLogo(Document doc, Element header, String logoUrlPath) { + String logoContent = null; - try { - // 1. Try custom logo - if (isNotBlank(logoUrlPath)) { - File logoFile = new File(logoUrlPath); - if (!logoFile.isAbsolute()) { - File appDataDir = OpenmrsUtil.getDirectoryInApplicationDataDirectory(""); - logoFile = new File(appDataDir, logoUrlPath); - } - if (logoFile.exists() && logoFile.canRead()) { - logoPath = logoFile.getAbsolutePath(); + // 1. Try custom logo + if (isNotBlank(logoUrlPath)) { + File logoFile = resolveSecureLogoPath(logoUrlPath); + if (logoFile != null && logoFile.exists() && logoFile.canRead() && logoFile.isFile()) { + try { + byte[] customLogoBytes = OpenmrsUtil.getFileAsBytes(logoFile); + if (customLogoBytes != null && customLogoBytes.length > 0) { + String base64Image = Base64.getEncoder().encodeToString(customLogoBytes); + logoContent = "data:image/png;base64," + base64Image; + } + } catch (IOException e) { + log.error("Failed to load custom logo from file: {}", logoFile.getAbsolutePath(), e); } } - - // 2. Fall back to default logo - if (isBlank(logoPath) && defaultLogoBytes != null && defaultLogoBytes.length > 0) { + } + + if (isBlank(logoContent)) { + byte[] defaultLogoBytes = loadDefaultLogoFromClasspath(); + if (defaultLogoBytes != null && defaultLogoBytes.length > 0) { String base64Image = Base64.getEncoder().encodeToString(defaultLogoBytes); - logoPath = "data:image/png;base64," + base64Image; + logoContent = "data:image/png;base64," + base64Image; } - } catch (Exception e) { - throw new RenderingException("Failed to configure logo", e); } - - // Create and append logo elements if valid - if (isNotBlank(logoPath)) { + + if (isNotBlank(logoContent)) { Element branding = doc.createElement("branding"); Element image = doc.createElement("logo"); - image.setTextContent(logoPath); + image.setTextContent(logoContent); branding.appendChild(image); header.appendChild(branding); } + else if (isNotBlank(logoUrlPath)) { + // If a path was provided but we could not resolve or fall back, surface an error + log.error("Failed to configure logo: unresolved path '{}' and no default provided", logoUrlPath); + } + } + + private byte[] loadDefaultLogoFromClasspath() { + try (InputStream logoStream = OpenmrsClassLoader.getInstance().getResourceAsStream(DEFAULT_LOGO_CLASSPATH)) { + if (logoStream == null) { + log.warn("Default logo not found on classpath at: {}", DEFAULT_LOGO_CLASSPATH); + return null; + } + return IOUtils.toByteArray(logoStream); + } + catch (IOException e) { + log.error("Failed to load default logo from classpath at: {}", DEFAULT_LOGO_CLASSPATH, e); + return null; + } + } + + /** + * Ensure that the supplied {@code logoUrlPath} refers to a file in the application data directory + * + * @param logoUrlPath The user-provided logo path + * @return A File object pointing to the logo if the path is valid, otherwise {@code null} + */ + protected File resolveSecureLogoPath(String logoUrlPath) { + if (isBlank(logoUrlPath)) { + return null; + } + + final File appDataDir = OpenmrsUtil.getApplicationDataDirectoryAsFile(); + try { + final Path appDataPath = appDataDir.toPath().toRealPath(); + final Path logoPath = Paths.get(logoUrlPath); + + // Reject absolute paths + if (logoPath.isAbsolute()) { + log.error("Absolute paths are not allowed for logo files: {}", logoUrlPath); + return null; + } + + // For relative paths, detect path traversal by comparing absolute and normalized paths + final Path logoAbsolutePath = logoPath.toAbsolutePath(); + final Path logoNormalizedPath = logoAbsolutePath.normalize(); + + if (!logoAbsolutePath.equals(logoNormalizedPath)) { + log.error("Path traversal detected in logo path: {}", logoUrlPath); + return null; + } + + // Resolve against application data directory and validate real location + final Path resolvedLogoPath = appDataPath.resolve(logoUrlPath).normalize(); + final Path resolvedLogoRealPath = resolvedLogoPath.toRealPath(); + + if (!isPathWithinAppDataDirectory(resolvedLogoRealPath, appDataPath)) { + log.error("Logo path must be within the application data directory: {}", logoUrlPath); + return null; + } + + return resolvedLogoRealPath.toFile(); + } catch (IllegalArgumentException e) { + log.error("Invalid logo path: " + logoUrlPath, e); + return null; + } catch (IOException e) { + log.error("Failed to access logo file: {}", logoUrlPath, e); + return null; + } + } + + private boolean isPathWithinAppDataDirectory(Path path, Path appDataPath) { + return path.startsWith(appDataPath); } private Map createConfigKeyMap() { diff --git a/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java b/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java index fb3eb02..ece961f 100644 --- a/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java +++ b/api/src/main/java/org/openmrs/module/patientdocuments/reports/PatientIdStickerPdfReport.java @@ -58,12 +58,12 @@ public class PatientIdStickerPdfReport { @Autowired private InitializerService initializerService; - public byte[] generatePdf(Patient patient, byte[] defaultLogoBytes) throws RuntimeException { + public byte[] generatePdf(Patient patient) throws RuntimeException { validatePatientAndPrivileges(patient); try { ReportData reportData = createReportData(patient); - byte[] xmlBytes = renderReportToXml(reportData, defaultLogoBytes); + byte[] xmlBytes = renderReportToXml(reportData); return transformXmlToPdf(xmlBytes); } catch (Exception e) { @@ -95,10 +95,10 @@ private ReportData createReportData(Patient patient) throws EvaluationException return reportData; } - private byte[] renderReportToXml(ReportData reportData, byte[] defaultLogoBytes) throws IOException { + private byte[] renderReportToXml(ReportData reportData) throws IOException { PatientIdStickerXmlReportRenderer renderer = new PatientIdStickerXmlReportRenderer(); try (ByteArrayOutputStream xmlOutputStream = new ByteArrayOutputStream()) { - renderer.render(reportData, null, xmlOutputStream, defaultLogoBytes); + renderer.render(reportData, null, xmlOutputStream); return xmlOutputStream.toByteArray(); } } diff --git a/api/src/test/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRendererTest.java b/api/src/test/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRendererTest.java index 79dd159..a72a037 100644 --- a/api/src/test/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRendererTest.java +++ b/api/src/test/java/org/openmrs/module/patientdocuments/renderer/PatientIdStickerXmlReportRendererTest.java @@ -15,10 +15,15 @@ import org.openmrs.Patient; import org.openmrs.module.patientdocuments.reports.PatientIdStickerPdfReport; import org.openmrs.test.jupiter.BaseModuleContextSensitiveTest; +import org.openmrs.util.OpenmrsUtil; import org.springframework.beans.factory.annotation.Autowired; import org.openmrs.module.reporting.report.manager.ReportManagerUtil; import org.openmrs.module.patientdocuments.reports.PatientIdStickerReportManager; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; + public class PatientIdStickerXmlReportRendererTest extends BaseModuleContextSensitiveTest { @Autowired @@ -34,8 +39,40 @@ public void setup() throws Exception { public void generatePdf_shouldThrowWhenPatientIsMissing() throws Exception { Patient badPatient = null; Assertions.assertThrows(IllegalArgumentException.class, () -> { - pdfReport.generatePdf(badPatient, null); + pdfReport.generatePdf(badPatient); }); } + @Test + public void resolveSecureLogoPath_shouldReturnFileWithinAppDataDirectory() throws Exception { + PatientIdStickerXmlReportRenderer renderer = new PatientIdStickerXmlReportRenderer(); + Path logosDirectory = Files.createDirectories(OpenmrsUtil.getApplicationDataDirectoryAsFile().toPath().resolve("logos")); + Path logoFile = logosDirectory.resolve("custom-logo.png"); + Files.write(logoFile, "image-data".getBytes()); + + File resolvedLogoFile = renderer.resolveSecureLogoPath("logos/custom-logo.png"); + + Assertions.assertNotNull(resolvedLogoFile, "Expected logo file within app data directory to be resolved"); + Assertions.assertEquals(logoFile.toRealPath(), resolvedLogoFile.toPath().toRealPath()); + } + + @Test + public void resolveSecureLogoPath_shouldRejectPathTraversalAttempts() throws Exception { + PatientIdStickerXmlReportRenderer renderer = new PatientIdStickerXmlReportRenderer(); + + File resolvedLogoFile = renderer.resolveSecureLogoPath("../malicious-logo.png"); + + Assertions.assertNull(resolvedLogoFile, "Path traversal attempts must be rejected"); + } + + @Test + public void resolveSecureLogoPath_shouldRejectAbsolutePaths() throws Exception { + PatientIdStickerXmlReportRenderer renderer = new PatientIdStickerXmlReportRenderer(); + Path outsideLogo = Files.createTempFile("absolute-path-logo", ".png"); + + File resolvedLogoFile = renderer.resolveSecureLogoPath(outsideLogo.toString()); + + Assertions.assertNull(resolvedLogoFile, "Absolute paths must be rejected"); + Files.deleteIfExists(outsideLogo); + } } diff --git a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java index 62db228..5469a7a 100644 --- a/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java +++ b/omod/src/main/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportController.java @@ -12,14 +12,8 @@ import static org.openmrs.module.patientdocuments.common.PatientDocumentsConstants.PATIENT_ID_STICKER_ID; import static org.openmrs.module.patientdocuments.common.PatientDocumentsConstants.MODULE_ARTIFACT_ID; -import java.io.IOException; -import java.io.InputStream; - -import javax.servlet.ServletContext; import javax.servlet.http.HttpServletResponse; -import javax.servlet.http.HttpServletRequest; -import org.apache.commons.io.IOUtils; import org.openmrs.Patient; import org.openmrs.api.PatientService; import org.openmrs.module.patientdocuments.reports.PatientIdStickerPdfReport; @@ -43,8 +37,6 @@ public class PatientIdStickerDataPdfExportController extends BaseRestController { private static final Logger logger = LoggerFactory.getLogger(PatientIdStickerDataPdfExportController.class); - - private static final String DEFAULT_LOGO_CLASSPATH = "/images/openmrs_logo_white_large.png"; private PatientIdStickerPdfReport pdfReport; @@ -57,10 +49,9 @@ public PatientIdStickerDataPdfExportController(@Qualifier("patientService") Pati this.pdfReport = pdfReport; } - private ResponseEntity writeResponse(Patient patient, boolean inline, ServletContext servletContext) { + private ResponseEntity writeResponse(Patient patient, boolean inline) { try { - byte[] defaultLogoBytes = loadDefaultLogo(servletContext); - byte[] pdfBytes = pdfReport.generatePdf(patient, defaultLogoBytes); + byte[] pdfBytes = pdfReport.generatePdf(patient); HttpHeaders headers = new HttpHeaders(); headers.set("Content-Type", "application/pdf"); @@ -76,27 +67,9 @@ private ResponseEntity writeResponse(Patient patient, boolean inline, Se .body("Error generating PDF".getBytes()); } } - - private byte[] loadDefaultLogo(ServletContext servletContext) { - if (servletContext == null) { - return null; - } - - try (InputStream logoStream = servletContext.getResourceAsStream(DEFAULT_LOGO_CLASSPATH)) { - if (logoStream == null) { - logger.warn("Logo file not found at: {}", DEFAULT_LOGO_CLASSPATH); - return null; - } - return IOUtils.toByteArray(logoStream); - } catch (IOException e) { - logger.error("Failed to load logo from: {}", DEFAULT_LOGO_CLASSPATH, e); - return null; - } - } @RequestMapping(method = RequestMethod.GET) public ResponseEntity getPatientIdSticker(HttpServletResponse response, - HttpServletRequest request, @RequestParam(value = "patientUuid") String patientUuid, @RequestParam(value = "inline", required = false, defaultValue = "true") boolean inline) { @@ -106,7 +79,6 @@ public ResponseEntity getPatientIdSticker(HttpServletResponse response, return null; } - ServletContext servletContext = request.getSession().getServletContext(); - return writeResponse(patient, inline, servletContext); + return writeResponse(patient, inline); } } diff --git a/omod/src/main/webapp/resources/openmrs_logo_white_large.png b/omod/src/main/webapp/resources/openmrs_logo_white_large.png new file mode 100644 index 0000000..2a777da Binary files /dev/null and b/omod/src/main/webapp/resources/openmrs_logo_white_large.png differ diff --git a/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java b/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java index 4318114..d12ed53 100644 --- a/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java +++ b/omod/src/test/java/org/openmrs/module/patientdocuments/web/rest/controller/PatientIdStickerDataPdfExportControllerTest.java @@ -21,7 +21,6 @@ import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; -import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.apache.pdfbox.pdmodel.PDDocument; import org.apache.pdfbox.text.PDFTextStripper; @@ -69,9 +68,8 @@ public void setup() throws Exception { public void getPatientIdSticker_shouldReturnValidPdfForEnglishLocale() throws Exception { Context.setLocale(Locale.ENGLISH); MockHttpServletResponse response = new MockHttpServletResponse(); - MockHttpServletRequest request = new MockHttpServletRequest(); - ResponseEntity result = patientStickerController.getPatientIdSticker(response, request, TEST_PATIENT_UUID, false); + ResponseEntity result = patientStickerController.getPatientIdSticker(response, TEST_PATIENT_UUID, false); byte[] pdfContent = result.getBody(); assertNotNull(pdfContent); @@ -94,9 +92,8 @@ public void getPatientIdSticker_shouldReturnValidPdfForEnglishLocale() throws Ex public void getPatientIdSticker_shouldReturnValidPdfForArabicLocale() throws Exception { Context.setLocale(new Locale("ar", "AR")); MockHttpServletResponse response = new MockHttpServletResponse(); - MockHttpServletRequest request = new MockHttpServletRequest(); - ResponseEntity result = patientStickerController.getPatientIdSticker(response, request, TEST_PATIENT_UUID, false); + ResponseEntity result = patientStickerController.getPatientIdSticker(response, TEST_PATIENT_UUID, false); byte[] pdfContent = result.getBody(); assertNotNull(pdfContent); @@ -117,11 +114,10 @@ public void getPatientIdSticker_shouldReturnValidPdfForArabicLocale() throws Exc @Test public void getPatientIdSticker_shouldReturn404ForInvalidPatient() throws Exception { MockHttpServletResponse response = new MockHttpServletResponse(); - MockHttpServletRequest request = new MockHttpServletRequest(); String invalidUuid = "invalid-uuid"; - ResponseEntity responseEntity = patientStickerController.getPatientIdSticker(response, request, invalidUuid, false); + ResponseEntity responseEntity = patientStickerController.getPatientIdSticker(response, invalidUuid, false); assertNull("Response entity should be null", responseEntity); assertEquals("Should return HTTP 404 status", HttpStatus.NOT_FOUND.value(), response.getStatus()); diff --git a/readme/PatientIdSticker.md b/readme/PatientIdSticker.md index 5d43193..f5c2297 100644 --- a/readme/PatientIdSticker.md +++ b/readme/PatientIdSticker.md @@ -28,7 +28,7 @@ These flags control which patient information is displayed on each sticker. | Key | Type | Description | |---------------------------------------------------------------|---------|--------------------------------------------------| | `report.patientIdSticker.stylesheet` | String | XSL stylesheet to use for rendering stickers | -| `report.patientIdSticker.logourl` | String | Logo path or URL displayed on the sticker (supports a path relative to `OPENMRS_APPLICATION_DATA_DIRECTORY`) | +| `report.patientIdSticker.logourl` | String | Logo path displayed on the sticker (relative must be within the application data directory.) | | `report.patientIdSticker.header` | Boolean | Show a header section on each sticker | | `report.patientIdSticker.barcode` | Boolean | Show a barcode section on each sticker | | `report.patientIdSticker.pages` | Number | Number of sticker pages to generate | @@ -58,7 +58,7 @@ These flags control which patient information is displayed on each sticker. "report.patientIdSticker.fields.gender": "true", "report.patientIdSticker.fields.fulladdress": "true", "report.patientIdSticker.stylesheet": "patientIdStickerFopStylesheet.xsl", - "report.patientIdSticker.logourl": "http://example.com/logo.png", + "report.patientIdSticker.logourl": "branding/logo.png", "report.patientIdSticker.pages": "10", "report.patientIdSticker.header": "true", "report.patientIdSticker.barcode": "true", @@ -93,7 +93,6 @@ The module supports internationalization through message properties. Field label - The secondary identifier type can be configured using `report.patientIdSticker.fields.identifier.secondary.type` if needed. - Available stylesheets include `patientIdStickerFopStylesheet.xsl` (default) and `msfStickerFopStylesheet.xsl` for MSF-specific layouts. - Logo resolution rules: - - If `report.patientIdSticker.logourl` is a relative path, it is resolved under `OPENMRS_APPLICATION_DATA_DIRECTORY` (e.g., `/openmrs/data/my_custom_logo.png`). - - If no logo is configured or the configured logo is unavailable, the default OpenMRS logo is loaded from the servlet context. - - Supported formats: an absolute filesystem path (for a custom logo) or a base64-encoded data URI (for the default OpenMRS logo), both of which are accepted by the renderer/XSL-FO processor. + - `report.patientIdSticker.logourl` should be a path relative to the`OPENMRS_APPLICATION_DATA_DIRECTORY` (e.g., `branding/my_custom_logo.png`). + - If no logo is configured or the configured logo is unavailable, the OpenMRS logo will be used as a default. - The barcode is generated from the preferred patient identifier when barcode is enabled. \ No newline at end of file diff --git a/readme/PatientIdStickerXSL.md b/readme/PatientIdStickerXSL.md index 4d9173f..5913d66 100644 --- a/readme/PatientIdStickerXSL.md +++ b/readme/PatientIdStickerXSL.md @@ -164,9 +164,9 @@ The optional header section can contain: - An organizational logo on the left (from a file path under `OPENMRS_APPLICATION_DATA_DIRECTORY`) - Custom header text on the right - Logo handling behavior: - - Relative paths are resolved under `OPENMRS_APPLICATION_DATA_DIRECTORY` - - If none configured or missing, the default OpenMRS logo is loaded from the servlet context. - - Supported formats: absolute filesystem path (custom logo) or base64 data URI (default logo). The XSL-FO processor can handle both. + - Logo path is resolved relative to `OPENMRS_APPLICATION_DATA_DIRECTORY`. + - If none configured or missing, the default OpenMRS logo is loaded from the classpath + - Supported formats: PNG only. ### Internationalization Section @@ -208,7 +208,7 @@ The stylesheet includes several responsive design elements: - **Demographic Grouping**: In MSF layout, groups Gender, DOB, and Age fields in a single row - **Secondary Identifier**: Special handling for secondary patient identifiers - **Internationalization**: Support for translated field labels and messages -- **Logo Handling**: Pulled from the `OPENMRS_APPLICATION_DATA_DIRECTORY` or servlet context for the default OpenMRS logo. +- **Logo Handling**: A custom logo can be provided as a PNG file stored in the application data directory. By default, this will use the OpenMRS logo. ## Technical Requirements @@ -278,6 +278,6 @@ The stylesheet includes several responsive design elements: - Configuration is managed through the Initializer module - Field visibility is controlled by boolean configuration properties - Secondary identifier type is specified by UUID in configuration -- Logo input is either an absolute filesystem path or a base64-encoded data URI (e.g., `data:image/png;base64,...`). Relative paths are resolved under `OPENMRS_APPLICATION_DATA_DIRECTORY`. +- Logo input is a relative filesystem path resolved under `OPENMRS_APPLICATION_DATA_DIRECTORY`. - Barcode generation uses the preferred patient identifier - Multiple stickers can be generated based on the `pages` configuration