@patrickdk77/office-defuse 0.1.1 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +211 -43
- package/dist/cfb-writer.d.ts +25 -0
- package/dist/cfb-writer.js +314 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +357 -0
- package/dist/contained.d.ts +46 -0
- package/dist/contained.js +152 -0
- package/dist/context.d.ts +107 -12
- package/dist/context.js +101 -24
- package/dist/detect.d.ts +13 -0
- package/dist/detect.js +83 -0
- package/dist/engine.d.ts +66 -1
- package/dist/engine.js +290 -35
- package/dist/errors.d.ts +17 -0
- package/dist/errors.js +27 -1
- package/dist/fields.d.ts +20 -13
- package/dist/fields.js +65 -23
- package/dist/findings.d.ts +2 -1
- package/dist/findings.js +17 -1
- package/dist/flat.d.ts +33 -0
- package/dist/flat.js +555 -0
- package/dist/formats.d.ts +9 -0
- package/dist/formats.js +7 -1
- package/dist/formulas.d.ts +27 -0
- package/dist/formulas.js +157 -1
- package/dist/index.d.ts +4 -2
- package/dist/index.js +11 -1
- package/dist/index.mjs +1 -1
- package/dist/io.d.ts +62 -1
- package/dist/io.js +245 -1
- package/dist/links.d.ts +27 -0
- package/dist/links.js +106 -1
- package/dist/msg.d.ts +3 -2
- package/dist/msg.js +172 -18
- package/dist/objects.d.ts +8 -5
- package/dist/objects.js +90 -20
- package/dist/odf.d.ts +87 -1
- package/dist/odf.js +204 -76
- package/dist/offcrypto.d.ts +24 -1
- package/dist/offcrypto.js +308 -69
- package/dist/ole.d.ts +19 -2
- package/dist/ole.js +98 -58
- package/dist/oleobj.d.ts +11 -0
- package/dist/oleobj.js +20 -3
- package/dist/ooxml.d.ts +108 -2
- package/dist/ooxml.js +392 -109
- package/dist/package.d.ts +14 -0
- package/dist/package.js +23 -7
- package/dist/plan.d.ts +104 -0
- package/dist/plan.js +73 -0
- package/dist/plugins.d.ts +8 -0
- package/dist/plugins.js +35 -0
- package/dist/ppt.js +45 -5
- package/dist/rewrite-cfb.d.ts +14 -0
- package/dist/rewrite-cfb.js +52 -0
- package/dist/rewrite-rtf.d.ts +9 -0
- package/dist/rewrite-rtf.js +45 -0
- package/dist/rewrite-xml.d.ts +36 -0
- package/dist/rewrite-xml.js +341 -0
- package/dist/rewrite-zip.d.ts +8 -0
- package/dist/rewrite-zip.js +498 -0
- package/dist/rtf.js +216 -42
- package/dist/rtfcomp.d.ts +7 -0
- package/dist/rtfcomp.js +80 -0
- package/dist/score.js +1 -1
- package/dist/sniff.d.ts +5 -0
- package/dist/sniff.js +38 -0
- package/dist/types.d.ts +66 -6
- package/dist/types.js +14 -0
- package/dist/vsd.d.ts +13 -0
- package/dist/vsd.js +689 -0
- package/dist/word.d.ts +4 -4
- package/dist/word.js +70 -23
- package/dist/xls.d.ts +10 -4
- package/dist/xls.js +58 -10
- package/dist/xml.d.ts +30 -0
- package/dist/xml.js +45 -23
- package/dist/zip-writer.d.ts +4 -0
- package/dist/zip-writer.js +20 -7
- package/dist/zip.d.ts +11 -2
- package/dist/zip.js +67 -6
- package/package.json +4 -1
package/dist/odf.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EntryRules = exports.packagePath = exports.hrefOf = exports.odfNamespace = exports.NS = void 0;
|
|
4
|
+
exports.oattr = oattr;
|
|
5
|
+
exports.folderOf = folderOf;
|
|
6
|
+
exports.nameOdf = nameOdf;
|
|
3
7
|
exports.inspectOdf = inspectOdf;
|
|
8
|
+
const contained_1 = require("./contained");
|
|
4
9
|
const context_1 = require("./context");
|
|
5
10
|
const errors_1 = require("./errors");
|
|
6
11
|
const formats_1 = require("./formats");
|
|
7
12
|
const formulas_1 = require("./formulas");
|
|
8
13
|
const links_1 = require("./links");
|
|
14
|
+
const plan_1 = require("./plan");
|
|
9
15
|
const types_1 = require("./types");
|
|
10
16
|
const zip_1 = require("./zip");
|
|
11
|
-
|
|
17
|
+
exports.NS = {
|
|
12
18
|
manifest: 'urn:oasis:names:tc:opendocument:xmlns:manifest:1.0',
|
|
13
19
|
office: 'urn:oasis:names:tc:opendocument:xmlns:office:1.0',
|
|
14
20
|
script: 'urn:oasis:names:tc:opendocument:xmlns:script:1.0',
|
|
@@ -20,6 +26,7 @@ const NS = {
|
|
|
20
26
|
meta: 'urn:oasis:names:tc:opendocument:xmlns:meta:1.0',
|
|
21
27
|
style: 'urn:oasis:names:tc:opendocument:xmlns:style:1.0',
|
|
22
28
|
database: 'urn:oasis:names:tc:opendocument:xmlns:database:1.0',
|
|
29
|
+
chart: 'urn:oasis:names:tc:opendocument:xmlns:chart:1.0',
|
|
23
30
|
xlink: 'http://www.w3.org/1999/xlink',
|
|
24
31
|
};
|
|
25
32
|
/**
|
|
@@ -35,21 +42,21 @@ const FOLDERS = [
|
|
|
35
42
|
/** Event names that fire as the document opens. Focus and activation count only for the document's own events. */
|
|
36
43
|
const ON_OPEN = new Set(['load', 'new', 'create', 'loadfinished', 'viewcreated']);
|
|
37
44
|
const ON_OPEN_DOCUMENT = new Set(['focus', 'activate']);
|
|
38
|
-
const LINK_ELEMENTS = new Set([`${NS.text} a`, `${NS.draw} a`, `${NS.draw} area-rectangle`, `${NS.draw} area-circle`, `${NS.draw} area-polygon`]);
|
|
45
|
+
const LINK_ELEMENTS = new Set([`${exports.NS.text} a`, `${exports.NS.draw} a`, `${exports.NS.draw} area-rectangle`, `${exports.NS.draw} area-circle`, `${exports.NS.draw} area-polygon`]);
|
|
39
46
|
const EXTERNAL_KIND = new Map([
|
|
40
|
-
[`${NS.meta} template`, types_1.OfficeDetail.Template],
|
|
41
|
-
[`${NS.meta} auto-reload`, types_1.OfficeDetail.Reload],
|
|
42
|
-
[`${NS.draw} image`, types_1.OfficeDetail.Image],
|
|
43
|
-
[`${NS.draw} fill-image`, types_1.OfficeDetail.Image],
|
|
44
|
-
[`${NS.style} background-image`, types_1.OfficeDetail.Image],
|
|
45
|
-
[`${NS.text} section-source`, types_1.OfficeDetail.Include],
|
|
46
|
-
[`${NS.table} table-source`, types_1.OfficeDetail.Include],
|
|
47
|
-
[`${NS.draw} object`, types_1.OfficeDetail.Object],
|
|
48
|
-
[`${NS.draw} object-ole`, types_1.OfficeDetail.Object],
|
|
49
|
-
[`${NS.draw} floating-frame`, types_1.OfficeDetail.Frame],
|
|
47
|
+
[`${exports.NS.meta} template`, types_1.OfficeDetail.Template],
|
|
48
|
+
[`${exports.NS.meta} auto-reload`, types_1.OfficeDetail.Reload],
|
|
49
|
+
[`${exports.NS.draw} image`, types_1.OfficeDetail.Image],
|
|
50
|
+
[`${exports.NS.draw} fill-image`, types_1.OfficeDetail.Image],
|
|
51
|
+
[`${exports.NS.style} background-image`, types_1.OfficeDetail.Image],
|
|
52
|
+
[`${exports.NS.text} section-source`, types_1.OfficeDetail.Include],
|
|
53
|
+
[`${exports.NS.table} table-source`, types_1.OfficeDetail.Include],
|
|
54
|
+
[`${exports.NS.draw} object`, types_1.OfficeDetail.Object],
|
|
55
|
+
[`${exports.NS.draw} object-ole`, types_1.OfficeDetail.Object],
|
|
56
|
+
[`${exports.NS.draw} floating-frame`, types_1.OfficeDetail.Frame],
|
|
50
57
|
]);
|
|
51
58
|
/** Elements whose reference always leaves the package: it resolves against the document's own location. */
|
|
52
|
-
const ALWAYS_OUTSIDE = new Set([`${NS.meta} template`, `${NS.meta} auto-reload`, `${NS.text} section-source`, `${NS.table} table-source`]);
|
|
59
|
+
const ALWAYS_OUTSIDE = new Set([`${exports.NS.meta} template`, `${exports.NS.meta} auto-reload`, `${exports.NS.text} section-source`, `${exports.NS.table} table-source`]);
|
|
53
60
|
/**
|
|
54
61
|
* LibreOffice reads urn:oasis:names:tc:<any>:xmlns:<name>:1.<any> as the OpenDocument namespace <name> 1.0, so this
|
|
55
62
|
* does too. The comparison ignores case, which can only find more.
|
|
@@ -65,90 +72,155 @@ function oattr(el, ns, local) {
|
|
|
65
72
|
return a.value;
|
|
66
73
|
return undefined;
|
|
67
74
|
}
|
|
75
|
+
/** The ODF_NS-normalized namespace of `ns`, exported for the rewrite. */
|
|
76
|
+
const odfNamespace = (ns) => odfNs(ns);
|
|
77
|
+
exports.odfNamespace = odfNamespace;
|
|
78
|
+
/** The edit that takes an element's xlink:href away, under whatever namespace URI the attribute has. */
|
|
79
|
+
function stripHref(el) {
|
|
80
|
+
return { strip: new Set(el.attributes.filter(a => a.local === 'href' && odfNs(a.ns) === exports.NS.xlink).map(a => (0, plan_1.attributeKey)(a.ns, a.local))) };
|
|
81
|
+
}
|
|
82
|
+
/** The xlink:href of an element, for the rewrite. */
|
|
83
|
+
const hrefOf = (el) => oattr(el, exports.NS.xlink, 'href');
|
|
84
|
+
exports.hrefOf = hrefOf;
|
|
68
85
|
/** A path inside the package as the manifest and the zip name it: without "./" and leading slashes. */
|
|
69
86
|
const packagePath = (href) => (0, zip_1.nameKey)(href.trim().replace(/^(\.\/)+/, ''));
|
|
70
|
-
|
|
87
|
+
exports.packagePath = packagePath;
|
|
88
|
+
/**
|
|
89
|
+
* The rules that read inside one XML entry of an ODF package, or inside a flat OpenDocument file. A flat file has no
|
|
90
|
+
* entries, so in one every relative reference names a file beside the document, which loads from outside it.
|
|
91
|
+
*/
|
|
71
92
|
class EntryRules {
|
|
72
93
|
ctx;
|
|
73
94
|
entry;
|
|
74
95
|
oleRefs;
|
|
96
|
+
flat;
|
|
75
97
|
/** Depth of the office:scripts element being read, 0 outside one. */
|
|
76
98
|
scriptsDepth = 0;
|
|
77
99
|
/** An office:script element being read, with whether it holds anything. */
|
|
78
100
|
script;
|
|
101
|
+
/** A link element being read, whose text and title go into the label check at its end. */
|
|
102
|
+
label;
|
|
79
103
|
constructor(ctx, entry,
|
|
80
104
|
/** Whether an object refers to each entry, by key. Only entries are kept, so a part full of references costs no memory. */
|
|
81
|
-
oleRefs) {
|
|
105
|
+
oleRefs, flat = false) {
|
|
82
106
|
this.ctx = ctx;
|
|
83
107
|
this.entry = entry;
|
|
84
108
|
this.oleRefs = oleRefs;
|
|
109
|
+
this.flat = flat;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Whether a reference names an entry of the package. LibreOffice's vnd.sun.star.Package: URLs name one too. In a
|
|
113
|
+
* flat file no entry exists, so only such a URL stays inside, naming nothing.
|
|
114
|
+
*/
|
|
115
|
+
inside(href) {
|
|
116
|
+
const own = /^\s*vnd\.sun\.star\.package:(.*)$/is.exec(href);
|
|
117
|
+
if (own)
|
|
118
|
+
return this.flat || (0, links_1.isInsidePackage)(own[1]);
|
|
119
|
+
return !this.flat && (0, links_1.isInsidePackage)(href);
|
|
85
120
|
}
|
|
86
121
|
report(category, detail, data) {
|
|
87
|
-
this.ctx.report(category, detail, this.entry, data);
|
|
122
|
+
return this.ctx.report(category, detail, this.entry, data);
|
|
123
|
+
}
|
|
124
|
+
/** Records the edit for the element when the finding strips. */
|
|
125
|
+
edit(r, el, edit = { remove: true }) {
|
|
126
|
+
if (r.action === 'strip')
|
|
127
|
+
this.ctx.plan.element(this.entry, el.index, edit);
|
|
88
128
|
}
|
|
89
129
|
start(raw) {
|
|
90
130
|
if (this.script)
|
|
91
131
|
this.script.content = true;
|
|
92
132
|
const el = { ...raw, ns: odfNs(raw.ns) };
|
|
93
133
|
const key = `${el.ns} ${el.local}`;
|
|
94
|
-
const href = oattr(el, NS.xlink, 'href');
|
|
95
|
-
if (el.ns === NS.office && el.local === 'scripts' && !this.scriptsDepth)
|
|
134
|
+
const href = oattr(el, exports.NS.xlink, 'href');
|
|
135
|
+
if (el.ns === exports.NS.office && el.local === 'scripts' && !this.scriptsDepth)
|
|
96
136
|
this.scriptsDepth = el.depth;
|
|
97
|
-
else if (el.ns === NS.office && el.local === 'script')
|
|
98
|
-
this.script = { depth: el.depth, language: oattr(el, NS.script, 'language') ?? '', content: false };
|
|
99
|
-
else if (el.ns === NS.script && el.local === 'event-listener')
|
|
137
|
+
else if (el.ns === exports.NS.office && el.local === 'script')
|
|
138
|
+
this.script = { depth: el.depth, language: oattr(el, exports.NS.script, 'language') ?? '', content: false, index: el.index };
|
|
139
|
+
else if (el.ns === exports.NS.script && el.local === 'event-listener')
|
|
100
140
|
this.scriptEvent(el, href);
|
|
101
|
-
else if (el.ns === NS.presentation && el.local === 'event-listener')
|
|
141
|
+
else if (el.ns === exports.NS.presentation && el.local === 'event-listener')
|
|
102
142
|
this.presentationEvent(el, href);
|
|
103
|
-
else if ((el.ns === NS.text && el.local === 'dde-connection-decl') || (el.ns === NS.office && el.local === 'dde-source')) {
|
|
104
|
-
this.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, { application: (0, context_1.clip)(oattr(el, NS.office, 'dde-application') ?? ''), topic: (0, context_1.clip)(oattr(el, NS.office, 'dde-topic') ?? '') });
|
|
143
|
+
else if ((el.ns === exports.NS.text && el.local === 'dde-connection-decl') || (el.ns === exports.NS.office && el.local === 'dde-source')) {
|
|
144
|
+
this.edit(this.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, { application: (0, context_1.clip)(oattr(el, exports.NS.office, 'dde-application') ?? ''), topic: (0, context_1.clip)(oattr(el, exports.NS.office, 'dde-topic') ?? '') }), raw);
|
|
105
145
|
}
|
|
106
|
-
else if (el.ns === NS.draw && el.local === 'applet') {
|
|
107
|
-
this.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Applet, { code: (0, context_1.clip)(oattr(el, NS.draw, 'code') ?? ''), ...(href ? { target: (0, context_1.clip)(href) } : {}) });
|
|
146
|
+
else if (el.ns === exports.NS.draw && el.local === 'applet') {
|
|
147
|
+
this.edit(this.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Applet, { code: (0, context_1.clip)(oattr(el, exports.NS.draw, 'code') ?? ''), ...(href ? { target: (0, context_1.clip)(href) } : {}) }), raw);
|
|
108
148
|
return;
|
|
109
149
|
}
|
|
110
|
-
else if (el.ns === NS.draw && el.local === 'plugin') {
|
|
111
|
-
this.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Plugin, { mimeType: (0, context_1.clip)(oattr(el, NS.draw, 'mime-type') ?? ''), ...(href ? { target: (0, context_1.clip)(href) } : {}) });
|
|
150
|
+
else if (el.ns === exports.NS.draw && el.local === 'plugin') {
|
|
151
|
+
this.edit(this.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Plugin, { mimeType: (0, context_1.clip)(oattr(el, exports.NS.draw, 'mime-type') ?? ''), ...(href ? { target: (0, context_1.clip)(href) } : {}) }), raw);
|
|
112
152
|
return;
|
|
113
153
|
}
|
|
114
|
-
else if (el.ns === NS.draw && el.local === 'object-ole' && (href === undefined ||
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
154
|
+
else if (el.ns === exports.NS.draw && el.local === 'object-ole' && (href === undefined || this.inside(href))) {
|
|
155
|
+
// The object's entry may still be ahead in the archive, so its plugin verdict decides this element later. The
|
|
156
|
+
// preview image beside the element stays either way.
|
|
157
|
+
const r = this.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, href ? { target: (0, context_1.clip)(href) } : undefined);
|
|
158
|
+
this.ctx.plan.objects.push({ part: this.entry, index: el.index, target: href ? (0, exports.packagePath)(href) : '', finding: r.finding, strips: r.action === 'strip' });
|
|
159
|
+
if (href && this.oleRefs.has((0, exports.packagePath)(href)))
|
|
160
|
+
this.oleRefs.set((0, exports.packagePath)(href), true);
|
|
118
161
|
}
|
|
119
162
|
for (const a of el.attributes) {
|
|
120
|
-
|
|
121
|
-
|
|
163
|
+
// The cell keeps its value without the formula.
|
|
164
|
+
if (odfNs(a.ns) !== exports.NS.table || (a.local !== 'formula' && a.local !== 'expression'))
|
|
165
|
+
continue;
|
|
166
|
+
const strip = { strip: new Set([(0, plan_1.attributeKey)(a.ns, a.local)]) };
|
|
167
|
+
if ((0, formulas_1.callsDde)(a.value))
|
|
168
|
+
this.edit(this.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, { formula: (0, context_1.clip)(a.value) }), raw, strip);
|
|
169
|
+
for (const [fn, kind] of (0, formulas_1.formulaCalls)(a.value))
|
|
170
|
+
this.edit(this.report(kind === 'fetch' ? types_1.OfficeCategory.External : types_1.OfficeCategory.Macro, types_1.OfficeDetail.Formula, { function: (0, context_1.clip)(fn), formula: (0, context_1.clip)(a.value) }), raw, strip);
|
|
122
171
|
}
|
|
123
|
-
if (href === undefined || (0, links_1.isFragment)(href) || (el.ns === NS.script && el.local === 'event-listener') || (el.ns === NS.presentation && el.local === 'event-listener'))
|
|
172
|
+
if (href === undefined || (0, links_1.isFragment)(href) || (el.ns === exports.NS.script && el.local === 'event-listener') || (el.ns === exports.NS.presentation && el.local === 'event-listener'))
|
|
124
173
|
return;
|
|
125
174
|
// A database document names its own embedded database this way.
|
|
126
|
-
if (el.ns === NS.database && el.local === 'connection-resource' && /^sdbc:embedded:/i.test(href.trim()))
|
|
175
|
+
if (el.ns === exports.NS.database && el.local === 'connection-resource' && /^sdbc:embedded:/i.test(href.trim()))
|
|
176
|
+
return;
|
|
177
|
+
// A chart names the document that holds it, its data source, as "..".
|
|
178
|
+
if (el.ns === exports.NS.chart && el.local === 'chart' && /^\.\.\/?$/.test(href.trim()))
|
|
127
179
|
return;
|
|
128
|
-
if (LINK_ELEMENTS.has(key)
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
this.
|
|
180
|
+
if (LINK_ELEMENTS.has(key)) {
|
|
181
|
+
// The text of a link stays without it. An image map area is nothing but its link.
|
|
182
|
+
const r = this.ctx.link(href, this.entry, { element: (0, context_1.clip)(el.qname) });
|
|
183
|
+
this.edit(r, raw, el.local === 'a' ? { unwrap: true } : { remove: true });
|
|
184
|
+
if (r.action !== 'strip' && !this.label)
|
|
185
|
+
this.label = { index: el.index, href, text: '', title: oattr(el, exports.NS.office, 'title') ?? oattr(el, exports.NS.xlink, 'title'), unwrap: el.local === 'a' };
|
|
186
|
+
}
|
|
187
|
+
else if (el.ns === exports.NS.form && el.local === 'form') {
|
|
188
|
+
// A form's own href is where it sends its fields, which goes as pdf-defuse's form submit actions go.
|
|
189
|
+
this.edit(this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.SubmitForm, { element: (0, context_1.clip)(el.qname), target: (0, context_1.clip)(href, 2000) }), raw, stripHref(raw));
|
|
190
|
+
}
|
|
191
|
+
else if (el.ns === exports.NS.form)
|
|
192
|
+
this.edit(this.ctx.link(href, this.entry, { element: (0, context_1.clip)(el.qname) }), raw, stripHref(raw));
|
|
193
|
+
else if (ALWAYS_OUTSIDE.has(key) || !this.inside(href)) {
|
|
194
|
+
this.edit(this.ctx.external(EXTERNAL_KIND.get(key) ?? types_1.OfficeDetail.Other, href, this.entry, { element: (0, context_1.clip)(el.qname) }, (0, context_1.clip)(el.local)), raw);
|
|
132
195
|
}
|
|
133
196
|
}
|
|
134
197
|
text(t) {
|
|
135
198
|
if (this.script && t.trim())
|
|
136
199
|
this.script.content = true;
|
|
200
|
+
if (this.label && this.label.text.length < 4096)
|
|
201
|
+
this.label.text += t;
|
|
137
202
|
}
|
|
138
203
|
end(el) {
|
|
139
|
-
|
|
204
|
+
const label = this.label;
|
|
205
|
+
if (label && label.index === el.index) {
|
|
206
|
+
this.label = undefined;
|
|
207
|
+
const r = this.ctx.labelMismatch(label.href, [label.text, label.title], this.entry, { element: (0, context_1.clip)(el.qname) });
|
|
208
|
+
if (r)
|
|
209
|
+
this.edit(r, el, label.unwrap ? { unwrap: true } : { remove: true });
|
|
210
|
+
}
|
|
211
|
+
if (this.scriptsDepth === el.depth && odfNs(el.ns) === exports.NS.office && el.local === 'scripts')
|
|
140
212
|
this.scriptsDepth = 0;
|
|
141
213
|
if (this.script && this.script.depth === el.depth) {
|
|
142
214
|
if (this.script.content)
|
|
143
|
-
this.report(types_1.OfficeCategory.Macro, /basic/i.test(this.script.language) ? types_1.OfficeDetail.OdfBasic : types_1.OfficeDetail.OdfScripts, { language: (0, context_1.clip)(this.script.language) });
|
|
215
|
+
this.edit(this.report(types_1.OfficeCategory.Macro, /basic/i.test(this.script.language) ? types_1.OfficeDetail.OdfBasic : types_1.OfficeDetail.OdfScripts, { language: (0, context_1.clip)(this.script.language) }), el, { remove: true });
|
|
144
216
|
this.script = undefined;
|
|
145
217
|
}
|
|
146
218
|
}
|
|
147
219
|
scriptEvent(el, href) {
|
|
148
|
-
const event = oattr(el, NS.script, 'event-name') ?? '';
|
|
220
|
+
const event = oattr(el, exports.NS.script, 'event-name') ?? '';
|
|
149
221
|
// LibreOffice runs nothing for a listener whose script URL and macro name are both empty. An empty href does not
|
|
150
222
|
// hide a macro name.
|
|
151
|
-
const target = [href, oattr(el, NS.script, 'macro-name')].find(v => v !== undefined && v !== '');
|
|
223
|
+
const target = [href, oattr(el, exports.NS.script, 'macro-name')].find(v => v !== undefined && v !== '');
|
|
152
224
|
if (target === undefined)
|
|
153
225
|
return;
|
|
154
226
|
const name = event
|
|
@@ -157,45 +229,43 @@ class EntryRules {
|
|
|
157
229
|
.replace(/^on/, '')
|
|
158
230
|
.replace(/-/g, '');
|
|
159
231
|
const onOpen = ON_OPEN.has(name) || (this.scriptsDepth > 0 && ON_OPEN_DOCUMENT.has(name));
|
|
160
|
-
const data = { event: (0, context_1.clip)(event), target: (0, context_1.clip)(target), language: (0, context_1.clip)(oattr(el, NS.script, 'language') ?? '') };
|
|
161
|
-
|
|
162
|
-
this.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.EventOnOpen, data);
|
|
163
|
-
else
|
|
164
|
-
this.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Event, data);
|
|
232
|
+
const data = { event: (0, context_1.clip)(event), target: (0, context_1.clip)(target), language: (0, context_1.clip)(oattr(el, exports.NS.script, 'language') ?? '') };
|
|
233
|
+
this.edit(this.report(types_1.OfficeCategory.Macro, onOpen ? types_1.OfficeDetail.EventOnOpen : types_1.OfficeDetail.Event, data), el);
|
|
165
234
|
}
|
|
166
235
|
presentationEvent(el, href) {
|
|
167
|
-
const action = oattr(el, NS.presentation, 'action') ?? '';
|
|
168
|
-
const data = { event: (0, context_1.clip)(oattr(el, NS.script, 'event-name') ?? ''), action: (0, context_1.clip)(action), ...(href ? { target: (0, context_1.clip)(href) } : {}) };
|
|
236
|
+
const action = oattr(el, exports.NS.presentation, 'action') ?? '';
|
|
237
|
+
const data = { event: (0, context_1.clip)(oattr(el, exports.NS.script, 'event-name') ?? ''), action: (0, context_1.clip)(action), ...(href ? { target: (0, context_1.clip)(href) } : {}) };
|
|
169
238
|
if (action === 'execute')
|
|
170
|
-
this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, data);
|
|
239
|
+
this.edit(this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, data), el);
|
|
171
240
|
else if (action === 'verb')
|
|
172
|
-
this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, data);
|
|
241
|
+
this.edit(this.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, data), el);
|
|
173
242
|
else if (href !== undefined && !(0, links_1.isFragment)(href))
|
|
174
|
-
this.ctx.link(href, this.entry, data);
|
|
243
|
+
this.edit(this.ctx.link(href, this.entry, data), el);
|
|
175
244
|
}
|
|
176
245
|
}
|
|
246
|
+
exports.EntryRules = EntryRules;
|
|
177
247
|
/** The manifest. Only paths that name an entry of the archive are kept, so a manifest of millions costs no memory. */
|
|
178
248
|
async function readManifest(ctx, entry) {
|
|
179
249
|
const keys = new Set(ctx.zip.entries.map(e => (0, zip_1.nameKey)(e.name)));
|
|
180
250
|
const m = { paths: new Set(), encrypted: false };
|
|
181
251
|
await ctx.parse(entry, {
|
|
182
252
|
start(el) {
|
|
183
|
-
if (odfNs(el.ns) !== NS.manifest)
|
|
253
|
+
if (odfNs(el.ns) !== exports.NS.manifest)
|
|
184
254
|
return;
|
|
185
255
|
if (el.local === 'encryption-data')
|
|
186
256
|
m.encrypted = true;
|
|
187
257
|
if (el.local !== 'file-entry')
|
|
188
258
|
return;
|
|
189
|
-
const path = oattr(el, NS.manifest, 'full-path');
|
|
259
|
+
const path = oattr(el, exports.NS.manifest, 'full-path');
|
|
190
260
|
if (path === undefined)
|
|
191
261
|
return;
|
|
192
262
|
if (path === '/') {
|
|
193
263
|
if (m.root !== undefined)
|
|
194
264
|
ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.DuplicateEntry, entry.name, { kind: 'manifest path', name: '/' });
|
|
195
|
-
m.root = oattr(el, NS.manifest, 'media-type') ?? '';
|
|
265
|
+
m.root = oattr(el, exports.NS.manifest, 'media-type') ?? '';
|
|
196
266
|
return;
|
|
197
267
|
}
|
|
198
|
-
const key = packagePath(path);
|
|
268
|
+
const key = (0, exports.packagePath)(path);
|
|
199
269
|
if (!keys.has(key))
|
|
200
270
|
return;
|
|
201
271
|
if (m.paths.has(key))
|
|
@@ -217,12 +287,16 @@ function folderOf(name) {
|
|
|
217
287
|
}
|
|
218
288
|
/** The most of the mimetype entry that is kept. The longest media type in the table is 56 characters. */
|
|
219
289
|
const MAX_MIMETYPE = 256;
|
|
220
|
-
/**
|
|
221
|
-
|
|
290
|
+
/**
|
|
291
|
+
* Names an OpenDocument package's format by the media type in its mimetype entry or, without one, its manifest's root
|
|
292
|
+
* entry. Throws when the two name different formats, or the media type is not one this package reads. `layout` hears,
|
|
293
|
+
* with the entry it concerns, of each way the mimetype entry and the manifest leave the layout OpenDocument asks for.
|
|
294
|
+
*/
|
|
295
|
+
async function nameOdf(ctx, layout = () => undefined) {
|
|
222
296
|
const zip = ctx.zip;
|
|
223
297
|
const mimetype = zip.entries.find(e => e.name === 'mimetype');
|
|
224
298
|
const manifestEntry = zip.get('META-INF/manifest.xml');
|
|
225
|
-
const
|
|
299
|
+
const note = (reason) => layout(mimetype?.name ?? 'META-INF/manifest.xml', reason);
|
|
226
300
|
let declared;
|
|
227
301
|
if (mimetype) {
|
|
228
302
|
let head = Buffer.alloc(0);
|
|
@@ -234,34 +308,42 @@ async function inspectOdf(ctx) {
|
|
|
234
308
|
declared = head.toString('latin1') + (mimetype.size > MAX_MIMETYPE ? '...' : '');
|
|
235
309
|
const first = zip.entries.reduce((a, b) => (b.localOffset < a.localOffset ? b : a));
|
|
236
310
|
if (first !== mimetype)
|
|
237
|
-
|
|
311
|
+
note('mimetype is not the first entry');
|
|
238
312
|
if (mimetype.method !== 0)
|
|
239
|
-
|
|
313
|
+
note('mimetype is compressed');
|
|
240
314
|
if (mimetype.localExtra)
|
|
241
|
-
|
|
315
|
+
note('mimetype has an extra field');
|
|
242
316
|
if (declared !== declared.trim())
|
|
243
|
-
|
|
317
|
+
note('mimetype has white space around the media type');
|
|
244
318
|
declared = declared.trim();
|
|
245
319
|
}
|
|
246
320
|
else
|
|
247
|
-
|
|
321
|
+
note('there is no mimetype entry');
|
|
248
322
|
const manifest = manifestEntry ? await readManifest(ctx, manifestEntry) : undefined;
|
|
249
323
|
if (!manifest)
|
|
250
|
-
|
|
324
|
+
note('there is no manifest');
|
|
251
325
|
const root = manifest?.root;
|
|
252
326
|
if (declared !== undefined && root !== undefined && declared !== root && formats_1.ODF_FORMATS.has(declared) && formats_1.ODF_FORMATS.has(root)) {
|
|
253
327
|
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, 'The mimetype entry and the manifest name different formats', { mimetype: (0, context_1.clip)(declared), manifest: (0, context_1.clip)(root) });
|
|
254
328
|
}
|
|
255
|
-
const
|
|
256
|
-
const format = formats_1.ODF_FORMATS.get(
|
|
329
|
+
const mediaType = declared ?? root ?? '';
|
|
330
|
+
const format = formats_1.ODF_FORMATS.get(mediaType);
|
|
257
331
|
if (!format)
|
|
258
|
-
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The OpenDocument media type is not one this package reads', { mediaType: (0, context_1.clip)(
|
|
332
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The OpenDocument media type is not one this package reads', { mediaType: (0, context_1.clip)(mediaType) });
|
|
333
|
+
return { format, mediaType, mimetype, manifestEntry, manifest };
|
|
334
|
+
}
|
|
335
|
+
/** Inspects an OpenDocument package. */
|
|
336
|
+
async function inspectOdf(ctx) {
|
|
337
|
+
const zip = ctx.zip;
|
|
338
|
+
const { format, mediaType: type, mimetype, manifestEntry, manifest } = await nameOdf(ctx, (location, reason) => ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.OdfLayout, location, { reason }));
|
|
259
339
|
ctx.format = format;
|
|
260
340
|
if (manifest?.encrypted)
|
|
261
341
|
throw new errors_1.StopError(types_1.OfficeCategory.Encrypted, types_1.OfficeDetail.PasswordRequired, 'The manifest lists encrypted entries');
|
|
262
342
|
const folders = new Map();
|
|
263
343
|
const oleRefs = new Map(zip.entries.map(e => [(0, zip_1.nameKey)(e.name), false]));
|
|
264
344
|
const oleEntries = [];
|
|
345
|
+
/** Entries that are whole files in formats of their own, such as pictures, which go to the plugins. */
|
|
346
|
+
const wholeFiles = [];
|
|
265
347
|
for (const entry of zip.entries) {
|
|
266
348
|
if (entry === mimetype || entry === manifestEntry)
|
|
267
349
|
continue;
|
|
@@ -280,18 +362,64 @@ async function inspectOdf(ctx) {
|
|
|
280
362
|
await ctx.parse(entry, new EntryRules(ctx, entry.name, oleRefs));
|
|
281
363
|
else if (await ctx.drain(entry))
|
|
282
364
|
oleEntries.push(entry);
|
|
365
|
+
else if (!entry.directory && !/^meta-inf\//i.test(entry.name))
|
|
366
|
+
wholeFiles.push(entry);
|
|
283
367
|
}
|
|
368
|
+
const plan = ctx.plan;
|
|
369
|
+
// A folder goes whole, its empty files with it.
|
|
284
370
|
for (const [prefix, f] of folders)
|
|
285
|
-
ctx.report(f.category, f.detail, prefix, { entries: f.entries })
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
371
|
+
if (ctx.report(f.category, f.detail, prefix, { entries: f.entries }).action === 'strip')
|
|
372
|
+
plan.folders.add(`${(0, zip_1.nameKey)(prefix)}/`);
|
|
373
|
+
// OLE entries go to the plugins. When none takes one, the elements that refer to it decide, or EMBEDDED/OLE_FILE.
|
|
374
|
+
const decided = new Map();
|
|
375
|
+
for (const e of oleEntries) {
|
|
376
|
+
const key = (0, zip_1.nameKey)(e.name);
|
|
377
|
+
const decision = await (0, contained_1.decideContained)(ctx, { name: e.name, location: e.name, chunks: () => zip.read(e) });
|
|
378
|
+
if (decision) {
|
|
379
|
+
decided.set(key, decision.keep);
|
|
380
|
+
if (!decision.keep)
|
|
381
|
+
plan.drop.add(key);
|
|
382
|
+
else if (decision.replacement)
|
|
383
|
+
plan.replace.set(key, decision.replacement);
|
|
384
|
+
}
|
|
385
|
+
if (!oleRefs.get(key)) {
|
|
386
|
+
const r = ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleFile, e.name, { part: e.name }, (0, contained_1.presence)(decision));
|
|
387
|
+
if (!decision && r.action === 'strip')
|
|
388
|
+
plan.drop.add(key);
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
// A whole file no plugin takes stays, since no rule names it.
|
|
392
|
+
for (const e of wholeFiles) {
|
|
393
|
+
const decision = await (0, contained_1.decideContained)(ctx, { name: e.name, location: e.name, chunks: () => zip.read(e) });
|
|
394
|
+
if (decision && !decision.keep)
|
|
395
|
+
plan.drop.add((0, zip_1.nameKey)(e.name));
|
|
396
|
+
else if (decision?.replacement)
|
|
397
|
+
plan.replace.set((0, zip_1.nameKey)(e.name), decision.replacement);
|
|
398
|
+
}
|
|
399
|
+
const files = new Set(zip.entries.filter(e => !e.directory).map(e => (0, zip_1.nameKey)(e.name)));
|
|
400
|
+
for (const o of plan.objects) {
|
|
401
|
+
const kept = decided.get(o.target);
|
|
402
|
+
if (kept === true) {
|
|
403
|
+
// A plugin kept the object's file, so the object stays.
|
|
404
|
+
if (o.finding) {
|
|
405
|
+
o.finding.action = 'info';
|
|
406
|
+
ctx.run.superseded.add(o.finding);
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (kept === false || o.strips) {
|
|
411
|
+
plan.element(o.part, o.index, { remove: true });
|
|
412
|
+
if (files.has(o.target))
|
|
413
|
+
plan.drop.add(o.target);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
289
416
|
if (manifest) {
|
|
290
417
|
for (const e of zip.entries) {
|
|
291
418
|
if (e.directory || e === mimetype || e === manifestEntry || /^meta-inf\//i.test(e.name))
|
|
292
419
|
continue;
|
|
293
|
-
if (!manifest.paths.has((0, zip_1.nameKey)(e.name)))
|
|
294
|
-
|
|
420
|
+
if (!manifest.paths.has((0, zip_1.nameKey)(e.name)) && ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UnreferencedPart, e.name, { part: e.name }).action === 'strip')
|
|
421
|
+
plan.drop.add((0, zip_1.nameKey)(e.name));
|
|
295
422
|
}
|
|
296
423
|
}
|
|
424
|
+
plan.odf = { mimetype, manifest: manifestEntry, mediaType: type };
|
|
297
425
|
}
|
package/dist/offcrypto.d.ts
CHANGED
|
@@ -5,6 +5,8 @@ import { type ByteSource } from './types';
|
|
|
5
5
|
import type { Decrypt } from './xls';
|
|
6
6
|
/** The password Excel opens a workbook with when nobody set one to open it (MS-OFFCRYPTO 2.4.3). */
|
|
7
7
|
export declare const DEFAULT_PASSWORD = "VelvetSweatshop";
|
|
8
|
+
/** The password PowerPoint 97 to 2003 opens a presentation with when nobody set one to open it. */
|
|
9
|
+
export declare const POWERPOINT_PASSWORD = "/01Hannes Ruescher/01";
|
|
8
10
|
/** RC4, which OpenSSL 3 leaves out of its default provider. */
|
|
9
11
|
export declare class Rc4 {
|
|
10
12
|
private readonly s;
|
|
@@ -17,9 +19,30 @@ export declare class Rc4 {
|
|
|
17
19
|
/**
|
|
18
20
|
* Opens a workbook that its FILEPASS record (MS-XLS 2.4.117) encrypts with RC4 or RC4 CryptoAPI, with the caller's
|
|
19
21
|
* password or VelvetSweatshop. The records after FILEPASS are encrypted in 1024-byte blocks of the stream, each with its
|
|
20
|
-
* own key. Their headers are not, but take keystream bytes, and some records are never encrypted.
|
|
22
|
+
* own key. Their headers are not, but take keystream bytes, and some records are never encrypted. XOR obfuscation,
|
|
23
|
+
* the only kind an Excel 5.0 or 95 workbook has, is undone the same way record by record.
|
|
21
24
|
*/
|
|
22
25
|
export declare function unlockWorkbook(ctx: RunContext, filePass: BiffRecord, stream: string): Decrypt;
|
|
26
|
+
/** A stream decrypted where it is read. */
|
|
27
|
+
export type StreamDecrypt = (offset: number, bytes: Uint8Array) => Uint8Array;
|
|
28
|
+
/** What opens an encrypted Word 97 to 2003 document: a decryption for each of its three encrypted streams. */
|
|
29
|
+
export interface WordKey {
|
|
30
|
+
word: StreamDecrypt;
|
|
31
|
+
table: StreamDecrypt;
|
|
32
|
+
data: StreamDecrypt;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Opens an encrypted Word 97 to 2003 document (MS-DOC 2.2.6) with the caller's passwords. XOR obfuscation keeps its
|
|
36
|
+
* verifier in FibBase.lKey. RC4 and RC4 CryptoAPI keep their EncryptionHeader in the table stream's first lKey bytes.
|
|
37
|
+
* The WordDocument stream stays plain for its first 68 bytes, and Word has no default password.
|
|
38
|
+
*/
|
|
39
|
+
export declare function unlockDocument(ctx: RunContext, fib: Uint8Array, table: ByteSource, stream: string): Promise<WordKey>;
|
|
40
|
+
/**
|
|
41
|
+
* Opens an encrypted PowerPoint 97 to 2003 presentation (MS-PPT 2.3.7): `session` is the CryptSession10Container's
|
|
42
|
+
* data, an RC4 CryptoAPI EncryptionHeader. Returns the key of each persist object, whose block number is its persist
|
|
43
|
+
* id, after the caller's passwords and PowerPoint's own default have been tried.
|
|
44
|
+
*/
|
|
45
|
+
export declare function unlockPresentation(ctx: RunContext, session: Uint8Array, stream: string): (id: number) => Rc4;
|
|
23
46
|
/**
|
|
24
47
|
* Inspects an encrypted OOXML package: EncryptionInfo (MS-OFFCRYPTO 2.3.4) names the cipher, and the caller's password
|
|
25
48
|
* or VelvetSweatshop gives the key. The package it decrypts goes through the OOXML rules, read in place: Agile
|