feat: integrate XSLT Tools with toolbox portal
This commit is contained in:
174
src/App.tsx
174
src/App.tsx
@@ -1,5 +1,5 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import Layout from './components/Layout';
|
||||
import { AppShell } from '@add-ideas/toolbox-shell-react';
|
||||
import Toolbar from './components/Toolbar';
|
||||
import EditorPanel from './components/EditorPanel';
|
||||
import ActionDialog, {
|
||||
@@ -19,8 +19,14 @@ import {
|
||||
} from './validation/validationTypes';
|
||||
import { runTransformation } from './transform/transformService';
|
||||
import { createTransformationRun } from './transform/nativeXsltEngine';
|
||||
import { createSha256Hash } from './transform/hash';
|
||||
import {
|
||||
isStylesheetExecutionTrusted,
|
||||
type TrustedStylesheet,
|
||||
} from './transform/stylesheetTrust';
|
||||
import type { TransformEngineId } from './transform/transformTypes';
|
||||
import { createApproximateTrace } from './transform/traceAnalyzer';
|
||||
import { toolboxApp } from './toolboxApp';
|
||||
|
||||
interface DialogState {
|
||||
title: string;
|
||||
@@ -52,6 +58,19 @@ const App: React.FC = () => {
|
||||
DiagnosticMessage[]
|
||||
>([]);
|
||||
const [validationTouched, setValidationTouched] = useState(false);
|
||||
const [trustedStylesheet, setTrustedStylesheet] =
|
||||
useState<TrustedStylesheet | null>(null);
|
||||
const stylesheetRevisionRef = useRef(0);
|
||||
const stateRef = useRef(state);
|
||||
|
||||
useEffect(() => {
|
||||
stateRef.current = state;
|
||||
}, [state]);
|
||||
|
||||
const invalidateStylesheetTrust = () => {
|
||||
stylesheetRevisionRef.current += 1;
|
||||
setTrustedStylesheet(null);
|
||||
};
|
||||
|
||||
const xmlDiagnostics = useMemo(
|
||||
() => validateXml(state.xmlInput.text, 'XML input'),
|
||||
@@ -161,6 +180,7 @@ const App: React.FC = () => {
|
||||
}
|
||||
|
||||
replaceDocument(kind, opened.text, opened.name, false);
|
||||
if (kind === 'xsltCode') invalidateStylesheetTrust();
|
||||
setStatusMessage(`Loaded ${opened.name}.`);
|
||||
} catch (error) {
|
||||
if (isAbortError(error)) return;
|
||||
@@ -203,6 +223,16 @@ const App: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!trustedStylesheet ||
|
||||
trustedStylesheet.revision !== stylesheetRevisionRef.current
|
||||
) {
|
||||
setStatusMessage(
|
||||
'Review and trust this exact stylesheet before transforming.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (state.options.askBeforeOverwritingOutput) {
|
||||
const mayOverwriteOutput = await confirmOverwrite(
|
||||
'xmlOutput',
|
||||
@@ -213,13 +243,33 @@ const App: React.FC = () => {
|
||||
|
||||
setBusy(true);
|
||||
try {
|
||||
const currentRevision = stylesheetRevisionRef.current;
|
||||
const currentState = stateRef.current;
|
||||
const currentHash = await createSha256Hash(currentState.xsltCode.text);
|
||||
if (
|
||||
currentRevision !== stylesheetRevisionRef.current ||
|
||||
!isStylesheetExecutionTrusted(
|
||||
trustedStylesheet,
|
||||
currentHash,
|
||||
currentRevision
|
||||
)
|
||||
) {
|
||||
setTrustedStylesheet(null);
|
||||
setStatusMessage(
|
||||
'The stylesheet changed. Review and trust it again before transforming.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
const request = {
|
||||
xmlText: state.xmlInput.text,
|
||||
xsltText: state.xsltCode.text,
|
||||
engine: state.selectedEngine,
|
||||
xmlText: currentState.xmlInput.text,
|
||||
xsltText: currentState.xsltCode.text,
|
||||
engine: currentState.selectedEngine,
|
||||
};
|
||||
|
||||
const result = await runTransformation(request);
|
||||
const result = await runTransformation(request, {
|
||||
trustedStylesheet,
|
||||
currentRevision,
|
||||
});
|
||||
setRuntimeDiagnostics(result.diagnostics);
|
||||
|
||||
if (hasErrors(result.diagnostics)) {
|
||||
@@ -229,11 +279,16 @@ const App: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const output = state.options.prettifyOutputAfterTransform
|
||||
const output = currentState.options.prettifyOutputAfterTransform
|
||||
? tryFormatXml(result.output)
|
||||
: result.output;
|
||||
|
||||
replaceDocument('xmlOutput', output, state.xmlOutput.fileName, true);
|
||||
replaceDocument(
|
||||
'xmlOutput',
|
||||
output,
|
||||
currentState.xmlOutput.fileName,
|
||||
true
|
||||
);
|
||||
const run = await createTransformationRun(request, output);
|
||||
|
||||
setLastTransformation(run);
|
||||
@@ -253,6 +308,66 @@ const App: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleTrustStylesheet = async () => {
|
||||
const revision = stylesheetRevisionRef.current;
|
||||
const stylesheetText = stateRef.current.xsltCode.text;
|
||||
try {
|
||||
const hash = await createSha256Hash(stylesheetText);
|
||||
const confirmed = await askForConfirmation({
|
||||
title: 'Trust this executable stylesheet?',
|
||||
content: (
|
||||
<>
|
||||
<p>
|
||||
XSLT is executable code. Both engines may access browser or
|
||||
network resources. Only continue if you trust the source of this
|
||||
exact stylesheet.
|
||||
</p>
|
||||
<p>
|
||||
A trusted SaxonJS stylesheet may access data stored by other
|
||||
Toolbox apps on this same site, including PDF workspaces and
|
||||
portal preferences. It may send that data over the network or
|
||||
navigate this page away.
|
||||
</p>
|
||||
<p>
|
||||
Size limits and a best-effort asynchronous deadline reduce
|
||||
accidental overload, but the engines do not provide complete
|
||||
process or network isolation. Synchronous work cannot be forcibly
|
||||
stopped. Any edit, opened replacement, formatting change, or reset
|
||||
revokes trust.
|
||||
</p>
|
||||
</>
|
||||
),
|
||||
proceedLabel: 'Trust this exact stylesheet',
|
||||
danger: true,
|
||||
});
|
||||
if (!confirmed) {
|
||||
setStatusMessage('Stylesheet remains untrusted.');
|
||||
return;
|
||||
}
|
||||
if (
|
||||
revision !== stylesheetRevisionRef.current ||
|
||||
stylesheetText !== stateRef.current.xsltCode.text
|
||||
) {
|
||||
setTrustedStylesheet(null);
|
||||
setStatusMessage(
|
||||
'The stylesheet changed during review. Review it again.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
setTrustedStylesheet({ hash, revision });
|
||||
setStatusMessage(
|
||||
'This exact stylesheet is trusted for the current session.'
|
||||
);
|
||||
} catch (error) {
|
||||
setTrustedStylesheet(null);
|
||||
setStatusMessage(
|
||||
error instanceof Error
|
||||
? error.message
|
||||
: 'Could not fingerprint the stylesheet.'
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMoveOutputToInput = async () => {
|
||||
if (!state.xmlOutput.text.trim()) {
|
||||
setStatusMessage('The output editor is empty. Nothing to move.');
|
||||
@@ -292,6 +407,7 @@ const App: React.FC = () => {
|
||||
|
||||
if (!mayProceed) return;
|
||||
replaceDocument(kind, formatted, documentState.fileName, true);
|
||||
if (kind === 'xsltCode') invalidateStylesheetTrust();
|
||||
setStatusMessage(`Formatted ${documentState.label}.`);
|
||||
} catch (error) {
|
||||
setStatusMessage(
|
||||
@@ -336,6 +452,7 @@ const App: React.FC = () => {
|
||||
|
||||
if (!mayProceed) return;
|
||||
resetWorkspace();
|
||||
invalidateStylesheetTrust();
|
||||
setRuntimeDiagnostics([]);
|
||||
setValidationTouched(false);
|
||||
setStatusMessage('Workspace reset.');
|
||||
@@ -402,7 +519,8 @@ const App: React.FC = () => {
|
||||
event.stopPropagation();
|
||||
|
||||
if (!busy) {
|
||||
void handleApplyTransformation();
|
||||
if (trustedStylesheet) void handleApplyTransformation();
|
||||
else void handleTrustStylesheet();
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -412,10 +530,25 @@ const App: React.FC = () => {
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleGlobalKeyDown, true);
|
||||
};
|
||||
}, [busy, handleApplyTransformation, handleOpenHelp]);
|
||||
});
|
||||
|
||||
return (
|
||||
<Layout onOpenHelp={() => handleOpenHelp()}>
|
||||
<AppShell
|
||||
app={toolboxApp}
|
||||
className="xslt-app-shell"
|
||||
appActions={
|
||||
<button
|
||||
type="button"
|
||||
className="app-help-button"
|
||||
onClick={handleOpenHelp}
|
||||
aria-haspopup="dialog"
|
||||
title="Open help and keyboard shortcuts (F1)"
|
||||
aria-label="Open help"
|
||||
>
|
||||
<i className="bi bi-question-circle" aria-hidden="true" /> Help
|
||||
</button>
|
||||
}
|
||||
>
|
||||
<div className="workspace-grid">
|
||||
<EditorPanel
|
||||
ref={xmlInputEditorRef}
|
||||
@@ -443,7 +576,10 @@ const App: React.FC = () => {
|
||||
fileName={state.xsltCode.fileName}
|
||||
dirty={state.xsltCode.dirty}
|
||||
diagnostics={xsltDiagnostics}
|
||||
onChange={(value) => updateDocumentText('xsltCode', value)}
|
||||
onChange={(value) => {
|
||||
invalidateStylesheetTrust();
|
||||
updateDocumentText('xsltCode', value);
|
||||
}}
|
||||
onOpen={() => void handleOpenFile('xsltCode')}
|
||||
onSave={() => void handleSaveFile('xsltCode')}
|
||||
onUndo={() => xsltEditorRef.current?.undo()}
|
||||
@@ -487,10 +623,13 @@ const App: React.FC = () => {
|
||||
selectedEngine={state.selectedEngine}
|
||||
onEngineChange={handleEngineChange}
|
||||
onApplyTransformation={() => void handleApplyTransformation()}
|
||||
onTrustStylesheet={() => void handleTrustStylesheet()}
|
||||
onMoveOutputToInput={() => void handleMoveOutputToInput()}
|
||||
onValidate={handleValidate}
|
||||
onReset={() => void handleReset()}
|
||||
prettifyOutputAfterTransform={state.options.prettifyOutputAfterTransform}
|
||||
prettifyOutputAfterTransform={
|
||||
state.options.prettifyOutputAfterTransform
|
||||
}
|
||||
onPrettifyOutputAfterTransformChange={
|
||||
handlePrettifyOutputAfterTransformChange
|
||||
}
|
||||
@@ -499,6 +638,7 @@ const App: React.FC = () => {
|
||||
handleAskBeforeOverwritingOutputChange
|
||||
}
|
||||
busy={busy}
|
||||
stylesheetTrusted={trustedStylesheet !== null}
|
||||
/>
|
||||
|
||||
{statusMessage && <div className="status-banner">{statusMessage}</div>}
|
||||
@@ -520,7 +660,6 @@ const App: React.FC = () => {
|
||||
</section>
|
||||
)}
|
||||
|
||||
|
||||
<section className="trace-panel card">
|
||||
<div className="trace-header">
|
||||
<div>
|
||||
@@ -586,13 +725,8 @@ const App: React.FC = () => {
|
||||
{dialog?.content}
|
||||
</ActionDialog>
|
||||
|
||||
{helpOpen && (
|
||||
<HelpDialog
|
||||
open={helpOpen}
|
||||
onClose={handleCloseHelp}
|
||||
/>
|
||||
)}
|
||||
</Layout>
|
||||
{helpOpen && <HelpDialog open={helpOpen} onClose={handleCloseHelp} />}
|
||||
</AppShell>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user