ACS-10505 modifying libreoffice workdir and user's registryconfig file to block external links

This commit is contained in:
bsayan2
2025-12-17 13:29:24 +05:30
parent d80e7ba463
commit 04f90be122
9 changed files with 380 additions and 12 deletions
+5
View File
@@ -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}
@@ -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}
+5
View File
@@ -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}
@@ -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 <http://www.gnu.org/licenses/>.
* #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<PatchItem> patchItems = readPatchItemsFromJson();
// Remove existing matching items
for (PatchItem item : patchItems)
{
String pattern = String.format(
"<item oor:path=\"%s\"><prop oor:name=\"%s\"[^>]*>.*?</prop></item>",
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("</oor:items>", " " + patch + "\n</oor:items>");
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<PatchItem> 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<PatchItem> 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<PatchItem> items)
{
StringBuilder xml = new StringBuilder();
for (PatchItem item : items)
{
xml.append("<item oor:path=\"").append(item.path).append("\">");
xml.append("<prop oor:name=\"").append(item.propName).append("\" oor:op=\"").append(item.op).append("\">");
xml.append("<value>").append(item.value).append("</value>");
xml.append("</prop></item>");
}
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;
}
}
}
@@ -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<File> 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;
}
}
@@ -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();
}
@@ -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}
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
@@ -0,0 +1,12 @@
{
"items": [
{
"oor:path": "/org.openoffice.Office.Common/Security/Scripting",
"prop": {
"oor:name": "BlockUntrustedRefererLinks",
"oor:op": "fuse"
},
"value": true
}
]
}
+4 -3
View File
@@ -21,7 +21,7 @@
<acs-compatible.java.version>11</acs-compatible.java.version>
<image.tag>latest</image.tag>
<image.registry>quay.io</image.registry>
<dependency.pdfbox.version>3.0.5</dependency.pdfbox.version>
<dependency.pdfbox.version>3.0.6</dependency.pdfbox.version>
<dependency.alfresco-jodconverter-core.version>3.0.1.20</dependency.alfresco-jodconverter-core.version>
<env.project_version>${project.version}</env.project_version>
<dependency.jackson.version>2.19.2</dependency.jackson.version>
@@ -316,7 +316,7 @@
</excludes>
<failOnMissingHeader>true</failOnMissingHeader>
<licenseName>community</licenseName>
<licenseResolver>file:${project.parent.basedir}/license</licenseResolver>
<licenseResolver>file:C:/Users/sabhattacharya/Alfresco/license</licenseResolver>
</configuration>
</execution>
</executions>
@@ -335,7 +335,8 @@
<LIBREOFFICE_MAX_TASKS_PER_PROCESS>200</LIBREOFFICE_MAX_TASKS_PER_PROCESS>
<LIBREOFFICE_TIMEOUT>1200000</LIBREOFFICE_TIMEOUT>
<LIBREOFFICE_PORT_NUMBERS>8100</LIBREOFFICE_PORT_NUMBERS>
<LIBREOFFICE_TEMPLATE_PROFILE_DIR />
<LIBREOFFICE_TEMPLATE_PROFILE_DIR>/opt/libreoffice-profile/templateProfileDir</LIBREOFFICE_TEMPLATE_PROFILE_DIR>
<LIBREOFFICE_WORK_PROFILE_DIR>/opt/libreoffice-profile/workDir</LIBREOFFICE_WORK_PROFILE_DIR>
<LIBREOFFICE_IS_ENABLED>true</LIBREOFFICE_IS_ENABLED>
<PDF_RENDERER_EXE>/usr/bin/alfresco-pdf-renderer</PDF_RENDERER_EXE>
<SERVER_PORT>8090</SERVER_PORT>