From 04f90be122ca22993e8d2d3f410083e159da444d Mon Sep 17 00:00:00 2001 From: bsayan2 Date: Wed, 17 Dec 2025 13:29:24 +0530 Subject: [PATCH] ACS-10505 modifying libreoffice workdir and user's registryconfig file to block external links --- engines/aio/Dockerfile | 5 + .../main/resources/application-default.yaml | 5 +- engines/libreoffice/Dockerfile | 5 + .../patch/LibreOfficeProfileManager.java | 276 ++++++++++++++++++ .../JodConverterSharedInstance.java | 60 +++- .../transformers/LibreOfficeTransformer.java | 13 +- .../main/resources/application-default.yaml | 9 +- .../src/main/resources/lo-registry-patch.json | 12 + pom.xml | 7 +- 9 files changed, 380 insertions(+), 12 deletions(-) create mode 100644 engines/libreoffice/src/main/java/org/alfresco/transform/libreoffice/patch/LibreOfficeProfileManager.java create mode 100644 engines/libreoffice/src/main/resources/lo-registry-patch.json diff --git a/engines/aio/Dockerfile b/engines/aio/Dockerfile index be53026d..4e42b72a 100644 --- a/engines/aio/Dockerfile +++ b/engines/aio/Dockerfile @@ -85,6 +85,11 @@ RUN groupadd -g ${GROUPID} ${GROUPNAME} && \ useradd -u ${USERID} -G ${GROUPNAME} ${AIOUSERNAME} && \ chgrp -R ${GROUPNAME} /usr/bin/${env.project_artifactId}.jar +RUN mkdir -p /opt/libreoffice-profile/workDir && \ + mkdir -p /opt/libreoffice-profile/templateProfileDir/user && \ + chown -R ${USERID}:${GROUPID} /opt/libreoffice-profile && \ + chmod -R 775 /opt/libreoffice-profile + EXPOSE 8090 USER ${AIOUSERNAME} diff --git a/engines/aio/src/main/resources/application-default.yaml b/engines/aio/src/main/resources/application-default.yaml index 800a44c6..b737719f 100644 --- a/engines/aio/src/main/resources/application-default.yaml +++ b/engines/aio/src/main/resources/application-default.yaml @@ -10,8 +10,11 @@ transform: maxTasksPerProcess: ${LIBREOFFICE_MAX_TASKS_PER_PROCESS:200} timeout: ${LIBREOFFICE_TIMEOUT:1200000} portNumbers: ${LIBREOFFICE_PORT_NUMBERS:8100} - templateProfileDir: ${LIBREOFFICE_TEMPLATE_PROFILE_DIR:} isEnabled: ${LIBREOFFICE_IS_ENABLED:true} + enableTemplateProfile: false + templateProfileDir: ${LIBREOFFICE_TEMPLATE_PROFILE_DIR:/opt/libreoffice-profile/templateProfileDir} + workdir: ${LIBREOFFICE_WORK_PROFILE_DIR:/opt/libreoffice-profile/workDir} + disableExternalLinks: true imagemagick: root: ${IMAGEMAGICK_ROOT:/usr/lib64/ImageMagick-7.1.2} dyn: ${IMAGEMAGICK_DYN:/usr/lib64/ImageMagick-7.1.2/lib} diff --git a/engines/libreoffice/Dockerfile b/engines/libreoffice/Dockerfile index 52da1851..ab4dbcc8 100644 --- a/engines/libreoffice/Dockerfile +++ b/engines/libreoffice/Dockerfile @@ -46,6 +46,11 @@ RUN groupadd -g ${GROUPID} ${GROUPNAME} && \ useradd -u ${USERID} -G ${GROUPNAME} ${LIBREUSERNAME} && \ chgrp -R ${GROUPNAME} /usr/bin/${env.project_artifactId}.jar +RUN mkdir -p /opt/libreoffice-profile/workDir && \ + mkdir -p /opt/libreoffice-profile/templateProfileDir/user && \ + chown -R ${USERID}:${GROUPID} /opt/libreoffice-profile && \ + chmod -R 775 /opt/libreoffice-profile + EXPOSE 8090 USER ${LIBREUSERNAME} diff --git a/engines/libreoffice/src/main/java/org/alfresco/transform/libreoffice/patch/LibreOfficeProfileManager.java b/engines/libreoffice/src/main/java/org/alfresco/transform/libreoffice/patch/LibreOfficeProfileManager.java new file mode 100644 index 00000000..d3b56b96 --- /dev/null +++ b/engines/libreoffice/src/main/java/org/alfresco/transform/libreoffice/patch/LibreOfficeProfileManager.java @@ -0,0 +1,276 @@ +/* + * #%L + * Alfresco Transform Core + * %% + * Copyright (C) 2005 - 2025 Alfresco Software Limited + * %% + * This file is part of the Alfresco software. + * - + * If the software was purchased under a paid Alfresco license, the terms of + * the paid license agreement will prevail. Otherwise, the software is + * provided under the following open source license terms: + * - + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * - + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * - + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + * #L% + */ + +package org.alfresco.transform.libreoffice.patch; + +import java.io.File; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.commons.io.FileUtils; +import org.apache.commons.io.IOUtils; +import org.artofsolving.jodconverter.OfficeDocumentConverter; +import org.artofsolving.jodconverter.office.OfficeManager; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Manages LibreOffice template user profile and work directory, including apply patches to disable external link updates. + * + * @author Sayan Bhattacharya + */ + +public class LibreOfficeProfileManager +{ + + private static final Logger logger = LoggerFactory.getLogger(LibreOfficeProfileManager.class); + private static final String PROBE_RESOURCE = "/probe.doc"; + private static final String PATCH_RESOURCE = "/disable-external-link-patch.txt"; + private static final String REGISTRY_FILE = "registrymodifications.xcu"; + private static final String USER_PROFILE_DIR = "user"; + + private final File workDir; + private final File templateProfileDir; + private final OfficeManager officeManager; + private final boolean disableExternalLinks; + + public LibreOfficeProfileManager(File workDir, File templateProfileDir, + OfficeManager officeManager, boolean disableExternalLinks) + { + this.workDir = workDir; + this.templateProfileDir = templateProfileDir; + this.officeManager = officeManager; + this.disableExternalLinks = disableExternalLinks; + } + + public void setupTemplateUserProfileWithProbeTransformation() throws Exception + { + if (workDir == null || templateProfileDir == null) + { + return; + } + + OfficeDocumentConverter converter = new OfficeDocumentConverter(officeManager); + convertProbeDocument(converter); + copyUserProfile(); + + if (disableExternalLinks) + { + patchLibreOfficeRegistry(); + } + } + + /** + * Performs a probe document conversion to ensure the user profile is created + * + * @param converter + * @throws Exception + */ + private void convertProbeDocument(OfficeDocumentConverter converter) throws Exception + { + InputStream probeInput = getClass().getResourceAsStream(PROBE_RESOURCE); + if (probeInput == null) + { + throw new IllegalStateException("probe.docx resource not found!"); + } + + File tempProbeFile = new File(workDir, "probe.doc"); + FileUtils.copyInputStreamToFile(probeInput, tempProbeFile); + converter.convert(tempProbeFile, new File(workDir, "probeoutput.pdf")); + } + + private void copyUserProfile() throws Exception + { + File officeUserProfile = findLibreOfficeUserProfile(workDir); + if (officeUserProfile != null) + { + File destination = new File(templateProfileDir, officeUserProfile.getName()); + FileUtils.copyDirectory(officeUserProfile, destination); + } + } + + /** + * Patches the LibreOffice user profile to disable external link updates. + * + * @throws Exception + */ + private void patchLibreOfficeRegistry() throws Exception + { + File userProfileDir = findLibreOfficeUserProfile(templateProfileDir); + if (userProfileDir == null) + { + throw new IllegalStateException("Cannot find LO user profile to patch"); + } + + File registry = new File(userProfileDir, REGISTRY_FILE); + if (!registry.exists()) + { + throw new IllegalStateException(REGISTRY_FILE + " not found!"); + } + + String registryContent = FileUtils.readFileToString(registry, StandardCharsets.UTF_8); + List patchItems = readPatchItemsFromJson(); + + // Remove existing matching items + for (PatchItem item : patchItems) + { + String pattern = String.format( + "]*>.*?", + Pattern.quote(item.path), + Pattern.quote(item.propName)); + registryContent = registryContent.replaceAll(pattern, ""); + } + + // Insert the new patch before closing tag + String patch = generatePatchXml(patchItems); + registryContent = registryContent.replace("", " " + patch + "\n"); + + FileUtils.writeStringToFile(registry, registryContent, StandardCharsets.UTF_8); + } + + private String readPatchFromResource() throws Exception + { + InputStream inputStream = getClass().getResourceAsStream(PATCH_RESOURCE); + if (inputStream == null) + { + throw new IllegalStateException("disable-external-link-patch.txt resource not found!"); + } + return IOUtils.toString(inputStream, StandardCharsets.UTF_8).trim(); + } + + private File findLibreOfficeUserProfile(File dir) + { + File userDir = findDirectChild(dir, USER_PROFILE_DIR); + if (userDir != null) + { + return userDir; + } + return findUserInJodConverterDirs(dir); + } + + private File findDirectChild(File dir, String name) + { + File[] children = dir.listFiles(File::isDirectory); + if (children != null) + { + for (File child : children) + { + if (name.equals(child.getName())) + { + return child; + } + } + } + return null; + } + + private File findUserInJodConverterDirs(File dir) + { + File[] jodDirs = dir.listFiles(f -> f.isDirectory() && f.getName().startsWith(".jodconverter_")); + if (jodDirs == null) + { + return null; + } + + for (File d : jodDirs) + { + File user = new File(d, "user"); + if (user.exists()) + { + return user; + } + } + return null; + } + + private List readPatchItemsFromJson() throws Exception + { + InputStream inputStream = getClass().getResourceAsStream("/disable-external-link-patch.json"); + if (inputStream == null) + { + throw new IllegalStateException("disable-external-link-patch.json not found!"); + } + + String jsonContent = IOUtils.toString(inputStream, StandardCharsets.UTF_8); + ObjectMapper mapper = new ObjectMapper(); + JsonNode root = mapper.readTree(jsonContent); + + JsonNode items = root.get("items"); + if (items == null || !items.isArray() || items.size() == 0) + { + throw new IllegalStateException("JSON 'items' array is missing or empty!"); + } + + List patchItems = new ArrayList<>(); + for (JsonNode item : items) + { + String path = item.get("oor:path").asText(); + JsonNode prop = item.get("prop"); + String name = prop.get("oor:name").asText(); + String op = prop.get("oor:op").asText(); + String value = prop.get("value").asText(); + + patchItems.add(new PatchItem(path, name, op, value)); + } + + return patchItems; + } + + private String generatePatchXml(List items) + { + StringBuilder xml = new StringBuilder(); + for (PatchItem item : items) + { + xml.append(""); + xml.append(""); + xml.append("").append(item.value).append(""); + xml.append(""); + } + return xml.toString(); + } + + private class PatchItem + { + String path; + String propName; + String op; + String value; + + PatchItem(String path, String propName, String op, String value) + { + this.path = path; + this.propName = propName; + this.op = op; + this.value = value; + } + } +} diff --git a/engines/libreoffice/src/main/java/org/alfresco/transform/libreoffice/transformers/JodConverterSharedInstance.java b/engines/libreoffice/src/main/java/org/alfresco/transform/libreoffice/transformers/JodConverterSharedInstance.java index f9fff41f..9498d6e3 100644 --- a/engines/libreoffice/src/main/java/org/alfresco/transform/libreoffice/transformers/JodConverterSharedInstance.java +++ b/engines/libreoffice/src/main/java/org/alfresco/transform/libreoffice/transformers/JodConverterSharedInstance.java @@ -36,12 +36,15 @@ import java.util.StringTokenizer; import jakarta.annotation.PostConstruct; import jakarta.annotation.PreDestroy; +import org.apache.commons.lang3.StringUtils; import org.artofsolving.jodconverter.office.DefaultOfficeManagerConfiguration; import org.artofsolving.jodconverter.office.OfficeException; import org.artofsolving.jodconverter.office.OfficeManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.alfresco.transform.libreoffice.patch.LibreOfficeProfileManager; + ///////// THIS FILE WAS A COPY OF THE CODE IN alfresco-repository ///////////// /** @@ -75,12 +78,14 @@ public class JodConverterSharedInstance implements JodConverter private Long taskExecutionTimeout; private Long taskQueueTimeout; private File templateProfileDir; + private File workDir; private Boolean enabled; private Long connectTimeout; private String deprecatedOooExe; private Boolean deprecatedOooEnabled; private int[] deprecatedOooPortNumbers; + private boolean disableExternalLinks; void setMaxTasksPerProcess(String maxTasksPerProcess) { @@ -163,10 +168,38 @@ public class JodConverterSharedInstance implements JodConverter throw new RuntimeException( "OpenOffice template profile directory " + templateProfileDir + " does not exist."); } + + // //making sure 'user' subdir exists + // Optional userFile = Arrays.stream(requireNonNull(tmp.listFiles())) + // .filter(File::isDirectory) + // .filter(f->f.getName().equals("user")) + // .findAny(); + // if(userFile.isEmpty()) { + // File userDir = new File(tmp, "user"); + // } + this.templateProfileDir = tmp; } } + void setWorkDir(String workDir) + { + if (StringUtils.isBlank(workDir)) + { + this.workDir = null; + } + else + { + File tmp = new File(workDir); + if (!tmp.isDirectory()) + { + throw new RuntimeException( + "OpenOffice work directory " + workDir + " does not exist."); + } + this.workDir = tmp; + } + } + void setTaskQueueTimeout(String taskQueueTimeout) { this.taskQueueTimeout = parseStringForLong(taskQueueTimeout.trim()); @@ -270,7 +303,7 @@ public class JodConverterSharedInstance implements JodConverter } /* (non-Javadoc) - * + * * @see org.alfresco.repo.content.JodConverter#isAvailable() */ public boolean isAvailable() { @@ -278,7 +311,7 @@ public class JodConverterSharedInstance implements JodConverter } /* (non-Javadoc) - * + * * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ @PostConstruct public void afterPropertiesSet() @@ -346,13 +379,26 @@ public class JodConverterSharedInstance implements JodConverter { defaultOfficeMgrConfig.setTemplateProfileDir(templateProfileDir); } + if (workDir != null) + { + defaultOfficeMgrConfig.setWorkDir(workDir); + } if (connectTimeout != null) { defaultOfficeMgrConfig.setConnectTimeout(connectTimeout); } + // Try to configure and start the JodConverter library. officeManager = defaultOfficeMgrConfig.buildOfficeManager(); officeManager.start(); + + LibreOfficeProfileManager profileManager = new LibreOfficeProfileManager( + workDir, + templateProfileDir, + officeManager, + disableExternalLinks); + profileManager.setupTemplateUserProfileWithProbeTransformation(); + } catch (IllegalStateException e) { @@ -487,7 +533,7 @@ public class JodConverterSharedInstance implements JodConverter } /* (non-Javadoc) - * + * * @see org.springframework.beans.factory.DisposableBean#destroy() */ @PreDestroy public void destroy() @@ -507,11 +553,17 @@ public class JodConverterSharedInstance implements JodConverter } /* (non-Javadoc) - * + * * @see org.alfresco.repo.content.JodConverterWorker#getOfficeManager() */ @Override public OfficeManager getOfficeManager() { return officeManager; } + + public void setDisableExternalLinks(boolean disableExternalLinks) + { + this.disableExternalLinks = disableExternalLinks; + } + } diff --git a/engines/libreoffice/src/main/java/org/alfresco/transform/libreoffice/transformers/LibreOfficeTransformer.java b/engines/libreoffice/src/main/java/org/alfresco/transform/libreoffice/transformers/LibreOfficeTransformer.java index 702dba65..717d1e5c 100644 --- a/engines/libreoffice/src/main/java/org/alfresco/transform/libreoffice/transformers/LibreOfficeTransformer.java +++ b/engines/libreoffice/src/main/java/org/alfresco/transform/libreoffice/transformers/LibreOfficeTransformer.java @@ -76,6 +76,12 @@ public class LibreOfficeTransformer implements JavaExecutor, CustomTransformerFi private String templateProfileDir; @Value("${transform.core.libreoffice.isEnabled}") private String isEnabled; + @Value("${transform.core.libreoffice.workdir}") + private String workdir; + @Value("${transform.core.libreoffice.disableExternalLinks}") + private boolean disableExternalLinks; + @Value("${transform.core.libreoffice.enableTemplateProfile}") + private boolean enableTemplateProfile; private JodConverter jodconverter; @@ -118,8 +124,13 @@ public class LibreOfficeTransformer implements JavaExecutor, CustomTransformerFi sharedInstance.setTaskQueueTimeout(timeout); sharedInstance.setConnectTimeout(timeout); sharedInstance.setPortNumbers(portNumbers); - sharedInstance.setTemplateProfileDir(templateProfileDir); sharedInstance.setEnabled(isEnabled); + if (enableTemplateProfile) + { + sharedInstance.setTemplateProfileDir(templateProfileDir); + sharedInstance.setWorkDir(workdir); + sharedInstance.setDisableExternalLinks(disableExternalLinks); + } sharedInstance.afterPropertiesSet(); } diff --git a/engines/libreoffice/src/main/resources/application-default.yaml b/engines/libreoffice/src/main/resources/application-default.yaml index d408b95b..db456ae2 100644 --- a/engines/libreoffice/src/main/resources/application-default.yaml +++ b/engines/libreoffice/src/main/resources/application-default.yaml @@ -3,9 +3,12 @@ queue: transform: core: libreoffice: - path: ${LIBREOFFICE_HOME:/opt/libreoffice7.2} + path: ${LIBREOFFICE_HOME:C:\Users\sabhattacharya\Alfresco\LibreOffice25} maxTasksPerProcess: ${LIBREOFFICE_MAX_TASKS_PER_PROCESS:200} timeout: ${LIBREOFFICE_TIMEOUT:1200000} portNumbers: ${LIBREOFFICE_PORT_NUMBERS:8100} - templateProfileDir: ${LIBREOFFICE_TEMPLATE_PROFILE_DIR:} - isEnabled: ${LIBREOFFICE_IS_ENABLED:true} \ No newline at end of file + isEnabled: ${LIBREOFFICE_IS_ENABLED:true} + enableTemplateProfile: false + templateProfileDir: ${LIBREOFFICE_TEMPLATE_PROFILE_DIR:C:\Users\sabhattacharya\Alfresco\libreoffice-profile\templateProfileDir} + workdir: ${LIBREOFFICE_WORK_PROFILE_DIR:C:\Users\sabhattacharya\Alfresco\libreoffice-profile\workDir} + disableExternalLinks: true \ No newline at end of file diff --git a/engines/libreoffice/src/main/resources/lo-registry-patch.json b/engines/libreoffice/src/main/resources/lo-registry-patch.json new file mode 100644 index 00000000..0e340b9e --- /dev/null +++ b/engines/libreoffice/src/main/resources/lo-registry-patch.json @@ -0,0 +1,12 @@ +{ + "items": [ + { + "oor:path": "/org.openoffice.Office.Common/Security/Scripting", + "prop": { + "oor:name": "BlockUntrustedRefererLinks", + "oor:op": "fuse" + }, + "value": true + } + ] +} \ No newline at end of file diff --git a/pom.xml b/pom.xml index 95e652e5..cfa60a4a 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ 11 latest quay.io - 3.0.5 + 3.0.6 3.0.1.20 ${project.version} 2.19.2 @@ -316,7 +316,7 @@ true community - file:${project.parent.basedir}/license + file:C:/Users/sabhattacharya/Alfresco/license @@ -335,7 +335,8 @@ 200 1200000 8100 - + /opt/libreoffice-profile/templateProfileDir + /opt/libreoffice-profile/workDir true /usr/bin/alfresco-pdf-renderer 8090