mirror of
https://github.com/xiaoqidun/ofdgo.git
synced 2026-10-29 16:44:23 +08:00
This commit is contained in:
1 parent
d6f70de7be
commit
3927929822
30 files changed
+4707
-1161
No files matched your search
@@ -353,8 +353,59 @@
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog id="signPanel" class="form-dialog security-dialog" aria-label="签章">
|
||||
<form id="signForm" autocomplete="off">
|
||||
<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="signKeyPassword">私钥口令</label><input id="signKeyPassword" type="password" autocomplete="off"></div>
|
||||
<div class="form-row"><label for="signRoots">信任根</label><input id="signRoots" type="file" accept=".pem,.cer,.crt,.der" multiple required></div>
|
||||
<div class="form-row"><label for="signIntermediates">中间证书</label><input id="signIntermediates" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
|
||||
<div class="form-row"><label for="signSeal">电子印章</label><input id="signSeal" type="file" accept=".esl,.ses,.der" aria-label="SES电子印章"></div>
|
||||
<div id="signPlacementFields" hidden>
|
||||
<div class="form-row"><label for="signPlacement">位置</label><select id="signPlacement"><option value="normal">页面</option><option value="seam">骑缝</option></select></div>
|
||||
<div class="form-row"><label for="signPages">页码</label><input id="signPages" type="text" autocomplete="off" spellcheck="false"></div>
|
||||
<div class="form-row"><label for="signX">横坐标</label><div class="form-unit"><input id="signX" type="number" min="0" step="any" value="10" aria-label="横坐标(毫米)"><span>mm</span></div></div>
|
||||
<div class="form-row"><label for="signY">纵坐标</label><div class="form-unit"><input id="signY" type="number" min="0" step="any" value="10" aria-label="纵坐标(毫米)"><span>mm</span></div></div>
|
||||
<div class="form-row"><label for="signWidth">宽度</label><div class="form-unit"><input id="signWidth" type="number" min="0.01" step="any" value="40" aria-label="宽度(毫米)"><span>mm</span></div></div>
|
||||
<div class="form-row"><label for="signHeight">高度</label><div class="form-unit"><input id="signHeight" type="number" min="0.01" step="any" value="40" aria-label="高度(毫米)"><span>mm</span></div></div>
|
||||
</div>
|
||||
<div class="form-row"><label for="signMode">方式</label><select id="signMode"><option value="append">追加</option><option value="replace">替换</option></select></div>
|
||||
<div class="form-row"><label for="signLock">禁止追加</label><input id="signLock" type="checkbox" aria-label="禁止追加签名"></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>
|
||||
</dialog>
|
||||
<dialog id="verifyPanel" class="form-dialog security-dialog" aria-label="验签">
|
||||
<form id="verifyForm">
|
||||
<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="verifyTimeRoots">时间戳根</label><input id="verifyTimeRoots" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
|
||||
<div class="form-row"><label for="verifyTokens">时间戳</label><input id="verifyTokens" type="file" accept=".tsr,.tst,.der" multiple></div>
|
||||
<div class="form-row"><label for="verifyCRLs">撤销表</label><input id="verifyCRLs" type="file" accept=".crl,.der" multiple></div>
|
||||
<div class="form-row"><label for="verifyOCSP">OCSP响应</label><input id="verifyOCSP" type="file" accept=".ocsp,.der" multiple></div>
|
||||
<div class="form-row"><span>要求</span><div class="form-options"><label><input id="verifyRequireTime" type="checkbox"><span>时间戳</span></label><label><input id="verifyRequireRevocation" type="checkbox"><span>撤销检查</span></label></div></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>
|
||||
</dialog>
|
||||
<dialog id="credentialsPanel" class="form-dialog security-dialog" aria-label="解锁文档">
|
||||
<form id="credentialsForm" autocomplete="off">
|
||||
<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 id="credentialsPasswordRow" class="form-row"><label for="credentialsPassword">口令</label><input id="credentialsPassword" type="password" autocomplete="off" required autofocus></div>
|
||||
<div id="credentialsCertificateRow" class="form-row" hidden><label for="credentialsCertificate">证书</label><input id="credentialsCertificate" type="file" accept=".pem,.cer,.crt,.der"></div>
|
||||
<div id="credentialsKeyRow" class="form-row" hidden><label for="credentialsKey">私钥</label><input id="credentialsKey" type="file" accept=".pem,.key,.der"></div>
|
||||
<div id="credentialsKeyPasswordRow" class="form-row" hidden><label for="credentialsKeyPassword">私钥口令</label><input id="credentialsKeyPassword" type="password" autocomplete="off"></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>
|
||||
</dialog>
|
||||
<dialog id="exportPanel" class="form-dialog" aria-label="导出文档">
|
||||
<form id="exportForm">
|
||||
<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 id="encryptionUserRow" class="form-row"><label for="encryptionUser">用户</label><input id="encryptionUser" type="text" autocomplete="off" spellcheck="false"></div>
|
||||
<div id="encryptionPasswordRow" class="form-row"><label for="encryptionPassword">口令</label><input id="encryptionPassword" type="password" autocomplete="new-password"></div>
|
||||
<div id="encryptionConfirmRow" class="form-row"><label for="encryptionConfirm">确认</label><input id="encryptionConfirm" type="password" autocomplete="new-password"></div>
|
||||
<div id="encryptionCertificatesRow" class="form-row" hidden><label for="encryptionCertificates">证书</label><input id="encryptionCertificates" type="file" accept=".pem,.cer,.crt,.der" multiple></div>
|
||||
</div>
|
||||
<div class="form-row">
|
||||
<span id="exportScopeLabel">范围</span>
|
||||
<div class="form-options" role="radiogroup" aria-labelledby="exportScopeLabel">
|
||||
@@ -500,6 +551,10 @@
|
||||
<dd id="metaFonts">0</dd>
|
||||
</div>
|
||||
</dl>
|
||||
<section id="securityPanel" class="security-panel" aria-labelledby="securityPanelTitle" hidden>
|
||||
<div class="panel-row"><div id="securityPanelTitle" class="panel-title compact">安全</div><div class="panel-actions"><button id="encryptSaveButton" class="small-button" type="button" title="加密另存" aria-label="加密另存" aria-haspopup="dialog" aria-controls="exportPanel" disabled>加密</button><button id="signButton" class="small-button" type="button" aria-haspopup="dialog" aria-controls="signPanel" disabled>签章</button><button id="verifyButton" class="small-button" type="button" aria-haspopup="dialog" aria-controls="verifyPanel" disabled>验签</button></div></div>
|
||||
<dl class="meta-list"><div><dt>加密</dt><dd id="securityEncryption">未加密</dd></div><div id="securityMethodRow" hidden><dt>算法</dt><dd id="securityMethod"></dd></div></dl>
|
||||
</section>
|
||||
<section id="attachmentPanel" class="attachment-panel" aria-labelledby="attachmentPanelTitle" hidden>
|
||||
<div id="attachmentPanelTitle" class="panel-title">附件</div>
|
||||
<div id="attachmentList" class="attachment-list"></div>
|
||||
|
||||
+66
-22
@@ -645,6 +645,23 @@ button:not(:disabled):hover {
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.security-dialog {
|
||||
width: 360px;
|
||||
}
|
||||
|
||||
.security-dialog .form-row {
|
||||
grid-template-columns: 4em minmax(0, 1fr);
|
||||
}
|
||||
|
||||
#verifyForm .form-row {
|
||||
grid-template-columns: 5em minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.security-dialog .form-row > label,
|
||||
.security-dialog .form-row > span {
|
||||
text-align-last: justify;
|
||||
}
|
||||
|
||||
.form-row {
|
||||
display: grid;
|
||||
grid-template-columns: 2em minmax(0, 1fr);
|
||||
@@ -711,6 +728,7 @@ button:not(:disabled):hover {
|
||||
}
|
||||
|
||||
.form-row input[type="text"],
|
||||
.form-row input[type="password"],
|
||||
.form-row input[type="number"],
|
||||
.form-row input[type="file"],
|
||||
.form-row select,
|
||||
@@ -895,6 +913,8 @@ body[data-hide-meta] .meta-panel {
|
||||
border-right: 1px solid var(--line);
|
||||
background: var(--panel);
|
||||
overflow: auto;
|
||||
font-size: 13px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.page-list-panel {
|
||||
@@ -1168,7 +1188,7 @@ body[data-hide-meta] .meta-panel {
|
||||
gap: 6px;
|
||||
margin: 8px 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
@@ -1205,7 +1225,7 @@ body[data-hide-meta] .meta-panel {
|
||||
display: block;
|
||||
margin-bottom: 3px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.search-result mark {
|
||||
@@ -1284,7 +1304,13 @@ button.outline-link:hover {
|
||||
transition: border-color 0.16s ease, background-color 0.16s ease;
|
||||
}
|
||||
|
||||
.page-list-item[aria-current] {
|
||||
.page-list-item:not(:disabled):hover {
|
||||
border-color: var(--line-strong);
|
||||
background: #f0f5f4;
|
||||
}
|
||||
|
||||
.page-list-item[aria-current],
|
||||
.page-list-item[aria-current]:hover {
|
||||
border-color: var(--accent);
|
||||
color: var(--accent-strong);
|
||||
font-weight: 700;
|
||||
@@ -1297,7 +1323,8 @@ button.outline-link:hover {
|
||||
}
|
||||
|
||||
.page-list-item[aria-current]:not([aria-pressed="false"]),
|
||||
.page-list-item[aria-pressed="true"] {
|
||||
.page-list-item[aria-pressed="true"],
|
||||
.page-list-item[aria-pressed="true"]:hover {
|
||||
border-color: var(--accent);
|
||||
background: #e4f3ef;
|
||||
color: var(--accent-strong);
|
||||
@@ -1351,7 +1378,7 @@ button.outline-link:hover {
|
||||
}
|
||||
|
||||
.thumb-size {
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
color: var(--text-muted);
|
||||
font-weight: 400;
|
||||
}
|
||||
@@ -2037,7 +2064,8 @@ button.outline-link:hover {
|
||||
.font-panel,
|
||||
.doc-font-panel,
|
||||
.attachment-panel,
|
||||
.signature-panel {
|
||||
.signature-panel,
|
||||
.security-panel {
|
||||
margin-top: 18px;
|
||||
padding-top: 12px;
|
||||
border-top: 1px solid var(--line);
|
||||
@@ -2070,7 +2098,7 @@ button.outline-link:hover {
|
||||
|
||||
.attachment-detail {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.count-pill {
|
||||
@@ -2083,7 +2111,7 @@ button.outline-link:hover {
|
||||
border: 1px solid var(--line);
|
||||
background: #ffffff;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -2104,7 +2132,6 @@ button.outline-link:hover {
|
||||
transition: border-color 0.16s ease, background-color 0.16s ease;
|
||||
}
|
||||
|
||||
.page-list-item:not(:disabled):hover,
|
||||
.doc-font-row:hover,
|
||||
.signature-row:hover,
|
||||
.font-row:hover {
|
||||
@@ -2194,10 +2221,6 @@ button.outline-link:hover {
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.doc-font-badges,
|
||||
.signature-badges {
|
||||
float: right;
|
||||
margin-left: 8px;
|
||||
}
|
||||
@@ -2210,7 +2233,7 @@ button.outline-link:hover {
|
||||
padding: 3px 6px;
|
||||
background: #eef1f4;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -2235,19 +2258,40 @@ button.outline-link:hover {
|
||||
|
||||
.info-line {
|
||||
display: grid;
|
||||
grid-template-columns: 30px minmax(0, 1fr);
|
||||
grid-template-columns: 2em minmax(0, 1fr);
|
||||
align-items: start;
|
||||
gap: 7px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.info-label {
|
||||
color: #7b8791;
|
||||
color: var(--text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.meta-panel .info-line {
|
||||
grid-template-columns: 4em minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.meta-panel .info-label {
|
||||
text-align-last: justify;
|
||||
}
|
||||
|
||||
.signature-row details {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.signature-row summary {
|
||||
cursor: pointer;
|
||||
color: var(--accent-strong);
|
||||
}
|
||||
|
||||
.signature-row details[open] > .info-line {
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.info-value {
|
||||
min-width: 0;
|
||||
overflow-wrap: anywhere;
|
||||
@@ -2263,8 +2307,8 @@ button.outline-link:hover {
|
||||
|
||||
.doc-font-detail {
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
line-height: 1.35;
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@@ -2337,7 +2381,7 @@ button.outline-link:hover {
|
||||
border: 1px solid var(--line);
|
||||
background: #f8fafb;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
text-align: center;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@@ -2348,7 +2392,7 @@ button.outline-link:hover {
|
||||
gap: 3px;
|
||||
height: 26px;
|
||||
color: var(--text-muted);
|
||||
font-size: 11px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
|
||||
+498
-42
@@ -24,6 +24,7 @@ let wasmWorker = null;
|
||||
let wasmRequestID = 0;
|
||||
let wasmRecoveryTimer = 0;
|
||||
let textMeasure = null;
|
||||
let credentialRequest = null;
|
||||
|
||||
const state = {
|
||||
composite: null,
|
||||
@@ -35,6 +36,8 @@ const state = {
|
||||
wasmRecovering: false,
|
||||
wasmRecoveries: 0,
|
||||
exporting: false,
|
||||
signing: false,
|
||||
signCanceled: false,
|
||||
exportRequestID: 0,
|
||||
editing: false,
|
||||
selectObjects: true,
|
||||
@@ -44,6 +47,7 @@ const state = {
|
||||
savedRevision: null,
|
||||
insertObject: null,
|
||||
importPageCount: 0,
|
||||
importEncrypted: false,
|
||||
importing: false,
|
||||
pageSelection: new Set(),
|
||||
pageSelectionAnchor: null,
|
||||
@@ -102,11 +106,74 @@ const state = {
|
||||
exportFormats: [],
|
||||
exportPages: null,
|
||||
exportBackdrop: false,
|
||||
exportEncrypted: false,
|
||||
showPages: !COMPACT_LAYOUT.matches,
|
||||
showMeta: !COMPACT_LAYOUT.matches,
|
||||
};
|
||||
|
||||
const el = {
|
||||
signButton: document.querySelector("#signButton"),
|
||||
signPanel: document.querySelector("#signPanel"),
|
||||
signForm: document.querySelector("#signForm"),
|
||||
signCertificate: document.querySelector("#signCertificate"),
|
||||
signKey: document.querySelector("#signKey"),
|
||||
signKeyPassword: document.querySelector("#signKeyPassword"),
|
||||
signRoots: document.querySelector("#signRoots"),
|
||||
signIntermediates: document.querySelector("#signIntermediates"),
|
||||
signSeal: document.querySelector("#signSeal"),
|
||||
signPlacementFields: document.querySelector("#signPlacementFields"),
|
||||
signPlacement: document.querySelector("#signPlacement"),
|
||||
signPages: document.querySelector("#signPages"),
|
||||
signX: document.querySelector("#signX"),
|
||||
signY: document.querySelector("#signY"),
|
||||
signWidth: document.querySelector("#signWidth"),
|
||||
signHeight: document.querySelector("#signHeight"),
|
||||
signMode: document.querySelector("#signMode"),
|
||||
signLock: document.querySelector("#signLock"),
|
||||
signStatus: document.querySelector("#signStatus"),
|
||||
signCancel: document.querySelector("#signCancel"),
|
||||
verifyButton: document.querySelector("#verifyButton"),
|
||||
verifyPanel: document.querySelector("#verifyPanel"),
|
||||
verifyForm: document.querySelector("#verifyForm"),
|
||||
verifyRoots: document.querySelector("#verifyRoots"),
|
||||
verifyCerts: document.querySelector("#verifyCerts"),
|
||||
verifyTimeRoots: document.querySelector("#verifyTimeRoots"),
|
||||
verifyTokens: document.querySelector("#verifyTokens"),
|
||||
verifyCRLs: document.querySelector("#verifyCRLs"),
|
||||
verifyOCSP: document.querySelector("#verifyOCSP"),
|
||||
verifyRequireTime: document.querySelector("#verifyRequireTime"),
|
||||
verifyRequireRevocation: document.querySelector("#verifyRequireRevocation"),
|
||||
verifyStatus: document.querySelector("#verifyStatus"),
|
||||
verifyCancel: document.querySelector("#verifyCancel"),
|
||||
securityPanel: document.querySelector("#securityPanel"),
|
||||
securityEncryption: document.querySelector("#securityEncryption"),
|
||||
securityMethod: document.querySelector("#securityMethod"),
|
||||
securityMethodRow: document.querySelector("#securityMethodRow"),
|
||||
encryptSaveButton: document.querySelector("#encryptSaveButton"),
|
||||
credentialsPanel: document.querySelector("#credentialsPanel"),
|
||||
credentialsType: document.querySelector("#credentialsType"),
|
||||
credentialsPasswordRow: document.querySelector("#credentialsPasswordRow"),
|
||||
credentialsCertificateRow: document.querySelector("#credentialsCertificateRow"),
|
||||
credentialsKeyRow: document.querySelector("#credentialsKeyRow"),
|
||||
credentialsCertificate: document.querySelector("#credentialsCertificate"),
|
||||
credentialsKey: document.querySelector("#credentialsKey"),
|
||||
credentialsKeyPassword: document.querySelector("#credentialsKeyPassword"),
|
||||
credentialsKeyPasswordRow: document.querySelector("#credentialsKeyPasswordRow"),
|
||||
credentialsForm: document.querySelector("#credentialsForm"),
|
||||
credentialsUser: document.querySelector("#credentialsUser"),
|
||||
credentialsPassword: document.querySelector("#credentialsPassword"),
|
||||
credentialsStatus: document.querySelector("#credentialsStatus"),
|
||||
credentialsCancel: document.querySelector("#credentialsCancel"),
|
||||
encryptionFields: document.querySelector("#encryptionFields"),
|
||||
encryptionType: document.querySelector("#encryptionType"),
|
||||
encryptionUserRow: document.querySelector("#encryptionUserRow"),
|
||||
encryptionPasswordRow: document.querySelector("#encryptionPasswordRow"),
|
||||
encryptionConfirmRow: document.querySelector("#encryptionConfirmRow"),
|
||||
encryptionCertificatesRow: document.querySelector("#encryptionCertificatesRow"),
|
||||
encryptionCertificates: document.querySelector("#encryptionCertificates"),
|
||||
encryptionUser: document.querySelector("#encryptionUser"),
|
||||
encryptionPassword: document.querySelector("#encryptionPassword"),
|
||||
encryptionConfirm: document.querySelector("#encryptionConfirm"),
|
||||
ofdInput: document.querySelector("#ofdInput"),
|
||||
ofdButton: document.querySelector("#ofdButton"),
|
||||
newButton: document.querySelector("#newButton"),
|
||||
@@ -755,6 +822,7 @@ el.importPanel.addEventListener("cancel", event => {
|
||||
});
|
||||
el.importPanel.addEventListener("close", () => {
|
||||
state.importPageCount = 0;
|
||||
state.importEncrypted = false;
|
||||
el.importForm.reset();
|
||||
if (state.ready) callWASM("ofdgoLoadImport", null).catch(err => showError(err, false));
|
||||
});
|
||||
@@ -770,7 +838,12 @@ el.importForm.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
if (document.body.hasAttribute("aria-busy") || !state.importPageCount || !el.importForm.reportValidity()) return;
|
||||
const position = { before:state.pageIndex, after:state.pageIndex+1, first:0, last:state.doc.pageCount }[el.importPosition.value];
|
||||
const saved = await changeDocument("ofdgoImportPages", null, el.importRange.value === "custom" ? el.importPages.value : "", position, el.importOutlines.checked);
|
||||
const options = [];
|
||||
if (state.importEncrypted) {
|
||||
if (!window.confirm(state.doc.encryption?.encrypted ? "按目标文档的加密策略导入,是否继续?" : "目标未加密,导入内容将按明文保存,是否继续?")) return;
|
||||
options.push(true);
|
||||
}
|
||||
const saved = await changeDocument("ofdgoImportPages", null, el.importRange.value === "custom" ? el.importPages.value : "", position, el.importOutlines.checked, ...options);
|
||||
if (saved) el.importPanel.close();
|
||||
});
|
||||
editorClick(el.copyPageButton, () => changeSelectedPages("copy"));
|
||||
@@ -926,6 +999,60 @@ el.insertPanel.addEventListener("close", () => { state.insertObject = null; });
|
||||
el.insertForm.addEventListener("submit", insertObject);
|
||||
el.textFontAdd.addEventListener("click", () => openFontFile(el.fontInput));
|
||||
el.saveButton.addEventListener("click", () => exportFile(true, null, "ofd"));
|
||||
el.encryptSaveButton.addEventListener("click", () => openExportPanel(true));
|
||||
el.signButton.addEventListener("click", () => {
|
||||
if (!state.doc || document.body.hasAttribute("aria-busy")) return;
|
||||
el.signForm.reset();
|
||||
el.signPages.value = state.editing && state.pageSelection.size ? selectedPageRange() : String(state.pageIndex + 1);
|
||||
el.signStatus.textContent = "";
|
||||
updateSignPlacement();
|
||||
el.signPanel.showModal();
|
||||
});
|
||||
el.signSeal.addEventListener("change", updateSignPlacement);
|
||||
el.signPlacement.addEventListener("change", updateSignPlacement);
|
||||
el.signCancel.addEventListener("click", () => state.signing ? cancelSigning() : el.signPanel.close());
|
||||
el.signPanel.addEventListener("cancel", event => {
|
||||
if (state.signing) { event.preventDefault(); cancelSigning(); }
|
||||
});
|
||||
el.signPanel.addEventListener("close", () => { if (!el.signPanel.open) el.signForm.reset(); });
|
||||
el.signForm.addEventListener("submit", signDocument);
|
||||
el.verifyButton.addEventListener("click", () => {
|
||||
if (!state.doc || document.body.hasAttribute("aria-busy")) return;
|
||||
el.verifyForm.reset();
|
||||
el.verifyStatus.textContent = "";
|
||||
el.verifyPanel.showModal();
|
||||
});
|
||||
el.verifyCancel.addEventListener("click", () => el.verifyPanel.close());
|
||||
el.verifyPanel.addEventListener("close", () => { if (!el.verifyPanel.open) el.verifyForm.reset(); });
|
||||
el.verifyForm.addEventListener("submit", verifyDocument);
|
||||
el.credentialsCancel.addEventListener("click", () => finishCredentials(null));
|
||||
el.credentialsPanel.addEventListener("cancel", event => { event.preventDefault(); finishCredentials(null); });
|
||||
el.credentialsPanel.addEventListener("close", () => { if (!el.credentialsPanel.open) finishCredentials(null); });
|
||||
el.credentialsType.addEventListener("change", updateCredentialsType);
|
||||
el.credentialsForm.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
if (!credentialRequest) return;
|
||||
if (el.credentialsType.value === "certificate") {
|
||||
const request = credentialRequest;
|
||||
let key;
|
||||
const keyPassword = new TextEncoder().encode(el.credentialsKeyPassword.value);
|
||||
el.credentialsKeyPassword.value = "";
|
||||
try {
|
||||
key = (await securityFiles(el.credentialsKey))[0];
|
||||
const certificate = (await securityFiles(el.credentialsCertificate))[0];
|
||||
if (credentialRequest !== request) { key?.fill(0); keyPassword.fill(0); return; }
|
||||
if (!key || !certificate) { el.credentialsStatus.textContent = "请选择证书和私钥"; key?.fill(0); keyPassword.fill(0); return; }
|
||||
finishCredentials({password:new Uint8Array(), key, keyPassword, certificate, userName:el.credentialsUser.value.trim()});
|
||||
} catch {
|
||||
key?.fill(0);
|
||||
keyPassword.fill(0);
|
||||
if (credentialRequest === request) el.credentialsStatus.textContent = "文件读取失败";
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!el.credentialsPassword.value) return;
|
||||
finishCredentials({ password: new TextEncoder().encode(el.credentialsPassword.value), userName: el.credentialsUser.value.trim() });
|
||||
});
|
||||
el.togglePagesButton.addEventListener("click", () => toggleSidebar("pages"));
|
||||
el.toggleMetaButton.addEventListener("click", () => toggleSidebar("meta"));
|
||||
el.pagesTab.addEventListener("click", () => showNavigation(el.pagesTab));
|
||||
@@ -983,8 +1110,10 @@ el.viewerPanel.addEventListener("paste", pasteEditorContent);
|
||||
document.addEventListener("selectionchange", syncSelection);
|
||||
el.exportFormat.addEventListener("change", () => updateDPIControl());
|
||||
el.exportPageButton.addEventListener("click", () => exportFile(false));
|
||||
el.exportButton.addEventListener("click", openExportPanel);
|
||||
el.exportButton.addEventListener("click", () => openExportPanel());
|
||||
el.exportCancel.addEventListener("click", () => el.exportPanel.close());
|
||||
el.exportPanel.addEventListener("close", () => { if (!el.exportPanel.open) clearEncryptionForm(); });
|
||||
el.encryptionType.addEventListener("change", updateEncryptionType);
|
||||
el.cancelExportButton.addEventListener("click", cancelExport);
|
||||
el.exportPanel.addEventListener("pointerdown", (event) => {
|
||||
state.exportBackdrop = event.target === el.exportPanel;
|
||||
@@ -1000,9 +1129,32 @@ el.exportSpecified.addEventListener("change", () => {
|
||||
el.exportRange.focus();
|
||||
});
|
||||
el.exportRange.addEventListener("input", updateExportRange);
|
||||
el.exportForm.addEventListener("submit", (event) => {
|
||||
el.exportForm.addEventListener("submit", async (event) => {
|
||||
event.preventDefault();
|
||||
if (!el.exportSubmit.disabled) {
|
||||
if (state.exportEncrypted) {
|
||||
const certificates = el.encryptionType.value === "certificate";
|
||||
if (certificates && !el.encryptionCertificates.files?.length) {
|
||||
el.exportRangeStatus.textContent = "请选择接收者证书";
|
||||
return;
|
||||
}
|
||||
if (!certificates && !el.encryptionPassword.value) {
|
||||
el.exportRangeStatus.textContent = "请输入口令";
|
||||
el.encryptionPassword.focus();
|
||||
return;
|
||||
}
|
||||
if (!certificates && el.encryptionPassword.value !== el.encryptionConfirm.value) {
|
||||
el.exportRangeStatus.textContent = "口令不一致";
|
||||
el.encryptionConfirm.focus();
|
||||
return;
|
||||
}
|
||||
const encryption = certificates
|
||||
? {password:new Uint8Array(), userName:"", recipientFiles:Array.from(el.encryptionCertificates.files)}
|
||||
: { password: new TextEncoder().encode(el.encryptionPassword.value), userName: el.encryptionUser.value.trim() };
|
||||
clearEncryptionForm();
|
||||
try { return await exportFile(true, state.exportPages, "ofd", encryption); }
|
||||
finally { encryption.password.fill(0); }
|
||||
}
|
||||
return exportFile(true, state.exportPages);
|
||||
}
|
||||
});
|
||||
@@ -1317,18 +1469,20 @@ async function loadImportFile() {
|
||||
const file = el.importFile.files[0];
|
||||
const openSeq = state.openSeq;
|
||||
state.importPageCount = 0;
|
||||
state.importEncrypted = false;
|
||||
el.importSubmit.disabled = true;
|
||||
el.importStatus.textContent = "";
|
||||
setBusy(true);
|
||||
try {
|
||||
const data = file ? new Uint8Array(await file.arrayBuffer()) : null;
|
||||
if (openSeq !== state.openSeq) return;
|
||||
const info = await callWASM("ofdgoLoadImport", data);
|
||||
const info = data ? await openWithCredentials(null, openSeq, data, true) : await callWASM("ofdgoLoadImport", null);
|
||||
if (openSeq !== state.openSeq || !el.importPanel.open) {
|
||||
await callWASM("ofdgoLoadImport", null);
|
||||
return;
|
||||
}
|
||||
state.importPageCount = info?.pageCount || 0;
|
||||
state.importEncrypted = Boolean(info?.encrypted);
|
||||
el.importPages.title = state.importPageCount ? `页码 1-${state.importPageCount}` : "页码";
|
||||
el.importStatus.textContent = info?.signed ? "保留签章外观,原签名不再有效" : "";
|
||||
el.importSubmit.disabled = !state.importPageCount;
|
||||
@@ -1494,6 +1648,7 @@ async function createDocument(event) {
|
||||
return;
|
||||
}
|
||||
const title = el.createName.value.trim() || "未命名";
|
||||
clearSecurityForms();
|
||||
const openSeq = ++state.openSeq;
|
||||
setBusy(true, "正在新建文档", null, "正在新建文档");
|
||||
try {
|
||||
@@ -2714,10 +2869,11 @@ async function loadWASM() {
|
||||
if (wasmRequests.get(data.id)?.openSeq === state.openSeq && state.exporting) {
|
||||
if (data.stage === "save") {
|
||||
el.cancelExportButton.disabled = true;
|
||||
if (state.signing) el.signCancel.disabled = true;
|
||||
setProgress("正在保存", null);
|
||||
} else if (!el.cancelExportButton.disabled) {
|
||||
if (data.stage === "prepare") {
|
||||
const label = { snapshot: "正在准备", ids: "正在检查", commit: "正在整理", fonts: "正在处理字体", pages: "正在处理页面", references: "正在检查引用", resources: "正在整理资源", write: "正在写入" }[data.phase];
|
||||
const label = { snapshot: "正在准备", ids: "正在检查", commit: "正在整理", fonts: "正在处理字体", pages: "正在处理页面", references: "正在检查引用", resources: "正在整理资源", write: "正在写入", sign: "正在签署", encrypt: "正在加密" }[data.phase];
|
||||
setProgress(label, data.total ? data.completed / data.total * 100 : null);
|
||||
} else if (data.completed === data.total) {
|
||||
setProgress("正在收尾", null);
|
||||
@@ -2753,6 +2909,7 @@ async function loadWASM() {
|
||||
request.resolve(data.data);
|
||||
} else {
|
||||
const err = new Error(missingGlyphMessage(data.missingGlyphs) || (data.reasonCode === "layoutRequired" ? "跨段或换行修改需先设置段落排版" : data.error));
|
||||
err.code = data.code;
|
||||
if (data.canceled) {
|
||||
err.name = "AbortError";
|
||||
}
|
||||
@@ -2768,6 +2925,7 @@ function markWASMExited(wasmSeq = state.wasmSeq, err) {
|
||||
return;
|
||||
}
|
||||
state.ready = false;
|
||||
clearSecurityForms();
|
||||
state.wasmExited = true;
|
||||
wasmWorker?.terminate();
|
||||
wasmWorker = null;
|
||||
@@ -2830,6 +2988,7 @@ async function openOFD(file) {
|
||||
if (!discardChanges()) {
|
||||
return;
|
||||
}
|
||||
clearSecurityForms();
|
||||
state.wasmRecoveries = 0;
|
||||
const openSeq = ++state.openSeq;
|
||||
setBusy(true, "正在读取文档", 10, STATUS.opening);
|
||||
@@ -3306,6 +3465,86 @@ function updateFontPermissionHint() {
|
||||
el.fontPermissionHint.hidden = !fontManager.canReadLocal() || fontManager.permission === "granted";
|
||||
}
|
||||
|
||||
function clearSecurityForms() {
|
||||
finishCredentials(null);
|
||||
clearEncryptionForm();
|
||||
el.signKey.value = "";
|
||||
el.signKeyPassword.value = "";
|
||||
if (el.signPanel.open) el.signPanel.close();
|
||||
if (el.verifyPanel.open) el.verifyPanel.close();
|
||||
}
|
||||
|
||||
function updateCredentialsType() {
|
||||
const certificate = el.credentialsType.value === "certificate";
|
||||
el.credentialsStatus.textContent = "";
|
||||
el.credentialsPasswordRow.hidden = certificate;
|
||||
el.credentialsCertificateRow.hidden = el.credentialsKeyRow.hidden = !certificate;
|
||||
el.credentialsKeyPasswordRow.hidden = !certificate;
|
||||
el.credentialsPassword.required = !certificate;
|
||||
el.credentialsCertificate.required = el.credentialsKey.required = certificate;
|
||||
el.credentialsPassword.value = "";
|
||||
el.credentialsCertificate.value = el.credentialsKey.value = "";
|
||||
el.credentialsKeyPassword.value = "";
|
||||
}
|
||||
|
||||
function finishCredentials(credentials) {
|
||||
const request = credentialRequest;
|
||||
credentialRequest = null;
|
||||
el.credentialsPassword.value = "";
|
||||
el.credentialsUser.value = "";
|
||||
el.credentialsCertificate.value = el.credentialsKey.value = "";
|
||||
el.credentialsKeyPassword.value = "";
|
||||
if (el.credentialsPanel.open) el.credentialsPanel.close();
|
||||
if (request && request.openSeq === state.openSeq) request.resolve(credentials);
|
||||
else {
|
||||
credentials?.password.fill(0);
|
||||
credentials?.key?.fill(0);
|
||||
credentials?.keyPassword?.fill(0);
|
||||
request?.resolve(null);
|
||||
}
|
||||
}
|
||||
|
||||
function requestCredentials(code, openSeq) {
|
||||
finishCredentials(null);
|
||||
el.credentialsStatus.textContent = code === "invalidCredentials" ? "解锁失败,请重试" : "";
|
||||
el.progressPanel.hidden = true;
|
||||
return new Promise(resolve => {
|
||||
credentialRequest = { resolve, openSeq };
|
||||
el.credentialsPanel.showModal();
|
||||
(el.credentialsType.value === "certificate" ? el.credentialsCertificate : el.credentialsPassword).focus();
|
||||
});
|
||||
}
|
||||
|
||||
async function openWithCredentials(fonts, openSeq, data = state.ofdBytes, importing = false) {
|
||||
let credentials = null;
|
||||
try {
|
||||
while (openSeq === state.openSeq) {
|
||||
try {
|
||||
return importing ? await callWASM("ofdgoLoadImport", data.slice(), credentials)
|
||||
: await callWASM("ofdgoOpen", data, fonts, state.renderAnnotations, credentials);
|
||||
} catch (err) {
|
||||
if (openSeq !== state.openSeq || !["credentialsRequired", "invalidCredentials"].includes(err.code)) throw err;
|
||||
credentials?.password.fill(0);
|
||||
credentials?.key?.fill(0);
|
||||
credentials?.keyPassword?.fill(0);
|
||||
credentials = await requestCredentials(err.code, openSeq);
|
||||
if (!credentials || openSeq !== state.openSeq) {
|
||||
const canceled = new Error("打开已取消");
|
||||
canceled.name = "AbortError";
|
||||
throw canceled;
|
||||
}
|
||||
}
|
||||
}
|
||||
const canceled = new Error("打开已取消");
|
||||
canceled.name = "AbortError";
|
||||
throw canceled;
|
||||
} finally {
|
||||
credentials?.password.fill(0);
|
||||
credentials?.key?.fill(0);
|
||||
credentials?.keyPassword?.fill(0);
|
||||
}
|
||||
}
|
||||
|
||||
async function openDocument(options = {}) {
|
||||
if (!state.ofdBytes && !state.editorInfo) {
|
||||
return;
|
||||
@@ -3341,7 +3580,7 @@ async function openDocument(options = {}) {
|
||||
}
|
||||
const doc = options.doc || (options.reuseSession
|
||||
? await callWASM("ofdgoConfigure", fonts, state.renderAnnotations)
|
||||
: await callWASM("ofdgoOpen", state.ofdBytes, fonts, state.renderAnnotations));
|
||||
: await openWithCredentials(fonts, openSeq));
|
||||
if (openSeq !== state.openSeq) {
|
||||
return;
|
||||
}
|
||||
@@ -3439,6 +3678,12 @@ async function openDocument(options = {}) {
|
||||
return;
|
||||
}
|
||||
if (options.throwError) throw err;
|
||||
if (!options.reuseSession && !options.doc) {
|
||||
state.ofdBytes = null;
|
||||
state.doc = null;
|
||||
renderSecurity();
|
||||
updateControls();
|
||||
}
|
||||
showError(err, true);
|
||||
} finally {
|
||||
if (openSeq === state.openSeq) {
|
||||
@@ -3522,6 +3767,7 @@ async function downloadAttachment(attachment) {
|
||||
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
||||
return;
|
||||
}
|
||||
if (state.doc.encryption?.encrypted && !window.confirm("附件将按明文下载,是否继续?")) return;
|
||||
const openSeq = state.openSeq;
|
||||
state.exporting = true;
|
||||
updateControls();
|
||||
@@ -3558,11 +3804,165 @@ async function downloadAttachment(attachment) {
|
||||
}
|
||||
}
|
||||
|
||||
function openExportPanel() {
|
||||
function updateEncryptionType() {
|
||||
const certificates = el.encryptionType.value === "certificate";
|
||||
el.encryptionUserRow.hidden = el.encryptionPasswordRow.hidden = el.encryptionConfirmRow.hidden = certificates;
|
||||
el.encryptionCertificatesRow.hidden = !certificates;
|
||||
el.encryptionPassword.required = el.encryptionConfirm.required = state.exportEncrypted && !certificates;
|
||||
el.encryptionCertificates.required = state.exportEncrypted && certificates;
|
||||
el.encryptionPassword.value = el.encryptionConfirm.value = "";
|
||||
el.encryptionCertificates.value = "";
|
||||
}
|
||||
|
||||
async function encryptionRecipients(files) {
|
||||
const names = new Set();
|
||||
return Promise.all(files.map(async file => {
|
||||
const base = file.name.replace(/\.[^.]+$/, "") || "用户";
|
||||
let name = base, index = 2;
|
||||
while (names.has(name)) name = `${base} ${index++}`;
|
||||
names.add(name);
|
||||
return {userName:name, certificate:new Uint8Array(await file.arrayBuffer())};
|
||||
}));
|
||||
}
|
||||
|
||||
function clearEncryptionForm() {
|
||||
el.encryptionPassword.value = "";
|
||||
el.encryptionConfirm.value = "";
|
||||
el.encryptionUser.value = "";
|
||||
el.encryptionCertificates.value = "";
|
||||
state.exportEncrypted = false;
|
||||
}
|
||||
|
||||
function renderSecurity() {
|
||||
const encryption = state.doc?.encryption;
|
||||
el.securityPanel.hidden = !state.doc;
|
||||
el.securityEncryption.textContent = encryption?.encrypted ? "已加密" : "未加密";
|
||||
el.securityMethod.textContent = encryption?.method || "";
|
||||
el.securityMethodRow.hidden = !encryption?.method;
|
||||
el.encryptSaveButton.disabled = !state.doc || !state.ready || state.exporting || document.body.hasAttribute("aria-busy");
|
||||
el.verifyButton.disabled = el.encryptSaveButton.disabled;
|
||||
el.signButton.disabled = el.encryptSaveButton.disabled;
|
||||
}
|
||||
|
||||
function cancelSigning() {
|
||||
if (el.signCancel.disabled) return;
|
||||
state.signCanceled = true;
|
||||
el.signStatus.textContent = "正在取消";
|
||||
cancelExport();
|
||||
}
|
||||
|
||||
function updateSignPlacement() {
|
||||
const stamp = Boolean(el.signSeal.files?.length);
|
||||
el.signPlacementFields.hidden = !stamp;
|
||||
el.signPages.required = stamp;
|
||||
el.signX.disabled = !stamp || el.signPlacement.value === "seam";
|
||||
}
|
||||
|
||||
async function signDocument(event) {
|
||||
event.preventDefault();
|
||||
if (!state.doc || document.body.hasAttribute("aria-busy")) return;
|
||||
if (!el.signCertificate.files?.length || !el.signKey.files?.length || !el.signRoots.files?.length) {
|
||||
el.signStatus.textContent = "请选择证书、私钥和信任根";
|
||||
return;
|
||||
}
|
||||
if (el.signMode.value === "replace" && !window.confirm("替换现有签名,是否继续?")) return;
|
||||
let openSeq = state.openSeq;
|
||||
let key;
|
||||
const keyPassword = new TextEncoder().encode(el.signKeyPassword.value);
|
||||
el.signKeyPassword.value = "";
|
||||
state.exporting = true;
|
||||
state.signing = true;
|
||||
state.signCanceled = false;
|
||||
el.signCancel.disabled = false;
|
||||
setBusy(true, "正在签署", null, "正在签署");
|
||||
el.signForm.querySelectorAll("input, select, button[type=submit]").forEach(input => { input.disabled = true; });
|
||||
try {
|
||||
const name = `${baseFileName()}_签章.ofd`;
|
||||
const file = window.showSaveFilePicker ? await window.showSaveFilePicker({suggestedName:name, types:[{description:"OFD",accept:{"application/ofd":[".ofd"]}}]}) : null;
|
||||
if (openSeq !== state.openSeq || state.signCanceled) return;
|
||||
if (canvasEditor.input || canvasEditor.crop || canvasEditor.nudge || canvasEditor.nudgeCommit) {
|
||||
setBusy(false);
|
||||
if (!await canvasEditor.commitNudge() || !await canvasEditor.commitText() || !await canvasEditor.commitCrop()) return;
|
||||
openSeq = state.openSeq;
|
||||
setBusy(true, "正在签署", null, "正在签署");
|
||||
}
|
||||
key = (await securityFiles(el.signKey))[0];
|
||||
const options = {
|
||||
key, keyPassword, certificate:(await securityFiles(el.signCertificate))[0], roots:await securityFiles(el.signRoots),
|
||||
intermediates:await securityFiles(el.signIntermediates), seal:(await securityFiles(el.signSeal))[0] || null,
|
||||
pages:el.signPages.value.trim(), placement:el.signPlacement.value,
|
||||
x:Number(el.signX.value), y:Number(el.signY.value), width:Number(el.signWidth.value), height:Number(el.signHeight.value),
|
||||
mode:el.signMode.value, lock:el.signLock.checked,
|
||||
};
|
||||
el.signKey.value = "";
|
||||
if (openSeq !== state.openSeq || state.signCanceled || !el.signPanel.open) return;
|
||||
const result = await callWASM("ofdgoSaveSigned", null, options, file);
|
||||
if (openSeq !== state.openSeq) return;
|
||||
if (result.blob) downloadBytes(result.blob, result.mime, name);
|
||||
el.signPanel.close();
|
||||
setStatus("签署已另存");
|
||||
} catch (err) {
|
||||
if (openSeq === state.openSeq) el.signStatus.textContent = err.name === "AbortError" ? "签署已取消" : err.message;
|
||||
} finally {
|
||||
key?.fill(0);
|
||||
keyPassword.fill(0);
|
||||
el.signKey.value = "";
|
||||
el.signForm.querySelectorAll("input, select, button[type=submit]").forEach(input => { input.disabled = false; });
|
||||
el.signCancel.disabled = false;
|
||||
if (state.signCanceled && openSeq === state.openSeq) el.signStatus.textContent = "签署已取消";
|
||||
state.signing = false;
|
||||
updateSignPlacement();
|
||||
state.exporting = false;
|
||||
state.exportRequestID = 0;
|
||||
el.cancelExportButton.hidden = true;
|
||||
if (openSeq === state.openSeq) { setBusy(false); updateControls(); }
|
||||
}
|
||||
}
|
||||
|
||||
async function securityFiles(input) {
|
||||
return Promise.all(Array.from(input.files || [], async file => new Uint8Array(await file.arrayBuffer())));
|
||||
}
|
||||
|
||||
async function verifyDocument(event) {
|
||||
event.preventDefault();
|
||||
if (!state.doc || document.body.hasAttribute("aria-busy")) return;
|
||||
const openSeq = state.openSeq;
|
||||
setBusy(true, "正在验签", null, "正在验签");
|
||||
el.verifyForm.inert = true;
|
||||
try {
|
||||
const options = {
|
||||
roots: await securityFiles(el.verifyRoots), certificates: await securityFiles(el.verifyCerts),
|
||||
timestampRoots: await securityFiles(el.verifyTimeRoots), tokens: await securityFiles(el.verifyTokens),
|
||||
crls: await securityFiles(el.verifyCRLs), ocsp: await securityFiles(el.verifyOCSP),
|
||||
requireTimestamp: el.verifyRequireTime.checked, requireRevocation: el.verifyRequireRevocation.checked,
|
||||
};
|
||||
if (openSeq !== state.openSeq || !el.verifyPanel.open) return;
|
||||
const info = await callWASM("ofdgoVerifySignatures", options);
|
||||
if (openSeq !== state.openSeq || !el.verifyPanel.open) return;
|
||||
Object.assign(state.doc, info);
|
||||
renderMeta();
|
||||
el.verifyPanel.close();
|
||||
setStatus(info.signatureCount ? "验签完成" : "无签名");
|
||||
} catch (err) {
|
||||
if (openSeq === state.openSeq) el.verifyStatus.textContent = err.message;
|
||||
} finally {
|
||||
el.verifyForm.inert = false;
|
||||
if (openSeq === state.openSeq) setBusy(false);
|
||||
}
|
||||
}
|
||||
|
||||
function openExportPanel(encrypted = false) {
|
||||
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
||||
return;
|
||||
}
|
||||
el.exportForm.reset();
|
||||
clearEncryptionForm();
|
||||
state.exportEncrypted = encrypted;
|
||||
el.encryptionFields.hidden = !encrypted;
|
||||
el.encryptionPassword.required = el.encryptionConfirm.required = encrypted;
|
||||
updateEncryptionType();
|
||||
el.exportPanel.setAttribute("aria-label", encrypted ? "加密另存" : "导出文档");
|
||||
el.exportSubmit.textContent = encrypted ? "保存" : "导出";
|
||||
if (state.editing && state.pageSelection.size) {
|
||||
el.exportSpecified.checked = true;
|
||||
el.exportRange.value = selectedPageRange();
|
||||
@@ -3600,14 +4000,15 @@ async function updateExportRange() {
|
||||
}
|
||||
}
|
||||
|
||||
async function exportFile(whole, indices = null, value = el.exportFormat.value) {
|
||||
async function exportFile(whole, indices = null, value = el.exportFormat.value, encryption = null) {
|
||||
if (!state.doc || document.body.hasAttribute("aria-busy")) {
|
||||
return;
|
||||
}
|
||||
const saving = value === "ofd";
|
||||
if (saving && !state.editorInfo) {
|
||||
if (saving && !state.editorInfo && !encryption) {
|
||||
return;
|
||||
}
|
||||
if (!saving && state.doc.encryption?.encrypted && !window.confirm("此格式将输出明文,是否继续?")) return;
|
||||
const format = saving ? { value: "ofd", label: "OFD", extension: "ofd", mime: "application/ofd" } : exportFormatInfo(value);
|
||||
if (!format) {
|
||||
return;
|
||||
@@ -3640,7 +4041,12 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value)
|
||||
openSeq = state.openSeq;
|
||||
setBusy(true, `正在生成 ${label}`, null, status);
|
||||
}
|
||||
const result = saving ? await callWASM("ofdgoSaveDocument", indices, file) : whole
|
||||
if (encryption?.recipientFiles) {
|
||||
encryption.recipients = await encryptionRecipients(encryption.recipientFiles);
|
||||
delete encryption.recipientFiles;
|
||||
if (openSeq !== state.openSeq) return;
|
||||
}
|
||||
const result = encryption ? await callWASM("ofdgoSaveEncrypted", indices, encryption, file) : saving ? await callWASM("ofdgoSaveDocument", indices, file) : whole
|
||||
? await callWASM("ofdgoExportDocument", format.value, dpi, indices, state.renderBackend, file)
|
||||
: await callWASM("ofdgoExportPage", pageIndex, format.value, dpi, state.renderBackend, file);
|
||||
if (openSeq !== state.openSeq) {
|
||||
@@ -3649,7 +4055,7 @@ async function exportFile(whole, indices = null, value = el.exportFormat.value)
|
||||
if (result.blob) {
|
||||
downloadBytes(result.blob, result.mime, fileName);
|
||||
}
|
||||
if (saving && indices === null) {
|
||||
if (saving && indices === null && state.editorInfo) {
|
||||
state.savedRevision = state.editorInfo.revision;
|
||||
setDirty(false);
|
||||
}
|
||||
@@ -5344,6 +5750,7 @@ function markThumbnailError(index) {
|
||||
}
|
||||
|
||||
function renderMeta(keepDetails = false) {
|
||||
renderSecurity();
|
||||
const doc = state.doc || {};
|
||||
el.metaPanel.setAttribute("aria-busy", String(!!doc.detailsPending));
|
||||
document.title = `OFDGo WebUI - ${state.fileName}`;
|
||||
@@ -5440,41 +5847,88 @@ function renderSignatures() {
|
||||
|
||||
const badges = document.createElement("div");
|
||||
badges.className = "signature-badges";
|
||||
badges.append(fontBadge(signature.status === "valid" ? "通过" : signature.status === "invalid" ? "异常" : "未验", signature.status));
|
||||
badges.append(fontBadge(signature.trustedValid ? "可信" : signature.status === "valid" ? "完整" : signature.status === "invalid" ? "异常" : "未验", signature.status));
|
||||
|
||||
head.append(badges, name);
|
||||
row.append(head);
|
||||
appendInfoLine(row, "编号", signature.id);
|
||||
appendInfoLine(row, "版本", signature.version);
|
||||
appendInfoLine(row, "章图", signature.sealType);
|
||||
appendInfoLine(row, "章号", signature.sealId);
|
||||
appendInfoLine(row, "章名", signature.sealName);
|
||||
appendInfoLine(row, "厂商", signature.sealVendor);
|
||||
appendInfoLine(row, "签者", signature.signer);
|
||||
appendInfoLine(row, "签署人", signature.signer);
|
||||
appendInfoLine(row, "时间", formatDocumentTime(signature.signatureDateTime));
|
||||
appendInfoLine(row, "机构", signatureAgency(signature));
|
||||
appendSignatureCheck(row, "原文", signature.dataHashChecked, signature.dataHashOK);
|
||||
appendSignatureCheck(row, "签名", signature.signedValueChecked, signature.signedValueOK);
|
||||
if (signature.type !== "Sign") {
|
||||
appendSignatureCheck(row, "章验", signature.sealChecked, signature.sealOK);
|
||||
appendSignaturePolicy(row, "一致", signature.sealMatchChecked, signature.sealMatchOK);
|
||||
}
|
||||
appendSignatureCheck(row, "证书", signature.certChecked, signature.certOK);
|
||||
appendSignaturePolicy(row, "签期", signature.signatureTimeChecked, signature.signatureTimeOK);
|
||||
appendSignaturePolicy(row, "章期", signature.sealTimeChecked, signature.sealTimeOK);
|
||||
if (signature.sealCertTimeChecked) {
|
||||
appendInfoLine(row, "制期", signature.sealCertTimeOK ? "有效" : "失效", signature.sealCertTimeOK ? "ok" : "");
|
||||
}
|
||||
appendSignaturePolicy(row, "时效", signature.certTimeChecked, signature.certTimeOK);
|
||||
appendSignatureCheck(row, "信任", signature.certTrustChecked, signature.certTrustOK);
|
||||
appendInfoLine(row, "保护", signatureReferenceText(signature), signatureReferenceStatus(signature));
|
||||
appendInfoLine(row, "算法", signature.signatureMethod);
|
||||
appendInfoLine(row, "散列", signature.digestMethod);
|
||||
appendInfoLine(row, "序号", signature.signSerial);
|
||||
appendInfoLine(row, "主体", signature.signSubject && signature.signSubject !== signature.signer ? signature.signSubject : "");
|
||||
appendInfoLine(row, "颁发", signature.signIssuer);
|
||||
appendInfoLine(row, "章证", signature.sealSubject);
|
||||
appendInfoLine(row, "保护文件", signatureReferenceText(signature), signatureReferenceStatus(signature));
|
||||
appendInfoLine(row, "错误", signature.error, "fail");
|
||||
const checks = document.createElement("details");
|
||||
const checksTitle = document.createElement("summary");
|
||||
checksTitle.textContent = "校验详情";
|
||||
checks.append(checksTitle);
|
||||
appendSignatureCheck(checks, "原文", signature.dataHashChecked, signature.dataHashOK);
|
||||
appendSignatureCheck(checks, "签名", signature.signedValueChecked, signature.signedValueOK);
|
||||
if (signature.type !== "Sign") {
|
||||
appendSignatureCheck(checks, "印章", signature.sealChecked, signature.sealOK);
|
||||
appendSignaturePolicy(checks, "印章匹配", signature.sealMatchChecked, signature.sealMatchOK);
|
||||
}
|
||||
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.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.uncoveredFiles?.join("、"), "fail");
|
||||
row.append(checks);
|
||||
for (const timestamp of signature.timestamps || []) {
|
||||
const details = document.createElement("details");
|
||||
const title = document.createElement("summary");
|
||||
title.textContent = timestamp.valid ? "可信时间戳" : "时间戳详情";
|
||||
details.append(title);
|
||||
appendInfoLine(details, timestamp.valid ? "时间" : "声明时间", formatDocumentTime(timestamp.time));
|
||||
appendSignatureCheck(details, "绑定", timestamp.bindingChecked, timestamp.bindingOK);
|
||||
appendSignatureCheck(details, "签名", timestamp.signedValueChecked, timestamp.signedValueOK);
|
||||
appendSignatureCheck(details, "信任", timestamp.certTrustChecked, timestamp.certTrustOK);
|
||||
appendSignatureCheck(details, "时效", timestamp.certTimeChecked, timestamp.certTimeOK);
|
||||
appendInfoLine(details, "错误", timestamp.error, "fail");
|
||||
row.append(details);
|
||||
}
|
||||
for (const revocation of signature.revocations || []) {
|
||||
const details = document.createElement("details");
|
||||
const title = document.createElement("summary");
|
||||
title.textContent = "撤销详情";
|
||||
details.append(title);
|
||||
appendInfoLine(details, "证书", revocation.subject);
|
||||
appendInfoLine(details, "来源", revocation.source);
|
||||
const valid = revocation.checked && revocation.ok && revocation.status === "good";
|
||||
appendInfoLine(details, "状态", !revocation.checked ? "未验" : valid ? "未撤销" : revocation.status === "revoked" ? "已撤销" : "未知", valid ? "ok" : revocation.checked ? "fail" : "");
|
||||
appendInfoLine(details, "更新", formatDocumentTime(revocation.thisUpdate));
|
||||
appendInfoLine(details, "截止", formatDocumentTime(revocation.nextUpdate));
|
||||
appendInfoLine(details, "撤销时间", formatDocumentTime(revocation.revokedAt));
|
||||
appendInfoLine(details, "错误", revocation.error, "fail");
|
||||
row.append(details);
|
||||
}
|
||||
const details = document.createElement("details");
|
||||
const title = document.createElement("summary");
|
||||
title.textContent = "签章信息";
|
||||
details.append(title);
|
||||
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.signatureMethod);
|
||||
appendInfoLine(details, "摘要", signature.digestMethod);
|
||||
appendInfoLine(details, "序列号", signature.signSerial);
|
||||
appendInfoLine(details, "证书主体", signature.signSubject);
|
||||
appendInfoLine(details, "颁发者", signature.signIssuer);
|
||||
appendInfoLine(details, "制章证书", signature.sealSubject);
|
||||
row.append(details);
|
||||
fragment.append(row);
|
||||
}
|
||||
el.signatureList.append(fragment);
|
||||
@@ -5493,11 +5947,11 @@ function signatureSummary(signatures) {
|
||||
if (unchecked) {
|
||||
parts.push(`未验 ${unchecked}`);
|
||||
}
|
||||
return parts.length > 1 ? parts.join(" · ") : `通过 ${signatures.length}`;
|
||||
return parts.length > 1 ? parts.join(" · ") : `${signatures.every(signature => signature.trustedValid) ? "可信" : "完整"} ${signatures.length}`;
|
||||
}
|
||||
|
||||
function signatureNameNode(signature) {
|
||||
const stamps = signature.stamps || [];
|
||||
const stamps = signature.docIndex ? [] : signature.stamps || [];
|
||||
const name = document.createElement("div");
|
||||
name.className = "signature-name";
|
||||
if (!stamps.length) {
|
||||
@@ -6060,6 +6514,7 @@ function updateFitSpace() {
|
||||
}
|
||||
|
||||
function updateControls() {
|
||||
renderSecurity();
|
||||
const hasDoc = Boolean(state.doc);
|
||||
updateRenderBackend();
|
||||
updateEditorTools();
|
||||
@@ -6221,11 +6676,11 @@ async function callWASM(name, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = ++wasmRequestID;
|
||||
wasmRequests.set(id, { resolve, reject, openSeq: state.openSeq });
|
||||
if (name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment" || name === "ofdgoSaveDocument" || name === "ofdgoImportPages") {
|
||||
if (name === "ofdgoExportPage" || name === "ofdgoExportDocument" || name === "ofdgoExportAttachment" || name === "ofdgoSaveDocument" || name === "ofdgoSaveEncrypted" || name === "ofdgoSaveSigned" || name === "ofdgoImportPages") {
|
||||
state.exportRequestID = id;
|
||||
el.cancelExportButton.hidden = false;
|
||||
el.cancelExportButton.disabled = false;
|
||||
el.cancelExportButton.title = { ofdgoSaveDocument: "取消保存", ofdgoImportPages: "取消导入", ofdgoExportAttachment: "取消下载" }[name] || "取消导出";
|
||||
el.cancelExportButton.title = { ofdgoSaveDocument: "取消保存", ofdgoSaveEncrypted: "取消保存", ofdgoSaveSigned: "取消签署", ofdgoImportPages: "取消导入", ofdgoExportAttachment: "取消下载" }[name] || "取消导出";
|
||||
el.cancelExportButton.setAttribute("aria-label", el.cancelExportButton.title);
|
||||
}
|
||||
try {
|
||||
@@ -6253,6 +6708,7 @@ function showError(err, empty = !state.doc) {
|
||||
|
||||
function setBusy(busy, text = "", percent = 0, status = "") {
|
||||
document.body.toggleAttribute("aria-busy", busy);
|
||||
renderSecurity();
|
||||
updateRenderBackend();
|
||||
el.editButton.disabled = busy || !state.doc || !state.ready || state.exporting;
|
||||
el.createForm.inert = busy;
|
||||
|
||||
@@ -7,7 +7,7 @@ self.onmessage = ({ data }) => {
|
||||
operations.get(data.id)?.abort();
|
||||
return;
|
||||
}
|
||||
if (data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment" || data.name === "ofdgoSaveDocument" || data.name === "ofdgoImportPages") {
|
||||
if (data.name === "ofdgoExportPage" || data.name === "ofdgoExportDocument" || data.name === "ofdgoExportAttachment" || data.name === "ofdgoSaveDocument" || data.name === "ofdgoSaveEncrypted" || data.name === "ofdgoSaveSigned" || data.name === "ofdgoImportPages") {
|
||||
operations.set(data.id, new AbortController());
|
||||
}
|
||||
pending = pending.then(() => handleMessage(data));
|
||||
@@ -17,6 +17,10 @@ async function handleMessage({ id, name, args }) {
|
||||
let channel;
|
||||
const signal = operations.get(id)?.signal;
|
||||
const importing = name === "ofdgoImportPages";
|
||||
const saving = name === "ofdgoSaveDocument" || name === "ofdgoSaveEncrypted" || name === "ofdgoSaveSigned";
|
||||
const key = name === "ofdgoOpen" ? args[3]?.key : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.key : null;
|
||||
const keyPassword = name === "ofdgoOpen" ? args[3]?.keyPassword : name === "ofdgoSaveSigned" || name === "ofdgoLoadImport" ? args[1]?.keyPassword : null;
|
||||
const password = name === "ofdgoOpen" ? args[3]?.password : name === "ofdgoSaveEncrypted" || name === "ofdgoLoadImport" ? args[1]?.password : null;
|
||||
try {
|
||||
signal?.throwIfAborted();
|
||||
const chunks = [];
|
||||
@@ -37,7 +41,7 @@ async function handleMessage({ id, name, args }) {
|
||||
});
|
||||
} else {
|
||||
const file = args.pop();
|
||||
const indices = name === "ofdgoSaveDocument" ? args.shift() : null;
|
||||
const indices = saving ? args.shift() : null;
|
||||
if (file) output = await file.createWritable();
|
||||
signal.throwIfAborted();
|
||||
args.push((bytes, done) => {
|
||||
@@ -58,7 +62,7 @@ async function handleMessage({ id, name, args }) {
|
||||
self.postMessage({ id, type: "export", stage: "pages", completed, total });
|
||||
finish(done);
|
||||
});
|
||||
} else if (name === "ofdgoSaveDocument") {
|
||||
} else if (saving) {
|
||||
args.push((phase, completed, total, done) => {
|
||||
self.postMessage({ id, type: "export", stage: "prepare", phase, completed, total });
|
||||
finish(done);
|
||||
@@ -94,6 +98,9 @@ async function handleMessage({ id, name, args }) {
|
||||
}
|
||||
self.postMessage(result);
|
||||
} finally {
|
||||
password?.fill(0);
|
||||
key?.fill(0);
|
||||
keyPassword?.fill(0);
|
||||
channel?.port1.close();
|
||||
channel?.port2.close();
|
||||
operations.delete(id);
|
||||
|
||||
Reference in new issue
Block a user