feat(编辑能力): 修复重复编号及加密文档编辑异常,统一签名信息顺序,优化弹窗标题与布局
build.yaml / build (push) Successful in 42s

This commit is contained in:
xiaoqidun committed 2026-09-23 23:39:34 +08:00
1 parent 3927929822
commit e3908c1149
8 files changed
+317 -145

No files matched your search

+8 -4
View File
@@ -353,8 +353,9 @@
</div> </div>
</form> </form>
</dialog> </dialog>
<dialog id="signPanel" class="form-dialog security-dialog" aria-label="签章"> <dialog id="signPanel" class="form-dialog security-dialog" aria-labelledby="signTitle">
<form id="signForm" autocomplete="off"> <form id="signForm" autocomplete="off">
<h2 id="signTitle" class="form-title">签署文档</h2>
<div class="form-row"><label for="signCertificate">证书</label><input id="signCertificate" type="file" accept=".pem,.cer,.crt,.der" required></div> <div class="form-row"><label for="signCertificate">证书</label><input id="signCertificate" type="file" accept=".pem,.cer,.crt,.der" required></div>
<div class="form-row"><label for="signKey">私钥</label><input id="signKey" type="file" accept=".pem,.key,.der" required></div> <div class="form-row"><label for="signKey">私钥</label><input id="signKey" type="file" accept=".pem,.key,.der" required></div>
<div class="form-row"><label for="signKeyPassword">私钥口令</label><input id="signKeyPassword" type="password" autocomplete="off"></div> <div class="form-row"><label for="signKeyPassword">私钥口令</label><input id="signKeyPassword" type="password" autocomplete="off"></div>
@@ -374,8 +375,9 @@
<div class="form-actions"><div id="signStatus" class="form-status" role="status" aria-live="polite"></div><button id="signCancel" class="button" type="button">取消</button><button class="button" type="submit" title="签署另存">签署</button></div> <div class="form-actions"><div id="signStatus" class="form-status" role="status" aria-live="polite"></div><button id="signCancel" class="button" type="button">取消</button><button class="button" type="submit" title="签署另存">签署</button></div>
</form> </form>
</dialog> </dialog>
<dialog id="verifyPanel" class="form-dialog security-dialog" aria-label="验签"> <dialog id="verifyPanel" class="form-dialog security-dialog" aria-labelledby="verifyTitle">
<form id="verifyForm"> <form id="verifyForm">
<h2 id="verifyTitle" class="form-title">验证签名</h2>
<div class="form-row"><label for="verifyRoots">信任根</label><input id="verifyRoots" type="file" accept=".pem,.cer,.crt,.der" multiple></div> <div class="form-row"><label for="verifyRoots">信任根</label><input id="verifyRoots" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
<div class="form-row"><label for="verifyCerts">证书</label><input id="verifyCerts" type="file" accept=".pem,.cer,.crt,.der" multiple></div> <div class="form-row"><label for="verifyCerts">证书</label><input id="verifyCerts" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
<div class="form-row"><label for="verifyTimeRoots">时间戳根</label><input id="verifyTimeRoots" type="file" accept=".pem,.cer,.crt,.der" multiple></div> <div class="form-row"><label for="verifyTimeRoots">时间戳根</label><input id="verifyTimeRoots" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
@@ -386,8 +388,9 @@
<div class="form-actions"><div id="verifyStatus" class="form-status" role="status" aria-live="polite"></div><button id="verifyCancel" class="button" type="button">取消</button><button class="button" type="submit">验签</button></div> <div class="form-actions"><div id="verifyStatus" class="form-status" role="status" aria-live="polite"></div><button id="verifyCancel" class="button" type="button">取消</button><button class="button" type="submit">验签</button></div>
</form> </form>
</dialog> </dialog>
<dialog id="credentialsPanel" class="form-dialog security-dialog" aria-label="解锁文档"> <dialog id="credentialsPanel" class="form-dialog security-dialog" aria-labelledby="credentialsTitle">
<form id="credentialsForm" autocomplete="off"> <form id="credentialsForm" autocomplete="off">
<h2 id="credentialsTitle" class="form-title">解锁文档</h2>
<div class="form-row"><label for="credentialsType">方式</label><select id="credentialsType"><option value="password">口令</option><option value="certificate">证书</option></select></div> <div class="form-row"><label for="credentialsType">方式</label><select id="credentialsType"><option value="password">口令</option><option value="certificate">证书</option></select></div>
<div class="form-row"><label for="credentialsUser">用户</label><input id="credentialsUser" type="text" autocomplete="off" spellcheck="false"></div> <div class="form-row"><label for="credentialsUser">用户</label><input id="credentialsUser" type="text" autocomplete="off" spellcheck="false"></div>
<div id="credentialsPasswordRow" class="form-row"><label for="credentialsPassword">口令</label><input id="credentialsPassword" type="password" autocomplete="off" required autofocus></div> <div id="credentialsPasswordRow" class="form-row"><label for="credentialsPassword">口令</label><input id="credentialsPassword" type="password" autocomplete="off" required autofocus></div>
@@ -397,8 +400,9 @@
<div class="form-actions"><div id="credentialsStatus" class="form-status" role="status" aria-live="polite"></div><button id="credentialsCancel" class="button" type="button">取消</button><button class="button" type="submit">解锁</button></div> <div class="form-actions"><div id="credentialsStatus" class="form-status" role="status" aria-live="polite"></div><button id="credentialsCancel" class="button" type="button">取消</button><button class="button" type="submit">解锁</button></div>
</form> </form>
</dialog> </dialog>
<dialog id="exportPanel" class="form-dialog" aria-label="导出文档"> <dialog id="exportPanel" class="form-dialog" aria-labelledby="exportTitle">
<form id="exportForm"> <form id="exportForm">
<h2 id="exportTitle" class="form-title">导出文档</h2>
<div id="encryptionFields" hidden> <div id="encryptionFields" hidden>
<div class="form-row"><label for="encryptionType">方式</label><select id="encryptionType"><option value="password">口令</option><option value="certificate">证书</option></select></div> <div class="form-row"><label for="encryptionType">方式</label><select id="encryptionType"><option value="password">口令</option><option value="certificate">证书</option></select></div>
<div id="encryptionUserRow" class="form-row"><label for="encryptionUser">用户</label><input id="encryptionUser" type="text" autocomplete="off" spellcheck="false"></div> <div id="encryptionUserRow" class="form-row"><label for="encryptionUser">用户</label><input id="encryptionUser" type="text" autocomplete="off" spellcheck="false"></div>
+10
View File
@@ -645,6 +645,16 @@ button:not(:disabled):hover {
padding: 12px; padding: 12px;
} }
.form-title {
margin: -12px -12px 12px;
padding: 10px 12px;
border-bottom: 1px solid var(--line);
background: #eef1f4;
font-size: 14px;
font-weight: 600;
line-height: 1.5;
}
.security-dialog { .security-dialog {
width: 360px; width: 360px;
} }
+21 -20
View File
@@ -350,6 +350,7 @@ const el = {
exportPageButton: document.querySelector("#exportPageButton"), exportPageButton: document.querySelector("#exportPageButton"),
exportButton: document.querySelector("#exportButton"), exportButton: document.querySelector("#exportButton"),
exportPanel: document.querySelector("#exportPanel"), exportPanel: document.querySelector("#exportPanel"),
exportTitle: document.querySelector("#exportTitle"),
exportForm: document.querySelector("#exportForm"), exportForm: document.querySelector("#exportForm"),
exportAll: document.querySelector("#exportAll"), exportAll: document.querySelector("#exportAll"),
exportSpecified: document.querySelector("#exportSpecified"), exportSpecified: document.querySelector("#exportSpecified"),
@@ -1602,7 +1603,7 @@ async function toggleEditor() {
const openSeq = ++state.openSeq; const openSeq = ++state.openSeq;
setBusy(true, "正在准备编辑", null, "正在准备编辑"); setBusy(true, "正在准备编辑", null, "正在准备编辑");
try { try {
const doc = await callWASM("ofdgoEditDocument"); const doc = await callWASM("ofdgoEditDocument", pageIndex);
if (openSeq !== state.openSeq) return; if (openSeq !== state.openSeq) return;
state.savedRevision = doc.revision; state.savedRevision = doc.revision;
state.editorViews.clear(); state.editorViews.clear();
@@ -3961,7 +3962,7 @@ function openExportPanel(encrypted = false) {
el.encryptionFields.hidden = !encrypted; el.encryptionFields.hidden = !encrypted;
el.encryptionPassword.required = el.encryptionConfirm.required = encrypted; el.encryptionPassword.required = el.encryptionConfirm.required = encrypted;
updateEncryptionType(); updateEncryptionType();
el.exportPanel.setAttribute("aria-label", encrypted ? "加密另存" : "导出文档"); el.exportTitle.textContent = encrypted ? "加密另存" : "导出文档";
el.exportSubmit.textContent = encrypted ? "保存" : "导出"; el.exportSubmit.textContent = encrypted ? "保存" : "导出";
if (state.editing && state.pageSelection.size) { if (state.editing && state.pageSelection.size) {
el.exportSpecified.checked = true; el.exportSpecified.checked = true;
@@ -5851,9 +5852,9 @@ function renderSignatures() {
head.append(badges, name); head.append(badges, name);
row.append(head); row.append(head);
appendInfoLine(row, "章名", signature.sealName);
appendInfoLine(row, "签署人", signature.signer); appendInfoLine(row, "签署人", signature.signer);
appendInfoLine(row, "时间", formatDocumentTime(signature.signatureDateTime)); appendInfoLine(row, "时间", formatDocumentTime(signature.signatureDateTime));
appendInfoLine(row, "章名", signature.sealName);
appendInfoLine(row, "机构", signatureAgency(signature)); appendInfoLine(row, "机构", signatureAgency(signature));
appendSignatureCheck(row, "信任", signature.certTrustChecked, signature.certTrustOK); appendSignatureCheck(row, "信任", signature.certTrustChecked, signature.certTrustOK);
appendInfoLine(row, "保护文件", signatureReferenceText(signature), signatureReferenceStatus(signature)); appendInfoLine(row, "保护文件", signatureReferenceText(signature), signatureReferenceStatus(signature));
@@ -5864,25 +5865,25 @@ function renderSignatures() {
checks.append(checksTitle); checks.append(checksTitle);
appendSignatureCheck(checks, "原文", signature.dataHashChecked, signature.dataHashOK); appendSignatureCheck(checks, "原文", signature.dataHashChecked, signature.dataHashOK);
appendSignatureCheck(checks, "签名", signature.signedValueChecked, signature.signedValueOK); appendSignatureCheck(checks, "签名", signature.signedValueChecked, signature.signedValueOK);
appendSignatureCheck(checks, "证书", signature.certChecked, signature.certOK);
appendSignaturePolicy(checks, "证书期限", signature.certTimeChecked, signature.certTimeOK);
appendInfoLine(checks, "信任错误", signature.certTrustError, "fail");
appendSignaturePolicy(checks, "签署时间", signature.signatureTimeChecked, signature.signatureTimeOK);
if (signature.type !== "Sign") { if (signature.type !== "Sign") {
appendSignatureCheck(checks, "印章", signature.sealChecked, signature.sealOK); appendSignatureCheck(checks, "印章", signature.sealChecked, signature.sealOK);
appendSignaturePolicy(checks, "印章匹配", signature.sealMatchChecked, signature.sealMatchOK); appendSignaturePolicy(checks, "印章匹配", signature.sealMatchChecked, signature.sealMatchOK);
appendSignaturePolicy(checks, "印章期限", signature.sealTimeChecked, signature.sealTimeOK);
if (signature.sealCertTimeChecked) {
appendInfoLine(checks, "制章证书", signature.sealCertTimeOK ? "有效" : "失效", signature.sealCertTimeOK ? "ok" : "fail");
}
} }
appendSignatureCheck(checks, "证书", signature.certChecked, signature.certOK);
appendSignaturePolicy(checks, "签署时间", signature.signatureTimeChecked, signature.signatureTimeOK);
appendSignaturePolicy(checks, "印章期限", signature.sealTimeChecked, signature.sealTimeOK);
if (signature.sealCertTimeChecked) {
appendInfoLine(checks, "制章证书", signature.sealCertTimeOK ? "有效" : "失效", signature.sealCertTimeOK ? "ok" : "fail");
}
appendSignaturePolicy(checks, "证书期限", signature.certTimeChecked, signature.certTimeOK);
appendSignatureCheck(checks, "策略", signature.policyChecked, signature.policyOK);
appendSignatureCheck(checks, "覆盖", signature.coverageChecked, signature.coverageOK); appendSignatureCheck(checks, "覆盖", signature.coverageChecked, signature.coverageOK);
appendSignatureCheck(checks, "时间戳", signature.timestampChecked, signature.timestampOK);
appendSignatureCheck(checks, "撤销", signature.revocationChecked, signature.revocationOK);
appendInfoLine(checks, "策略错误", signature.policyError, "fail");
appendInfoLine(checks, "信任错误", signature.certTrustError, "fail");
appendInfoLine(checks, "覆盖错误", signature.coverageError, "fail"); appendInfoLine(checks, "覆盖错误", signature.coverageError, "fail");
appendInfoLine(checks, "未保护", signature.uncoveredFiles?.join("、"), "fail"); appendInfoLine(checks, "未保护", signature.uncoveredFiles?.join("、"), "fail");
appendSignatureCheck(checks, "策略", signature.policyChecked, signature.policyOK);
appendInfoLine(checks, "策略错误", signature.policyError, "fail");
appendSignatureCheck(checks, "时间戳", signature.timestampChecked, signature.timestampOK);
appendSignatureCheck(checks, "撤销", signature.revocationChecked, signature.revocationOK);
row.append(checks); row.append(checks);
for (const timestamp of signature.timestamps || []) { for (const timestamp of signature.timestamps || []) {
const details = document.createElement("details"); const details = document.createElement("details");
@@ -5917,17 +5918,17 @@ function renderSignatures() {
title.textContent = "签章信息"; title.textContent = "签章信息";
details.append(title); details.append(title);
appendInfoLine(details, "编号", signature.id); appendInfoLine(details, "编号", signature.id);
appendInfoLine(details, "版本", signature.version);
appendInfoLine(details, "章图", signature.sealType);
appendInfoLine(details, "章号", signature.sealId);
appendInfoLine(details, "厂商", signature.sealVendor);
appendInfoLine(details, "文档", signature.docRoot); appendInfoLine(details, "文档", signature.docRoot);
appendInfoLine(details, "算法", signature.signatureMethod); appendInfoLine(details, "算法", signature.signatureMethod);
appendInfoLine(details, "摘要", signature.digestMethod); appendInfoLine(details, "摘要", signature.digestMethod);
appendInfoLine(details, "序列号", signature.signSerial);
appendInfoLine(details, "证书主体", signature.signSubject); appendInfoLine(details, "证书主体", signature.signSubject);
appendInfoLine(details, "颁发者", signature.signIssuer); appendInfoLine(details, "颁发者", signature.signIssuer);
appendInfoLine(details, "序列号", signature.signSerial);
appendInfoLine(details, "章号", signature.sealId);
appendInfoLine(details, "章图", signature.sealType);
appendInfoLine(details, "制章证书", signature.sealSubject); appendInfoLine(details, "制章证书", signature.sealSubject);
appendInfoLine(details, "印章厂商", signature.sealVendor);
appendInfoLine(details, "组件版本", signature.version);
row.append(details); row.append(details);
fragment.append(row); fragment.append(row);
} }
+45 -2
View File
@@ -498,6 +498,15 @@ func renderPage(args []js.Value) (any, error) {
dpi = args[2].Float() dpi = args[2].Float()
} }
raster := len(args) > 3 && args[3].Bool() raster := len(args) > 3 && args[3].Bool()
if currentEditor != nil {
page, err := currentEditor.Page(args[0].Int())
if err != nil {
return nil, err
}
if err := syncEditorPageIDs(args[0].Int(), page); err != nil {
return nil, err
}
}
page, err := currentSession.RenderPage(args[0].Int(), backend, dpi, raster) page, err := currentSession.RenderPage(args[0].Int(), backend, dpi, raster)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -569,7 +578,7 @@ func renderPage(args []js.Value) (any, error) {
} }
// pageTextString 提取OFD页面原文 // pageTextString 提取OFD页面原文
// 入参: args 浏览器参数 // 入参: args 页面索引
// 返回: any 页面原文, error 错误信息 // 返回: any 页面原文, error 错误信息
func pageTextString(args []js.Value) (any, error) { func pageTextString(args []js.Value) (any, error) {
if currentSession == nil { if currentSession == nil {
@@ -2906,7 +2915,7 @@ func updateInfo(args []js.Value) (any, error) {
} }
// editDocument 将已打开文档接入编辑器,沿用页面、资源及字体配置 // editDocument 将已打开文档接入编辑器,沿用页面、资源及字体配置
// 入参: args 浏览器参数 // 入参: args 当前页索引,省略时使用首页
// 返回: any 编辑状态, error 错误信息 // 返回: any 编辑状态, error 错误信息
func editDocument(args []js.Value) (any, error) { func editDocument(args []js.Value) (any, error) {
if currentSession == nil { if currentSession == nil {
@@ -2924,6 +2933,17 @@ func editDocument(args []js.Value) (any, error) {
editor.SetFontFS(currentSession.fontFS) editor.SetFontFS(currentSession.fontFS)
} }
editor.SetRenderBackends(currentSession.Renderer.Backends()) editor.SetRenderBackends(currentSession.Renderer.Backends())
page := 0
if len(args) != 0 {
page = args[0].Int()
}
content, err := editor.Page(page)
if err != nil {
return nil, err
}
if err := syncEditorPageIDs(page, content); err != nil {
return nil, err
}
currentEditor = editor currentEditor = editor
currentSession.editing = true currentSession.editing = true
copiedObjects = nil copiedObjects = nil
@@ -2931,6 +2951,29 @@ func editDocument(args []js.Value) (any, error) {
return editorSummary(), nil return editorSummary(), nil
} }
// syncEditorPageIDs 同步编辑副本的对象编号,不改变原渲染属性或丢弃正常页面缓存
// 入参: index 页面索引, content 编辑页面
// 返回: error 页面读取错误
func syncEditorPageIDs(index int, content *ofdgo.PageContent) error {
page, err := currentSession.pageContent(index)
if err != nil {
return err
}
for i, layer := range content.Content.Layer {
for j, object := range layer.Objects {
before := &page.Content.Layer[i].Objects[j]
if before.TextObject.ID != object.TextObject.ID || before.PathObject.ID != object.PathObject.ID || before.ImageObject.ID != object.ImageObject.ID || before.CompositeGraphicUnit.ID != object.CompositeGraphicUnit.ID {
before.TextObject.ID = object.TextObject.ID
before.PathObject.ID = object.PathObject.ID
before.ImageObject.ID = object.ImageObject.ID
before.CompositeGraphicUnit.ID = object.CompositeGraphicUnit.ID
delete(currentSession.textCache, index)
}
}
}
return nil
}
// previewEditor 通过内存快照更新预览,不生成中间压缩包 // previewEditor 通过内存快照更新预览,不生成中间压缩包
// 入参: editor 编辑文档, annotations 是否显示注解 // 入参: editor 编辑文档, annotations 是否显示注解
// 返回: editorInfo 文档信息, error 错误信息 // 返回: editorInfo 文档信息, error 错误信息
+86 -86
View File
@@ -43,65 +43,65 @@ type FontInfo = ofdgo.FontInfo
type SignatureInfo struct { type SignatureInfo struct {
DocIndex int `json:"docIndex"` DocIndex int `json:"docIndex"`
DocRoot string `json:"docRoot,omitempty"` DocRoot string `json:"docRoot,omitempty"`
ID string `json:"id"`
Type string `json:"type"`
Status string `json:"status"`
IntegrityValid bool `json:"integrityValid"` IntegrityValid bool `json:"integrityValid"`
TrustedValid bool `json:"trustedValid"` TrustedValid bool `json:"trustedValid"`
PolicyChecked bool `json:"policyChecked"` Error string `json:"error,omitempty"`
PolicyOK bool `json:"policyOK"` Signer string `json:"signer,omitempty"`
PolicyError string `json:"policyError,omitempty"` SignatureDateTime string `json:"signatureDateTime,omitempty"`
SealName string `json:"sealName,omitempty"`
Provider string `json:"provider,omitempty"`
Company string `json:"company,omitempty"`
SealType string `json:"sealType,omitempty"`
SealID string `json:"sealId,omitempty"`
SealVendor string `json:"sealVendor,omitempty"`
Version string `json:"version,omitempty"`
SignSubject string `json:"signSubject,omitempty"`
SignIssuer string `json:"signIssuer,omitempty"`
SignSerial string `json:"signSerial,omitempty"`
SealSubject string `json:"sealSubject,omitempty"`
SignatureMethod string `json:"signatureMethod,omitempty"`
DigestMethod string `json:"digestMethod,omitempty"`
ReferenceCount int `json:"referenceCount"`
ReferenceChecked int `json:"referenceChecked"`
ReferencePassed int `json:"referencePassed"`
Stamps []SignatureStampInfo `json:"stamps,omitempty"`
DataHashChecked bool `json:"dataHashChecked"`
DataHashOK bool `json:"dataHashOK"`
SignedValueChecked bool `json:"signedValueChecked"`
SignedValueOK bool `json:"signedValueOK"`
CertChecked bool `json:"certChecked"`
CertOK bool `json:"certOK"`
CertTimeChecked bool `json:"certTimeChecked,omitempty"`
CertTimeOK bool `json:"certTimeOK,omitempty"`
CertTrustChecked bool `json:"certTrustChecked,omitempty"`
CertTrustOK bool `json:"certTrustOK,omitempty"`
CertTrustError string `json:"certTrustError,omitempty"`
SignatureTimeChecked bool `json:"signatureTimeChecked,omitempty"`
SignatureTimeOK bool `json:"signatureTimeOK,omitempty"`
SealChecked bool `json:"sealChecked"`
SealOK bool `json:"sealOK"`
SealMatchChecked bool `json:"sealMatchChecked"`
SealMatchOK bool `json:"sealMatchOK"`
SealTimeChecked bool `json:"sealTimeChecked,omitempty"`
SealTimeOK bool `json:"sealTimeOK,omitempty"`
SealCertTimeChecked bool `json:"sealCertTimeChecked,omitempty"`
SealCertTimeOK bool `json:"sealCertTimeOK,omitempty"`
CoverageChecked bool `json:"coverageChecked"` CoverageChecked bool `json:"coverageChecked"`
CoverageOK bool `json:"coverageOK"` CoverageOK bool `json:"coverageOK"`
CoverageError string `json:"coverageError,omitempty"` CoverageError string `json:"coverageError,omitempty"`
UncoveredFiles []string `json:"uncoveredFiles,omitempty"` UncoveredFiles []string `json:"uncoveredFiles,omitempty"`
PolicyChecked bool `json:"policyChecked"`
PolicyOK bool `json:"policyOK"`
PolicyError string `json:"policyError,omitempty"`
TimestampChecked bool `json:"timestampChecked"` TimestampChecked bool `json:"timestampChecked"`
TimestampOK bool `json:"timestampOK"` TimestampOK bool `json:"timestampOK"`
Timestamps []TimestampInfo `json:"timestamps,omitempty"` Timestamps []TimestampInfo `json:"timestamps,omitempty"`
RevocationChecked bool `json:"revocationChecked"` RevocationChecked bool `json:"revocationChecked"`
RevocationOK bool `json:"revocationOK"` RevocationOK bool `json:"revocationOK"`
Revocations []RevocationInfo `json:"revocations,omitempty"` Revocations []RevocationInfo `json:"revocations,omitempty"`
ID string `json:"id"`
Type string `json:"type"`
Status string `json:"status"`
Version string `json:"version,omitempty"`
SealType string `json:"sealType,omitempty"`
SealID string `json:"sealId,omitempty"`
SealName string `json:"sealName,omitempty"`
SealVendor string `json:"sealVendor,omitempty"`
Signer string `json:"signer,omitempty"`
SignatureDateTime string `json:"signatureDateTime,omitempty"`
Provider string `json:"provider,omitempty"`
Company string `json:"company,omitempty"`
DataHashChecked bool `json:"dataHashChecked"`
DataHashOK bool `json:"dataHashOK"`
SignedValueChecked bool `json:"signedValueChecked"`
SignedValueOK bool `json:"signedValueOK"`
SealChecked bool `json:"sealChecked"`
SealOK bool `json:"sealOK"`
SealMatchChecked bool `json:"sealMatchChecked"`
SealMatchOK bool `json:"sealMatchOK"`
CertChecked bool `json:"certChecked"`
CertOK bool `json:"certOK"`
SignatureTimeChecked bool `json:"signatureTimeChecked,omitempty"`
SignatureTimeOK bool `json:"signatureTimeOK,omitempty"`
SealTimeChecked bool `json:"sealTimeChecked,omitempty"`
SealTimeOK bool `json:"sealTimeOK,omitempty"`
SealCertTimeChecked bool `json:"sealCertTimeChecked,omitempty"`
SealCertTimeOK bool `json:"sealCertTimeOK,omitempty"`
CertTimeChecked bool `json:"certTimeChecked,omitempty"`
CertTimeOK bool `json:"certTimeOK,omitempty"`
CertTrustChecked bool `json:"certTrustChecked,omitempty"`
CertTrustOK bool `json:"certTrustOK,omitempty"`
CertTrustError string `json:"certTrustError,omitempty"`
ReferenceCount int `json:"referenceCount"`
ReferenceChecked int `json:"referenceChecked"`
ReferencePassed int `json:"referencePassed"`
SignatureMethod string `json:"signatureMethod,omitempty"`
DigestMethod string `json:"digestMethod,omitempty"`
SignSerial string `json:"signSerial,omitempty"`
SignSubject string `json:"signSubject,omitempty"`
SignIssuer string `json:"signIssuer,omitempty"`
SealSubject string `json:"sealSubject,omitempty"`
Stamps []SignatureStampInfo `json:"stamps,omitempty"`
Error string `json:"error,omitempty"`
} }
// TimestampInfo 独立展示时间戳绑定、签名、信任和证书时效 // TimestampInfo 独立展示时间戳绑定、签名、信任和证书时效
@@ -681,63 +681,63 @@ func signatureInfo(report ofdgo.SignatureVerifyReport) SignatureInfo {
info := SignatureInfo{ info := SignatureInfo{
DocIndex: report.DocIndex, DocIndex: report.DocIndex,
DocRoot: report.DocRoot, DocRoot: report.DocRoot,
IntegrityValid: report.IntegrityValid(),
TrustedValid: report.TrustedValid(),
PolicyChecked: report.PolicyChecked,
PolicyOK: report.PolicyOK,
PolicyError: report.PolicyError,
CoverageChecked: report.CoverageChecked,
CoverageOK: report.CoverageOK,
CoverageError: report.CoverageError,
UncoveredFiles: report.UncoveredFiles,
TimestampChecked: report.TimestampChecked,
TimestampOK: report.TimestampOK,
RevocationChecked: report.RevocationChecked,
RevocationOK: report.RevocationOK,
ID: report.ID, ID: report.ID,
Type: string(report.Type), Type: string(report.Type),
Status: status, Status: status,
Version: report.Provider.Version, IntegrityValid: report.IntegrityValid(),
SealType: report.SealType, TrustedValid: report.TrustedValid(),
SealID: report.SealInfo.ID, Error: signatureReportError(report),
SealName: report.SealInfo.Name,
SealVendor: report.SealInfo.VendorID,
Signer: signatureSigner(report), Signer: signatureSigner(report),
SignatureDateTime: dateTime, SignatureDateTime: dateTime,
SealName: report.SealInfo.Name,
Provider: report.Provider.ProviderName, Provider: report.Provider.ProviderName,
Company: report.Provider.Company, Company: report.Provider.Company,
SealType: report.SealType,
SealID: report.SealInfo.ID,
SealVendor: report.SealInfo.VendorID,
Version: report.Provider.Version,
SignSubject: report.SignCert.Subject,
SignIssuer: report.SignCert.Issuer,
SignSerial: report.SignCert.SerialNumber,
SealSubject: report.SealCert.Subject,
SignatureMethod: report.SignatureMethod,
DigestMethod: report.DigestMethod,
ReferenceCount: len(report.References),
ReferenceChecked: checked,
ReferencePassed: passed,
Stamps: signatureStampInfos(report.StampPositions),
DataHashChecked: report.DataHashChecked, DataHashChecked: report.DataHashChecked,
DataHashOK: report.DataHashOK, DataHashOK: report.DataHashOK,
SignedValueChecked: report.SignedValueChecked, SignedValueChecked: report.SignedValueChecked,
SignedValueOK: report.SignedValueOK, SignedValueOK: report.SignedValueOK,
SealChecked: report.SealChecked,
SealOK: report.SealOK,
SealMatchChecked: report.SealMatchChecked,
SealMatchOK: report.SealMatchOK,
CertChecked: report.CertChecked, CertChecked: report.CertChecked,
CertOK: report.CertOK, CertOK: report.CertOK,
SignatureTimeChecked: report.SignatureTimeChecked,
SignatureTimeOK: report.SignatureTimeOK,
SealTimeChecked: report.SealTimeChecked,
SealTimeOK: report.SealTimeOK,
SealCertTimeChecked: report.SealCertTimeChecked,
SealCertTimeOK: report.SealCertTimeOK,
CertTimeChecked: report.CertTimeChecked, CertTimeChecked: report.CertTimeChecked,
CertTimeOK: report.CertTimeOK, CertTimeOK: report.CertTimeOK,
CertTrustChecked: report.CertTrustChecked, CertTrustChecked: report.CertTrustChecked,
CertTrustOK: report.CertTrustOK, CertTrustOK: report.CertTrustOK,
CertTrustError: report.CertTrustError, CertTrustError: report.CertTrustError,
ReferenceCount: len(report.References), SignatureTimeChecked: report.SignatureTimeChecked,
ReferenceChecked: checked, SignatureTimeOK: report.SignatureTimeOK,
ReferencePassed: passed, SealChecked: report.SealChecked,
SignatureMethod: report.SignatureMethod, SealOK: report.SealOK,
DigestMethod: report.DigestMethod, SealMatchChecked: report.SealMatchChecked,
SignSerial: report.SignCert.SerialNumber, SealMatchOK: report.SealMatchOK,
SignSubject: report.SignCert.Subject, SealTimeChecked: report.SealTimeChecked,
SignIssuer: report.SignCert.Issuer, SealTimeOK: report.SealTimeOK,
SealSubject: report.SealCert.Subject, SealCertTimeChecked: report.SealCertTimeChecked,
Stamps: signatureStampInfos(report.StampPositions), SealCertTimeOK: report.SealCertTimeOK,
Error: signatureReportError(report), CoverageChecked: report.CoverageChecked,
CoverageOK: report.CoverageOK,
CoverageError: report.CoverageError,
UncoveredFiles: report.UncoveredFiles,
PolicyChecked: report.PolicyChecked,
PolicyOK: report.PolicyOK,
PolicyError: report.PolicyError,
TimestampChecked: report.TimestampChecked,
TimestampOK: report.TimestampOK,
RevocationChecked: report.RevocationChecked,
RevocationOK: report.RevocationOK,
} }
for _, stamp := range report.Timestamps { for _, stamp := range report.Timestamps {
info.Timestamps = append(info.Timestamps, TimestampInfo{ info.Timestamps = append(info.Timestamps, TimestampInfo{
+114
View File
@@ -57,6 +57,7 @@ type editorSourcePage struct {
root *editorXML root *editorXML
original *PageContent original *PageContent
nodes map[string]*editorXML nodes map[string]*editorXML
repaired bool
} }
// ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因 // ObjectCapabilities 已有对象可执行的操作,Reason说明受限原因
@@ -260,6 +261,103 @@ func (e *Editor) sourceRGB() bool {
return space != nil && space.Type == "RGB" && (space.BitsPerComponent == 0 || space.BitsPerComponent == 8) && len(space.Palette) == 0 return space != nil && space.Type == "RGB" && (space.BitsPerComponent == 0 || space.BitsPerComponent == 8) && len(space.Palette) == 0
} }
// repairSourceObjectIDs 按实际节点区分重复对象,只修复没有引用歧义的编号
// 入参: data 页面原文, root 页面节点
// 返回: []byte 编辑副本, bool 是否修复, error 错误信息
func (e *Editor) repairSourceObjectIDs(data []byte, root *editorXML) ([]byte, bool, error) {
seen := make(map[string]bool)
duplicates := make(map[string]bool)
var nodes []*editorXML
var visit func(*editorXML) error
visit = func(parent *editorXML) error {
for _, node := range parent.children {
id := node.attr("ID")
if id != "" && seen[id] {
switch node.name.Local {
case "TextObject", "PathObject", "ImageObject", "CompositeObject":
nodes = append(nodes, node)
duplicates[id] = true
default:
return fmt.Errorf("duplicate container ID %q", id)
}
}
seen[id] = true
if node.name.Local == "PageBlock" {
if err := visit(node); err != nil {
return err
}
}
}
return nil
}
if content := root.child("Content"); content != nil {
for _, layer := range content.children {
if layer.name.Local == "Layer" {
if err := visit(layer); err != nil {
return nil, false, err
}
}
}
}
if len(nodes) == 0 {
return data, false, nil
}
if err := e.checkDuplicateObjectReferences(duplicates); err != nil {
return nil, false, err
}
if err := e.prepareSourceIDs(); err != nil {
return nil, false, err
}
maximum := e.maxID
var patches []editorXMLPatch
for _, node := range nodes {
maximum++
encoded, err := editorXMLAttribute(data, node, "ID", strconv.Itoa(maximum))
if err != nil {
return nil, false, err
}
patches = append(patches, editorXMLPatch{node.start, node.end, encoded})
}
e.maxID = maximum
return editorPatchXML(data, patches), true, nil
}
// checkDuplicateObjectReferences 拒绝无法判定目标的编号引用,不猜测其所属对象
// 入参: duplicates 重复编号
// 返回: error 引用歧义或读取错误
func (e *Editor) checkDuplicateObjectReferences(duplicates map[string]bool) error {
reader := e.source.reader
for _, name := range reader.fileNamesFold {
if !strings.EqualFold(path.Ext(name), ".xml") {
continue
}
input, err := reader.openFile(name)
if err != nil {
return err
}
decoder := xml.NewDecoder(input)
for {
token, readErr := decoder.Token()
if readErr != nil {
input.Close()
if readErr != io.EOF {
return readErr
}
break
}
if node, ok := token.(xml.StartElement); ok {
for _, attr := range node.Attr {
if editorObjectReference(attr.Name.Local) && duplicates[editorResourceID(attr.Value)] {
input.Close()
return fmt.Errorf("ambiguous reference to duplicate object ID %q in %s", attr.Value, name)
}
}
}
}
}
return nil
}
// loadSourcePage 首次访问时解析原始页面,不加载图片或字体二进制数据 // loadSourcePage 首次访问时解析原始页面,不加载图片或字体二进制数据
// 入参: index 页面索引 // 入参: index 页面索引
// 返回: error 错误信息 // 返回: error 错误信息
@@ -281,6 +379,21 @@ func (e *Editor) loadSourcePage(index int) error {
if err != nil { if err != nil {
return err return err
} }
data, repaired, err := e.repairSourceObjectIDs(data, root)
if err != nil {
return err
}
if repaired {
root, err = parseEditorXML(data)
if err != nil {
return err
}
var updated PageContent
if err := xml.Unmarshal(data, &updated); err != nil {
return err
}
page.Content = updated.Content
}
nodes := make(map[string]*editorXML) nodes := make(map[string]*editorXML)
if content := root.child("Content"); content != nil { if content := root.child("Content"); content != nil {
for _, layer := range content.children { for _, layer := range content.children {
@@ -293,6 +406,7 @@ func (e *Editor) loadSourcePage(index int) error {
} }
} }
source.data, source.root, source.nodes = data, root, nodes source.data, source.root, source.nodes = data, root, nodes
source.repaired = repaired
for i := range page.Content.Layer { for i := range page.Content.Layer {
layer := &page.Content.Layer[i] layer := &page.Content.Layer[i]
for j, object := range layer.Objects { for j, object := range layer.Objects {
+1 -1
View File
@@ -49,7 +49,7 @@ func (e *Editor) sourceParts(progress editorProgress) (map[string][]byte, error)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if !bytes.Equal(data, original.data) { if original.repaired || !bytes.Equal(data, original.data) {
parts[reader.ResPath(original.ref.BaseLoc)] = data parts[reader.ResPath(original.ref.BaseLoc)] = data
} }
} else { } else {
+32 -32
View File
@@ -46,32 +46,20 @@ import (
type SignatureVerifyReport struct { type SignatureVerifyReport struct {
DocIndex int DocIndex int
DocRoot string DocRoot string
PolicyChecked bool
PolicyOK bool
PolicyError string
CoverageChecked bool
CoverageOK bool
CoverageError string
CoveredFiles []string
UncoveredFiles []string
TimestampChecked bool
TimestampOK bool
Timestamps []SignatureTimestampReport
RevocationChecked bool
RevocationOK bool
Revocations []SignatureRevocationReport
ID string ID string
BaseLoc string BaseLoc string
Type SignType Type SignType
Provider SignatureProvider Valid bool
Error string
Signer string Signer string
SignCert SignatureCertInfo
SealCert SignatureCertInfo
SealInfo SignatureSealInfo
SealType string
SignatureMethod string
SignatureDateTime string SignatureDateTime string
SignatureTime time.Time SignatureTime time.Time
Provider SignatureProvider
SealInfo SignatureSealInfo
SealType string
SignCert SignatureCertInfo
SealCert SignatureCertInfo
SignatureMethod string
DigestMethod string DigestMethod string
References []SignatureReferenceVerify References []SignatureReferenceVerify
Stamps []SignatureStamp Stamps []SignatureStamp
@@ -82,25 +70,37 @@ type SignatureVerifyReport struct {
DataHashOK bool DataHashOK bool
SignedValueChecked bool SignedValueChecked bool
SignedValueOK bool SignedValueOK bool
SealChecked bool
SealOK bool
SealMatchChecked bool
SealMatchOK bool
CertChecked bool CertChecked bool
CertOK bool CertOK bool
SignatureTimeChecked bool
SignatureTimeOK bool
SealCertTimeChecked bool
SealCertTimeOK bool
SealTimeChecked bool
SealTimeOK bool
CertTimeChecked bool CertTimeChecked bool
CertTimeOK bool CertTimeOK bool
CertTrustChecked bool CertTrustChecked bool
CertTrustOK bool CertTrustOK bool
CertTrustError string CertTrustError string
Valid bool SignatureTimeChecked bool
Error string SignatureTimeOK bool
SealChecked bool
SealOK bool
SealMatchChecked bool
SealMatchOK bool
SealTimeChecked bool
SealTimeOK bool
SealCertTimeChecked bool
SealCertTimeOK bool
CoverageChecked bool
CoverageOK bool
CoverageError string
CoveredFiles []string
UncoveredFiles []string
PolicyChecked bool
PolicyOK bool
PolicyError string
TimestampChecked bool
TimestampOK bool
Timestamps []SignatureTimestampReport
RevocationChecked bool
RevocationOK bool
Revocations []SignatureRevocationReport
} }
// IntegrityValid 判断签名完整性是否有效 // IntegrityValid 判断签名完整性是否有效