package jnpf.bizcommon.onlyoffice.service.impl;
|
|
import cn.hutool.crypto.digest.DigestUtil;
|
import cn.hutool.http.HttpRequest;
|
import cn.hutool.http.HttpResponse;
|
import cn.hutool.json.JSONObject;
|
import cn.hutool.json.JSONUtil;
|
import cn.hutool.jwt.JWTUtil;
|
import jnpf.bizcommon.onlyoffice.entity.OnlyOfficeProperties;
|
import jnpf.dmsEntity.preview.model.DmsFilePreviewTicket;
|
import jnpf.exception.DataException;
|
import jnpf.util.JsonUtil;
|
import jnpf.util.TicketUtil;
|
import org.apache.pdfbox.Loader;
|
import org.apache.pdfbox.pdmodel.PDDocument;
|
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.stereotype.Component;
|
|
import java.io.ByteArrayOutputStream;
|
import java.io.InputStream;
|
import java.net.URI;
|
import java.net.URLEncoder;
|
import java.nio.charset.StandardCharsets;
|
import java.util.LinkedHashMap;
|
import java.util.Locale;
|
import java.util.Map;
|
import java.util.Set;
|
import java.util.function.Function;
|
|
@Component
|
public class OnlyOfficeDmsPrintRenderer {
|
private static final Set<String> CONVERTIBLE_TYPES = Set.of(
|
"doc", "docm", "docx", "dot", "dotx", "odp", "ods", "odt",
|
"otp", "ots", "ott", "pdf", "pot", "potx", "pps", "ppsx",
|
"ppt", "pptm", "pptx", "rtf", "txt", "xls", "xlsm", "xlsx",
|
"xlt", "xltx");
|
private static final long PRINT_BASE_TICKET_TTL_SECONDS = 180L;
|
|
private final OnlyOfficeProperties properties;
|
private final Function<DmsFilePreviewTicket, String> printBaseTicketIssuer;
|
|
@Autowired
|
public OnlyOfficeDmsPrintRenderer(OnlyOfficeProperties properties) {
|
this(properties, ticket -> TicketUtil.createTicket(
|
JsonUtil.getObjectToString(ticket), PRINT_BASE_TICKET_TTL_SECONDS));
|
}
|
|
OnlyOfficeDmsPrintRenderer(OnlyOfficeProperties properties,
|
Function<DmsFilePreviewTicket, String> printBaseTicketIssuer) {
|
this.properties = properties;
|
this.printBaseTicketIssuer = printBaseTicketIssuer;
|
}
|
|
public byte[] render(DmsFilePreviewTicket ticket, String accessTicket) {
|
return render(ticket, accessTicket, false);
|
}
|
|
public byte[] renderBase(DmsFilePreviewTicket ticket, String accessTicket) {
|
DmsFilePreviewTicket delegated = new DmsFilePreviewTicket();
|
BeanUtils.copyProperties(ticket, delegated);
|
delegated.setScope(DmsFilePreviewTicket.PRINT_BASE_SCOPE);
|
delegated.setExpiresAt(Math.min(ticket.getExpiresAt(),
|
System.currentTimeMillis() + PRINT_BASE_TICKET_TTL_SECONDS * 1_000L));
|
return render(delegated, printBaseTicketIssuer.apply(delegated), true);
|
}
|
|
private byte[] render(DmsFilePreviewTicket ticket, String accessTicket, boolean printBase) {
|
OnlyOfficeProperties.DmsPrint limits = properties.getDmsPrint();
|
if (ticket.getSizeBytes() != null && ticket.getSizeBytes() > limits.getMaxSourceBytes()) {
|
throw new DataException("打印源文件超过系统大小限制");
|
}
|
|
String fileType = printBase && reusablePersistentPdf(ticket)
|
? "pdf" : extension(ticket.getFileName());
|
if (!CONVERTIBLE_TYPES.contains(fileType)) {
|
throw new DataException("OnlyOffice 不支持该文件格式转换");
|
}
|
String sourceUrl = sourceUrl(ticket, accessTicket, printBase);
|
byte[] pdf;
|
if ("pdf".equals(fileType)) {
|
try {
|
pdf = download(sourceUrl, limits.getConversionTimeoutMillis(),
|
Math.min(limits.getMaxSourceBytes(), limits.getMaxPdfBytes()),
|
"读取打印 PDF 失败");
|
validatePdf(pdf, limits);
|
return pdf;
|
} catch (DataException exception) {
|
fileType = extension(ticket.getFileName());
|
if (!CONVERTIBLE_TYPES.contains(fileType) || "pdf".equals(fileType)) throw exception;
|
}
|
}
|
pdf = convert(sourceUrl, ticket.getFileName(), fileType,
|
limits.getConversionTimeoutMillis(), limits.getMaxPdfBytes());
|
validatePdf(pdf, limits);
|
return pdf;
|
}
|
|
private byte[] convert(String sourceUrl, String fileName, String fileType,
|
int timeout, long maxPdfBytes) {
|
Map<String, Object> request = new LinkedHashMap<>();
|
request.put("async", false);
|
request.put("filetype", fileType);
|
request.put("key", DigestUtil.sha256Hex(sourceUrl));
|
request.put("outputtype", "pdf");
|
request.put("title", fileName);
|
request.put("url", sourceUrl);
|
request.put("token", JWTUtil.createToken(request,
|
requireJwtSecret().getBytes(StandardCharsets.UTF_8)));
|
|
try (HttpResponse response = HttpRequest.post(
|
serverBase() + "/ConvertService.ashx?json=true")
|
.timeout(timeout).body(JSONUtil.toJsonStr(request)).execute()) {
|
if (!response.isOk()) {
|
throw new DataException("OnlyOffice 文件转换失败");
|
}
|
JSONObject result = JSONUtil.parseObj(response.body());
|
if (!Boolean.TRUE.equals(result.getBool("endConvert"))
|
|| result.getInt("error", 0) != 0
|
|| isBlank(result.getStr("fileUrl"))) {
|
throw new DataException("OnlyOffice 文件转换失败");
|
}
|
return download(serverBase() + resultPath(result.getStr("fileUrl")), timeout,
|
maxPdfBytes, "下载 OnlyOffice 转换结果失败");
|
} catch (DataException exception) {
|
throw exception;
|
} catch (Exception exception) {
|
throw new DataException("OnlyOffice 文件转换失败或超时");
|
}
|
}
|
|
private byte[] download(String url, int timeout, long maxBytes, String errorMessage) {
|
try (HttpResponse response = HttpRequest.get(url).timeout(timeout).execute()) {
|
if (!response.isOk()) {
|
throw new DataException(errorMessage);
|
}
|
try (InputStream input = response.bodyStream();
|
ByteArrayOutputStream output = new ByteArrayOutputStream()) {
|
byte[] buffer = new byte[16 * 1024];
|
long total = 0;
|
int read;
|
while ((read = input.read(buffer)) != -1) {
|
total += read;
|
if (total > maxBytes) {
|
throw new DataException(errorMessage + ":文件超过系统大小限制");
|
}
|
output.write(buffer, 0, read);
|
}
|
return output.toByteArray();
|
}
|
} catch (DataException exception) {
|
throw exception;
|
} catch (Exception exception) {
|
throw new DataException(errorMessage);
|
}
|
}
|
|
private void validatePdf(byte[] pdf, OnlyOfficeProperties.DmsPrint limits) {
|
if (pdf == null || pdf.length == 0 || pdf.length > limits.getMaxPdfBytes()) {
|
throw new DataException("打印 PDF 为空或超过系统大小限制");
|
}
|
try (PDDocument document = Loader.loadPDF(pdf)) {
|
if (document.getNumberOfPages() < 1
|
|| document.getNumberOfPages() > limits.getMaxPages()) {
|
throw new DataException("打印 PDF 页数超过系统限制");
|
}
|
} catch (DataException exception) {
|
throw exception;
|
} catch (Exception exception) {
|
throw new DataException("OnlyOffice 转换结果不是有效 PDF");
|
}
|
}
|
|
private String sourceUrl(DmsFilePreviewTicket ticket, String accessTicket, boolean printBase) {
|
String base = properties.getFileFetchBaseUrl();
|
if (isBlank(base)) {
|
throw new DataException("OnlyOffice 文件回源地址未配置");
|
}
|
return base.replaceAll("/+$", "") + "/api/file/Dms/OnlyOffice/"
|
+ ticket.getFileVersionId() + "?ticket="
|
+ URLEncoder.encode(accessTicket, StandardCharsets.UTF_8)
|
+ (printBase ? "&printBase=true" : "");
|
}
|
|
private boolean reusablePersistentPdf(DmsFilePreviewTicket ticket) {
|
if (ticket.getPdfSource() == null || ticket.getPdfSource().getObjectKey() == null
|
|| !ticket.getPdfSource().getObjectKey().startsWith("dms-derived/pdf/")
|
|| ticket.getPdfSource().getObjectKey().contains("..")) return false;
|
return ticket.getWatermarks() == null || ticket.getWatermarks().stream()
|
.allMatch(mark -> mark != null && Boolean.TRUE.equals(mark.getCustomBarcode()));
|
}
|
|
static String resultPath(String url) {
|
try {
|
URI uri = URI.create(url);
|
String rawPath = uri.getRawPath();
|
String decodedPath = uri.getPath();
|
if (isUnsafeResultPath(rawPath, decodedPath)) {
|
throw new IllegalArgumentException();
|
}
|
return rawPath + (uri.getRawQuery() == null ? "" : "?" + uri.getRawQuery());
|
} catch (Exception exception) {
|
throw new DataException("无效的 OnlyOffice 转换结果地址");
|
}
|
}
|
|
private static boolean isUnsafeResultPath(String rawPath, String decodedPath) {
|
if (rawPath == null || decodedPath == null
|
|| !decodedPath.startsWith("/cache/files/")
|
|| decodedPath.indexOf('\\') >= 0
|
|| decodedPath.indexOf('%') >= 0
|
|| !decodedPath.toLowerCase(Locale.ROOT).endsWith(".pdf")) {
|
return true;
|
}
|
for (String segment : decodedPath.split("/")) {
|
if (".".equals(segment) || "..".equals(segment)) {
|
return true;
|
}
|
}
|
return false;
|
}
|
|
private String extension(String fileName) {
|
int position = fileName == null ? -1 : fileName.lastIndexOf('.');
|
if (position < 0 || position == fileName.length() - 1) {
|
throw new DataException("打印文件缺少有效扩展名");
|
}
|
return fileName.substring(position + 1).toLowerCase(Locale.ROOT);
|
}
|
|
private String serverBase() {
|
String base = properties.getDsInternalUrl();
|
if (isBlank(base)) {
|
base = properties.getDsPublicUrl();
|
}
|
if (isBlank(base)) {
|
throw new DataException("OnlyOffice 服务地址未配置");
|
}
|
return base.replaceAll("/+$", "");
|
}
|
|
private String requireJwtSecret() {
|
String secret = properties.getJwtSecret();
|
if (isBlank(secret) || secret.contains("${")) {
|
throw new DataException("OnlyOffice 签名密钥未正确配置");
|
}
|
return secret;
|
}
|
|
private boolean isBlank(String value) {
|
return value == null || value.trim().isEmpty();
|
}
|
}
|