@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/ooxml.js
CHANGED
|
@@ -1,26 +1,40 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.relsSource = exports.R_NS = exports.SHEET_NS = exports.WORD_NS = exports.isXmlType = exports.TYPE = exports.REL = exports.XPS_NS = exports.XPS_MAIN = exports.RELS_NS = exports.CONTENT_TYPES_NS = void 0;
|
|
4
|
+
exports.resolveTarget = resolveTarget;
|
|
5
|
+
exports.resolvePartRef = resolvePartRef;
|
|
6
|
+
exports.xpsResources = xpsResources;
|
|
7
|
+
exports.readRelationships = readRelationships;
|
|
8
|
+
exports.readParts = readParts;
|
|
9
|
+
exports.readMain = readMain;
|
|
10
|
+
exports.nameOoxml = nameOoxml;
|
|
3
11
|
exports.inspectOoxml = inspectOoxml;
|
|
4
12
|
const biff_1 = require("./biff");
|
|
13
|
+
const contained_1 = require("./contained");
|
|
5
14
|
const context_1 = require("./context");
|
|
6
15
|
const errors_1 = require("./errors");
|
|
7
16
|
const fields_1 = require("./fields");
|
|
8
17
|
const formats_1 = require("./formats");
|
|
9
18
|
const formulas_1 = require("./formulas");
|
|
10
19
|
const links_1 = require("./links");
|
|
20
|
+
const plan_1 = require("./plan");
|
|
11
21
|
const types_1 = require("./types");
|
|
12
22
|
const xlsb_1 = require("./xlsb");
|
|
13
23
|
const xml_1 = require("./xml");
|
|
14
24
|
const zip_1 = require("./zip");
|
|
15
|
-
|
|
16
|
-
|
|
25
|
+
exports.CONTENT_TYPES_NS = 'http://schemas.openxmlformats.org/package/2006/content-types';
|
|
26
|
+
exports.RELS_NS = 'http://schemas.openxmlformats.org/package/2006/relationships';
|
|
17
27
|
const T = 'http://schemas.openxmlformats.org/officedocument/2006/relationships/';
|
|
18
28
|
const S = 'http://purl.oclc.org/ooxml/officedocument/relationships/';
|
|
19
29
|
const MS = 'http://schemas.microsoft.com/office/2006/relationships/';
|
|
20
30
|
const both = (name) => [T + name, S + name];
|
|
31
|
+
/** The relationship types that name an XPS package's FixedDocumentSequence, lowercased. */
|
|
32
|
+
exports.XPS_MAIN = { xps: 'http://schemas.microsoft.com/xps/2005/06/fixedrepresentation', oxps: 'http://schemas.openxps.org/oxps/v1.0/fixedrepresentation' };
|
|
33
|
+
/** The XPS and OpenXPS markup namespaces, lowercased. */
|
|
34
|
+
exports.XPS_NS = new Set(['http://schemas.microsoft.com/xps/2005/06', 'http://schemas.openxps.org/oxps/v1.0']);
|
|
21
35
|
/** Relationship types, lowercased, as OPC compares them without regard to case. */
|
|
22
|
-
|
|
23
|
-
main: new Set([...both('officedocument'), 'http://schemas.microsoft.com/visio/2010/relationships/document']),
|
|
36
|
+
exports.REL = {
|
|
37
|
+
main: new Set([...both('officedocument'), 'http://schemas.microsoft.com/visio/2010/relationships/document', exports.XPS_MAIN.xps, exports.XPS_MAIN.oxps]),
|
|
24
38
|
hyperlink: new Set(both('hyperlink')),
|
|
25
39
|
template: new Set(both('attachedtemplate')),
|
|
26
40
|
image: new Set([...both('image'), 'http://schemas.microsoft.com/office/2007/relationships/hdphoto']),
|
|
@@ -46,7 +60,7 @@ const REACHED_AS = new Map([
|
|
|
46
60
|
[`${MS}activexcontrolbinary`, AS.controlBinary],
|
|
47
61
|
]);
|
|
48
62
|
/** Content types, lowercased. */
|
|
49
|
-
|
|
63
|
+
exports.TYPE = {
|
|
50
64
|
vba: new Set(['application/vnd.ms-office.vbaproject']),
|
|
51
65
|
macrosheet: new Set(['application/vnd.ms-excel.macrosheet+xml', 'application/vnd.ms-excel.intlmacrosheet+xml', 'application/vnd.ms-excel.macrosheet', 'application/vnd.ms-excel.intlmacrosheet']),
|
|
52
66
|
ole: new Set(['application/vnd.openxmlformats-officedocument.oleobject']),
|
|
@@ -55,30 +69,50 @@ const TYPE = {
|
|
|
55
69
|
vml: 'application/vnd.openxmlformats-officedocument.vmldrawing',
|
|
56
70
|
xlsbExternalLink: 'application/vnd.ms-excel.externallink',
|
|
57
71
|
};
|
|
72
|
+
/** Whether the rules read a part of this content type as XML. */
|
|
73
|
+
const isXmlType = (ct) => ct !== undefined && (ct.endsWith('+xml') || ct === 'application/xml' || ct === 'text/xml' || ct === exports.TYPE.vml);
|
|
74
|
+
exports.isXmlType = isXmlType;
|
|
75
|
+
/**
|
|
76
|
+
* Binary parts that belong to the package's own format rather than being files of their own: the records of a binary
|
|
77
|
+
* workbook, printer settings, obfuscated fonts, VBA project signatures and the origin of digital signatures.
|
|
78
|
+
*/
|
|
79
|
+
const formatBinary = (ct) => ct.startsWith('application/vnd.ms-excel.') ||
|
|
80
|
+
ct.endsWith('.printersettings') ||
|
|
81
|
+
ct === 'application/vnd.openxmlformats-officedocument.obfuscatedfont' ||
|
|
82
|
+
ct === 'application/vnd.ms-package.obfuscated-opentype' ||
|
|
83
|
+
ct.startsWith('application/vnd.ms-office.vbaprojectsignature') ||
|
|
84
|
+
ct === 'application/vnd.openxmlformats-package.digital-signature-origin';
|
|
58
85
|
/** Relationship types that leave the package, by the finding they make. Any other is EXTERNAL/OTHER. */
|
|
59
86
|
const EXTERNAL_KINDS = [
|
|
60
|
-
[REL.template, types_1.OfficeDetail.Template],
|
|
87
|
+
[exports.REL.template, types_1.OfficeDetail.Template],
|
|
61
88
|
[new Set([...both('oleobject'), ...both('package')]), types_1.OfficeDetail.Object],
|
|
62
|
-
[REL.image, types_1.OfficeDetail.Image],
|
|
63
|
-
[REL.frame, types_1.OfficeDetail.Frame],
|
|
64
|
-
[REL.subDocument, types_1.OfficeDetail.Include],
|
|
65
|
-
[REL.workbook, types_1.OfficeDetail.Workbook],
|
|
89
|
+
[exports.REL.image, types_1.OfficeDetail.Image],
|
|
90
|
+
[exports.REL.frame, types_1.OfficeDetail.Frame],
|
|
91
|
+
[exports.REL.subDocument, types_1.OfficeDetail.Include],
|
|
92
|
+
[exports.REL.workbook, types_1.OfficeDetail.Workbook],
|
|
66
93
|
];
|
|
67
|
-
|
|
68
|
-
|
|
94
|
+
exports.WORD_NS = new Set(['http://schemas.openxmlformats.org/wordprocessingml/2006/main', 'http://purl.oclc.org/ooxml/wordprocessingml/main']);
|
|
95
|
+
exports.SHEET_NS = new Set(['http://schemas.openxmlformats.org/spreadsheetml/2006/main', 'http://purl.oclc.org/ooxml/spreadsheetml/main']);
|
|
69
96
|
const XDR_NS = new Set(['http://schemas.openxmlformats.org/drawingml/2006/spreadsheetdrawing', 'http://purl.oclc.org/ooxml/drawingml/spreadsheetdrawing']);
|
|
70
|
-
|
|
97
|
+
exports.R_NS = new Set(['http://schemas.openxmlformats.org/officedocument/2006/relationships', 'http://purl.oclc.org/ooxml/officedocument/relationships']);
|
|
71
98
|
const CUSTOM_UI_NS = new Set(['http://schemas.microsoft.com/office/2006/01/customui', 'http://schemas.microsoft.com/office/2009/07/customui']);
|
|
72
99
|
const VML_EXCEL_NS = 'urn:schemas-microsoft-com:office:excel';
|
|
73
100
|
const VISIO_NS = 'http://schemas.microsoft.com/office/visio/2012/main';
|
|
101
|
+
const EXTENDED_NS = new Set(['http://schemas.openxmlformats.org/officedocument/2006/extended-properties', 'http://purl.oclc.org/ooxml/officedocument/extendedproperties']);
|
|
102
|
+
/**
|
|
103
|
+
* The attributes of a SpreadsheetML data connection that name where it loads from (ECMA-376 18.13): an ODC file or an
|
|
104
|
+
* offline cube on the connection, a connection string and command, a web query's URL, or a text file.
|
|
105
|
+
*/
|
|
106
|
+
const CONNECTION_SOURCES = { connection: ['odcFile', 'sourceFile'], dbPr: ['connection', 'command'], webPr: ['url'], textPr: ['sourceFile'] };
|
|
74
107
|
const VISIO_CODE = /\b(RUNADDON|RUNADDONWARGS|CALLTHIS|RUNMACRO|OPENFILE|DOOLEVERB)\s*\(/i;
|
|
75
108
|
/** A Word attribute, qualified as Word writes it or unqualified. */
|
|
76
|
-
const wordAttr = (el, local) => el.attributes.find(a => a.local === local && (a.ns === '' || WORD_NS.has(a.ns.toLowerCase())))?.value;
|
|
109
|
+
const wordAttr = (el, local) => el.attributes.find(a => a.local === local && (a.ns === '' || exports.WORD_NS.has(a.ns.toLowerCase())))?.value;
|
|
77
110
|
/** The key of the part a relationships part belongs to, '' for the package, or undefined for any other part. */
|
|
78
111
|
const relsSource = (relsName) => {
|
|
79
112
|
const m = /^(.*?)_rels\/([^/]*)\.rels$/i.exec(relsName);
|
|
80
113
|
return m ? (0, zip_1.nameKey)(m[1] + m[2]) : undefined;
|
|
81
114
|
};
|
|
115
|
+
exports.relsSource = relsSource;
|
|
82
116
|
/**
|
|
83
117
|
* Resolves an internal target against the folder of the part it belongs to, as RFC 3986 resolves a relative
|
|
84
118
|
* reference: a ".." at the root of the package stays at the root. LibreOffice writes "../customXml/item1.xml" in the
|
|
@@ -96,6 +130,52 @@ function resolveTarget(relsName, target) {
|
|
|
96
130
|
}
|
|
97
131
|
return (0, zip_1.nameKey)(out.join('/'));
|
|
98
132
|
}
|
|
133
|
+
/** A reference in an XPS part to another part: an absolute part name, or a path relative to the part's folder. */
|
|
134
|
+
function resolvePartRef(part, ref) {
|
|
135
|
+
const folder = part.includes('/') ? part.slice(0, part.lastIndexOf('/') + 1) : '';
|
|
136
|
+
const path = (ref.startsWith('/') ? ref.slice(1) : folder + ref).split('#')[0];
|
|
137
|
+
const out = [];
|
|
138
|
+
for (const seg of path.split('/')) {
|
|
139
|
+
if (seg === '..')
|
|
140
|
+
out.pop();
|
|
141
|
+
else if (seg !== '.' && seg !== '')
|
|
142
|
+
out.push(seg);
|
|
143
|
+
}
|
|
144
|
+
return (0, zip_1.nameKey)(out.join('/'));
|
|
145
|
+
}
|
|
146
|
+
/** The URIs an XPS ImageSource names: the value itself, or a ColorConvertedBitmap's image and color profile. */
|
|
147
|
+
function imageSourceUris(value) {
|
|
148
|
+
const t = value.trim();
|
|
149
|
+
if (!t.startsWith('{'))
|
|
150
|
+
return [t];
|
|
151
|
+
const words = t
|
|
152
|
+
.replace(/^\{|\}$/g, '')
|
|
153
|
+
.trim()
|
|
154
|
+
.split(/\s+/);
|
|
155
|
+
return words[0] === 'ColorConvertedBitmap' ? words.slice(1) : [];
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The resources an XPS element names in its attributes, as the attribute and the URI: a document, page or resource
|
|
159
|
+
* dictionary's Source, a font's FontUri, an image brush's image and profile, and a ContextColor's profile.
|
|
160
|
+
*/
|
|
161
|
+
function xpsResources(el) {
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const a of el.attributes) {
|
|
164
|
+
if (a.ns !== '')
|
|
165
|
+
continue;
|
|
166
|
+
if (a.local === 'Source' || a.local === 'FontUri')
|
|
167
|
+
out.push([a.local, a.value.trim()]);
|
|
168
|
+
else if (a.local === 'ImageSource')
|
|
169
|
+
for (const uri of imageSourceUris(a.value))
|
|
170
|
+
out.push([a.local, uri]);
|
|
171
|
+
else {
|
|
172
|
+
const m = /^\s*ContextColor\s+(\S+)/.exec(a.value);
|
|
173
|
+
if (m)
|
|
174
|
+
out.push([a.local, m[1]]);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return out.filter(([, uri]) => uri !== '');
|
|
178
|
+
}
|
|
99
179
|
const duplicate = (ctx, location, kind, name) => ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.DuplicateEntry, location, { kind, name: (0, context_1.clip)(name) });
|
|
100
180
|
/**
|
|
101
181
|
* The defaults and overrides of [Content_Types].xml. Only those that type an entry of the archive are kept, so a
|
|
@@ -108,7 +188,7 @@ async function readContentTypes(ctx, entry) {
|
|
|
108
188
|
const overrides = new Map();
|
|
109
189
|
await ctx.parse(entry, {
|
|
110
190
|
start(el) {
|
|
111
|
-
if (el.ns.toLowerCase() !== CONTENT_TYPES_NS)
|
|
191
|
+
if (el.ns.toLowerCase() !== exports.CONTENT_TYPES_NS)
|
|
112
192
|
return;
|
|
113
193
|
const type = (0, xml_1.attr)(el, '', 'ContentType')?.trim().toLowerCase();
|
|
114
194
|
if (type === undefined)
|
|
@@ -141,7 +221,7 @@ async function readRelationships(ctx, entry, each) {
|
|
|
141
221
|
const ids = new Set();
|
|
142
222
|
await ctx.parse(entry, {
|
|
143
223
|
start(el) {
|
|
144
|
-
if (el.ns.toLowerCase() !== RELS_NS || el.local !== 'Relationship')
|
|
224
|
+
if (el.ns.toLowerCase() !== exports.RELS_NS || el.local !== 'Relationship')
|
|
145
225
|
return;
|
|
146
226
|
const id = (0, xml_1.attr)(el, '', 'Id') ?? '';
|
|
147
227
|
if (ids.has(id))
|
|
@@ -155,35 +235,48 @@ async function readRelationships(ctx, entry, each) {
|
|
|
155
235
|
},
|
|
156
236
|
});
|
|
157
237
|
}
|
|
158
|
-
/**
|
|
238
|
+
/**
|
|
239
|
+
* Word field codes in WordprocessingML: fldSimple, and fldChar marks around instrText. A field a strip finding names
|
|
240
|
+
* goes into the plan by the element numbers of its marks.
|
|
241
|
+
*/
|
|
159
242
|
class Fields {
|
|
160
243
|
stack;
|
|
161
244
|
capture = false;
|
|
245
|
+
/** Inside a w:t, whose text is part of a field's result when a field's code has ended. */
|
|
246
|
+
shown = false;
|
|
162
247
|
constructor(ctx, part) {
|
|
163
|
-
this.stack = new fields_1.FieldStack(ctx, part);
|
|
248
|
+
this.stack = new fields_1.FieldStack(ctx, part, marks => ctx.plan.part(part).fields.push(marks));
|
|
164
249
|
}
|
|
165
250
|
start(el) {
|
|
166
251
|
if (el.local === 'fldSimple')
|
|
167
|
-
this.stack.
|
|
252
|
+
this.stack.simpleBegin(wordAttr(el, 'instr') ?? '', el.index);
|
|
168
253
|
else if (el.local === 'fldChar') {
|
|
169
254
|
const kind = wordAttr(el, 'fldCharType');
|
|
170
255
|
if (kind === 'begin')
|
|
171
|
-
this.stack.begin();
|
|
256
|
+
this.stack.begin(el.index);
|
|
172
257
|
else if (kind === 'separate')
|
|
173
|
-
this.stack.separate();
|
|
258
|
+
this.stack.separate(el.index);
|
|
174
259
|
else if (kind === 'end')
|
|
175
|
-
this.stack.end();
|
|
260
|
+
this.stack.end(el.index);
|
|
176
261
|
}
|
|
177
262
|
else if (el.local === 'instrText' || el.local === 'delInstrText')
|
|
178
263
|
this.capture = true;
|
|
264
|
+
else if (el.local === 't')
|
|
265
|
+
this.shown = true;
|
|
179
266
|
}
|
|
180
267
|
end(el) {
|
|
181
268
|
if (el.local === 'instrText' || el.local === 'delInstrText')
|
|
182
269
|
this.capture = false;
|
|
270
|
+
else if (el.local === 't')
|
|
271
|
+
this.shown = false;
|
|
272
|
+
else if (el.local === 'fldSimple')
|
|
273
|
+
this.stack.simpleEnd();
|
|
183
274
|
}
|
|
184
275
|
text(t) {
|
|
185
276
|
if (this.capture)
|
|
186
277
|
this.stack.text(t);
|
|
278
|
+
else if (this.shown)
|
|
279
|
+
this.stack.result(t);
|
|
187
280
|
}
|
|
188
281
|
finish() {
|
|
189
282
|
this.stack.finish();
|
|
@@ -211,6 +304,19 @@ class PartRules {
|
|
|
211
304
|
*/
|
|
212
305
|
formula;
|
|
213
306
|
macroText;
|
|
307
|
+
/** The part is an Excel 4.0 macro sheet, whose formulas are its macros and which its own finding covers. */
|
|
308
|
+
macroSheet = false;
|
|
309
|
+
/** Word's hyperlink base, in the extended properties, which relative hyperlinks resolve against. */
|
|
310
|
+
hyperlinkBase;
|
|
311
|
+
/** The parts this XPS part names in its markup, by key, which count as reached from it. */
|
|
312
|
+
refs = new Set();
|
|
313
|
+
/**
|
|
314
|
+
* The labels of the hyperlinks the part names by relationship Id: their text, tooltips and display text. The
|
|
315
|
+
* relationships part comes after the part, so the targets are compared with these as it is read.
|
|
316
|
+
*/
|
|
317
|
+
labels = new Map();
|
|
318
|
+
/** A hyperlink whose text is being read, with the depth of the element whose end ends it. */
|
|
319
|
+
label;
|
|
214
320
|
/** The last namespace seen and its lowercase form. Neighboring elements share one, and this spares a copy each. */
|
|
215
321
|
ns = '';
|
|
216
322
|
lower = '';
|
|
@@ -219,6 +325,11 @@ class PartRules {
|
|
|
219
325
|
this.part = part;
|
|
220
326
|
this.fields = new Fields(ctx, part);
|
|
221
327
|
}
|
|
328
|
+
/** Records that the rewrite removes the element, when the finding strips. */
|
|
329
|
+
remove(r, el) {
|
|
330
|
+
if (r.action === 'strip')
|
|
331
|
+
this.ctx.plan.element(this.part, el.index, { remove: true });
|
|
332
|
+
}
|
|
222
333
|
lowerNs(el) {
|
|
223
334
|
if (el.ns !== this.ns) {
|
|
224
335
|
this.ns = el.ns;
|
|
@@ -226,45 +337,88 @@ class PartRules {
|
|
|
226
337
|
}
|
|
227
338
|
return this.lower;
|
|
228
339
|
}
|
|
340
|
+
/** Adds labels for the hyperlink a relationship Id names. */
|
|
341
|
+
addLabels(id, labels) {
|
|
342
|
+
const list = this.labels.get(id) ?? [];
|
|
343
|
+
for (const l of labels)
|
|
344
|
+
if (l?.trim())
|
|
345
|
+
list.push(l);
|
|
346
|
+
if (list.length)
|
|
347
|
+
this.labels.set(id, list);
|
|
348
|
+
}
|
|
349
|
+
/** The labels a hyperlink element gives: the text of `textDepth`'s element, its tooltip, and an Excel display name. */
|
|
350
|
+
hyperlink(el, textDepth, also) {
|
|
351
|
+
const id = el.attributes.find(a => a.local === 'id' && exports.R_NS.has(a.ns.toLowerCase()))?.value;
|
|
352
|
+
if (id === undefined)
|
|
353
|
+
return;
|
|
354
|
+
if (textDepth === undefined || this.label)
|
|
355
|
+
this.addLabels(id, also);
|
|
356
|
+
else
|
|
357
|
+
this.label = { id, depth: textDepth, text: '', also: also.filter((l) => l !== undefined) };
|
|
358
|
+
}
|
|
229
359
|
start(el) {
|
|
230
360
|
const ns = this.lowerNs(el);
|
|
231
361
|
if (CUSTOM_UI_NS.has(ns)) {
|
|
232
362
|
if (el.depth === 1)
|
|
233
363
|
this.ctx.report(types_1.OfficeCategory.Ui, types_1.OfficeDetail.Ribbon, this.part);
|
|
234
364
|
for (const a of el.attributes) {
|
|
235
|
-
if (a.ns
|
|
236
|
-
|
|
365
|
+
if (a.ns !== '' || !a.value.trim() || !/^(on[A-Z]|get[A-Z]|loadImage$)/.test(a.local))
|
|
366
|
+
continue;
|
|
367
|
+
// The callback goes and the control stays, doing nothing.
|
|
368
|
+
if (this.ctx.report(types_1.OfficeCategory.Ui, types_1.OfficeDetail.Callback, this.part, { callback: `${a.local}=${(0, context_1.clip)(a.value)}` }).action === 'strip') {
|
|
369
|
+
this.ctx.plan.element(this.part, el.index, { strip: new Set([(0, plan_1.attributeKey)('', a.local)]) });
|
|
370
|
+
}
|
|
237
371
|
}
|
|
238
372
|
return;
|
|
239
373
|
}
|
|
240
|
-
if (
|
|
374
|
+
if (el.depth === 1 && el.local === 'macrosheet')
|
|
375
|
+
this.macroSheet = true;
|
|
376
|
+
if (exports.WORD_NS.has(ns)) {
|
|
241
377
|
this.fields.start(el);
|
|
242
|
-
|
|
378
|
+
if (el.local === 'hyperlink')
|
|
379
|
+
this.hyperlink(el, el.depth, [wordAttr(el, 'tooltip')]);
|
|
380
|
+
}
|
|
381
|
+
else if (exports.SHEET_NS.has(ns)) {
|
|
382
|
+
if (el.local === 'hyperlink')
|
|
383
|
+
this.hyperlink(el, undefined, [(0, xml_1.attr)(el, '', 'display'), (0, xml_1.attr)(el, '', 'tooltip')]);
|
|
243
384
|
if (el.local === 'f' || el.local === 'definedName')
|
|
244
|
-
this.formula = { shown: new Shown(), quotes: new formulas_1.Quotes(), ddeBar: false };
|
|
385
|
+
this.formula = { shown: new Shown(), quotes: new formulas_1.Quotes(), ddeBar: false, index: el.index, calls: new formulas_1.FunctionCalls() };
|
|
245
386
|
if (el.local === 'definedName') {
|
|
246
387
|
// Excel runs the macro a name like _xlnm.Auto_Open refers to, as it does in a binary workbook.
|
|
247
388
|
const name = (0, xml_1.attr)(el, '', 'name') ?? '';
|
|
248
389
|
const auto = (0, biff_1.autoName)(name);
|
|
249
390
|
if (auto)
|
|
250
|
-
this.ctx.report(types_1.OfficeCategory.Macro, auto.onOpen ? types_1.OfficeDetail.EventOnOpen : types_1.OfficeDetail.Event, this.part, { event: auto.event, name: (0, context_1.clip)(name) });
|
|
391
|
+
this.remove(this.ctx.report(types_1.OfficeCategory.Macro, auto.onOpen ? types_1.OfficeDetail.EventOnOpen : types_1.OfficeDetail.Event, this.part, { event: auto.event, name: (0, context_1.clip)(name) }), el);
|
|
251
392
|
}
|
|
252
393
|
else if (el.local === 'ddeLink')
|
|
253
|
-
this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, this.part, { application: (0, context_1.clip)((0, xml_1.attr)(el, '', 'ddeService') ?? ''), topic: (0, context_1.clip)((0, xml_1.attr)(el, '', 'ddeTopic') ?? '') });
|
|
394
|
+
this.remove(this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, this.part, { application: (0, context_1.clip)((0, xml_1.attr)(el, '', 'ddeService') ?? ''), topic: (0, context_1.clip)((0, xml_1.attr)(el, '', 'ddeTopic') ?? '') }), el);
|
|
395
|
+
else if (Object.hasOwn(CONNECTION_SOURCES, el.local))
|
|
396
|
+
this.connection(el);
|
|
254
397
|
}
|
|
255
398
|
else if (XDR_NS.has(ns)) {
|
|
256
399
|
const macro = (0, xml_1.attr)(el, '', 'macro');
|
|
257
|
-
|
|
258
|
-
|
|
400
|
+
// The shape stays, and clicking it runs nothing.
|
|
401
|
+
if (macro?.trim() && this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { macro: (0, context_1.clip)(macro) }).action === 'strip') {
|
|
402
|
+
this.ctx.plan.element(this.part, el.index, { strip: new Set([(0, plan_1.attributeKey)('', 'macro')]) });
|
|
403
|
+
}
|
|
259
404
|
}
|
|
260
405
|
else if (ns === VML_EXCEL_NS && el.local === 'FmlaMacro')
|
|
261
|
-
this.macroText = new Shown();
|
|
406
|
+
this.macroText = { shown: new Shown(), index: el.index };
|
|
262
407
|
else if (ns === VISIO_NS && el.local === 'Cell')
|
|
263
408
|
this.visioCell(el);
|
|
264
|
-
if (
|
|
409
|
+
else if (exports.XPS_NS.has(ns))
|
|
410
|
+
this.xps(el);
|
|
411
|
+
else if (EXTENDED_NS.has(ns) && el.local === 'HyperlinkBase')
|
|
412
|
+
this.hyperlinkBase = { shown: new Shown(), index: el.index };
|
|
413
|
+
if (el.local === 'hlinkClick' || el.local === 'hlinkHover' || el.local === 'hlinkMouseOver') {
|
|
265
414
|
this.drawingAction(el);
|
|
415
|
+
// In a text run the link sits in the run's properties, and the run's text is what the reader sees.
|
|
416
|
+
this.hyperlink(el, el.depth >= 2 ? el.depth - 2 : undefined, [(0, xml_1.attr)(el, '', 'tooltip')]);
|
|
417
|
+
}
|
|
266
418
|
}
|
|
267
419
|
text(t) {
|
|
420
|
+
if (this.label && this.label.text.length < 4096)
|
|
421
|
+
this.label.text += t;
|
|
268
422
|
this.fields.text(t);
|
|
269
423
|
const f = this.formula;
|
|
270
424
|
if (f) {
|
|
@@ -273,78 +427,131 @@ class PartRules {
|
|
|
273
427
|
for (const c of t)
|
|
274
428
|
if (f.quotes.outside(c) && c === '|')
|
|
275
429
|
f.ddeBar = true;
|
|
430
|
+
f.calls.write(t);
|
|
276
431
|
if (f.entity === undefined && t.includes(xml_1.UNKNOWN))
|
|
277
432
|
f.entity = (0, xml_1.unknownEntity)(t);
|
|
278
433
|
}
|
|
279
|
-
this.macroText?.add(t);
|
|
434
|
+
this.macroText?.shown.add(t);
|
|
435
|
+
this.hyperlinkBase?.shown.add(t);
|
|
280
436
|
}
|
|
281
437
|
end(el) {
|
|
282
438
|
const ns = this.lowerNs(el);
|
|
283
|
-
if (
|
|
439
|
+
if (this.label && el.depth === this.label.depth) {
|
|
440
|
+
this.addLabels(this.label.id, [this.label.text, ...this.label.also]);
|
|
441
|
+
this.label = undefined;
|
|
442
|
+
}
|
|
443
|
+
if (exports.WORD_NS.has(ns))
|
|
284
444
|
this.fields.end(el);
|
|
285
|
-
if (SHEET_NS.has(ns) && (el.local === 'f' || el.local === 'definedName') && this.formula) {
|
|
286
|
-
|
|
445
|
+
if (exports.SHEET_NS.has(ns) && (el.local === 'f' || el.local === 'definedName') && this.formula) {
|
|
446
|
+
// A cell keeps its cached value without the formula. A defined name goes whole.
|
|
447
|
+
const { ddeBar, entity, shown, calls } = this.formula;
|
|
287
448
|
if (ddeBar)
|
|
288
|
-
this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, this.part, { formula: (0, context_1.clip)(shown.text) });
|
|
449
|
+
this.remove(this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, this.part, { formula: (0, context_1.clip)(shown.text) }), this.formula);
|
|
289
450
|
else if (entity !== undefined)
|
|
290
|
-
this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, this.part, { formula: (0, context_1.clip)(shown.text), entity: (0, context_1.clip)(entity) });
|
|
451
|
+
this.remove(this.ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Formula, this.part, { formula: (0, context_1.clip)(shown.text), entity: (0, context_1.clip)(entity) }), this.formula);
|
|
452
|
+
if (!this.macroSheet)
|
|
453
|
+
for (const [fn, kind] of calls.found)
|
|
454
|
+
this.remove(this.ctx.report(kind === 'fetch' ? types_1.OfficeCategory.External : types_1.OfficeCategory.Macro, types_1.OfficeDetail.Formula, this.part, { function: (0, context_1.clip)(fn), formula: (0, context_1.clip)(shown.text) }), this.formula);
|
|
291
455
|
this.formula = undefined;
|
|
292
456
|
}
|
|
293
457
|
if (ns === VML_EXCEL_NS && el.local === 'FmlaMacro' && this.macroText) {
|
|
294
|
-
if (this.macroText.content)
|
|
295
|
-
this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { macro: (0, context_1.clip)(this.macroText.text.trim()) });
|
|
458
|
+
if (this.macroText.shown.content)
|
|
459
|
+
this.remove(this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { macro: (0, context_1.clip)(this.macroText.shown.text.trim()) }), this.macroText);
|
|
296
460
|
this.macroText = undefined;
|
|
297
461
|
}
|
|
462
|
+
if (this.hyperlinkBase && EXTENDED_NS.has(ns) && el.local === 'HyperlinkBase') {
|
|
463
|
+
const base = this.hyperlinkBase.shown.text.trim();
|
|
464
|
+
if (base && !(0, links_1.isFragment)(base))
|
|
465
|
+
this.remove(this.ctx.link(base, this.part, { source: 'hyperlink base' }), this.hyperlinkBase);
|
|
466
|
+
this.hyperlinkBase = undefined;
|
|
467
|
+
}
|
|
298
468
|
if (el.depth === 1)
|
|
299
469
|
this.fields.finish();
|
|
300
470
|
}
|
|
471
|
+
/**
|
|
472
|
+
* A data connection's source. Each source a connection names is a finding, and the rewrite empties it, so the
|
|
473
|
+
* connection stays for the query tables and pivot caches that name it and loads nothing.
|
|
474
|
+
*/
|
|
475
|
+
connection(el) {
|
|
476
|
+
const set = new Map();
|
|
477
|
+
for (const name of CONNECTION_SOURCES[el.local]) {
|
|
478
|
+
const a = el.attributes.find(x => x.ns === '' && x.local === name);
|
|
479
|
+
if (!a?.value.trim())
|
|
480
|
+
continue;
|
|
481
|
+
const data = { element: (0, context_1.clip)(el.qname), attribute: name, target: (0, context_1.clip)(a.value, 2000) };
|
|
482
|
+
if (this.ctx.report(types_1.OfficeCategory.External, types_1.OfficeDetail.DataConnection, this.part, data).action === 'strip')
|
|
483
|
+
set.set(a.qname, '');
|
|
484
|
+
}
|
|
485
|
+
if (set.size)
|
|
486
|
+
this.ctx.plan.element(this.part, el.index, { set });
|
|
487
|
+
}
|
|
301
488
|
/** PowerPoint action settings, in a DrawingML hyperlink's action attribute. The relationship it names is reported on its own. */
|
|
302
489
|
drawingAction(el) {
|
|
303
490
|
const written = ((0, xml_1.attr)(el, '', 'action') ?? '').trim();
|
|
304
491
|
const action = written.toLowerCase();
|
|
305
492
|
if (!action)
|
|
306
493
|
return;
|
|
307
|
-
const id = el.attributes.find(a => a.local === 'id' && R_NS.has(a.ns.toLowerCase()))?.value;
|
|
494
|
+
const id = el.attributes.find(a => a.local === 'id' && exports.R_NS.has(a.ns.toLowerCase()))?.value;
|
|
308
495
|
const data = { trigger: el.local, action: (0, context_1.clip)(written), ...(id === undefined ? {} : { relationship: (0, context_1.clip)(id) }) };
|
|
309
496
|
if (action.startsWith('ppaction://program'))
|
|
310
|
-
this.ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, this.part, data);
|
|
497
|
+
this.remove(this.ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.RunProgram, this.part, data), el);
|
|
311
498
|
else if (action.startsWith('ppaction://macro'))
|
|
312
|
-
this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, data);
|
|
499
|
+
this.remove(this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, data), el);
|
|
313
500
|
else if (action.startsWith('ppaction://ole'))
|
|
314
|
-
this.ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, this.part, data);
|
|
501
|
+
this.remove(this.ctx.report(types_1.OfficeCategory.Action, types_1.OfficeDetail.OleVerb, this.part, data), el);
|
|
315
502
|
}
|
|
316
503
|
visioCell(el) {
|
|
317
504
|
const n = (0, xml_1.attr)(el, '', 'N') ?? '';
|
|
318
505
|
const formula = (0, xml_1.attr)(el, '', 'F') ?? '';
|
|
319
506
|
if (VISIO_CODE.test(formula)) {
|
|
320
507
|
if (n.startsWith('Event'))
|
|
321
|
-
this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Event, this.part, { event: (0, context_1.clip)(n), formula: (0, context_1.clip)(formula) });
|
|
508
|
+
this.remove(this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Event, this.part, { event: (0, context_1.clip)(n), formula: (0, context_1.clip)(formula) }), el);
|
|
322
509
|
else
|
|
323
|
-
this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { cell: (0, context_1.clip)(n), formula: (0, context_1.clip)(formula) });
|
|
510
|
+
this.remove(this.ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.Link, this.part, { cell: (0, context_1.clip)(n), formula: (0, context_1.clip)(formula) }), el);
|
|
324
511
|
}
|
|
325
512
|
const v = (0, xml_1.attr)(el, '', 'V');
|
|
326
513
|
if (n === 'Address' && v?.trim() && !(0, links_1.isFragment)(v))
|
|
327
|
-
this.ctx.link(v, this.part, { source: 'Visio hyperlink' });
|
|
514
|
+
this.remove(this.ctx.link(v, this.part, { source: 'Visio hyperlink' }), el);
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* XPS markup, which names the parts it uses in attributes rather than in relationships. A resource inside the
|
|
518
|
+
* package counts as reached from this part. One outside it loads from outside, and the element that names it goes.
|
|
519
|
+
* A link with a scheme or a network path gets the link checks and loses its target. A relative link moves inside
|
|
520
|
+
* the document.
|
|
521
|
+
*/
|
|
522
|
+
xps(el) {
|
|
523
|
+
for (const a of el.attributes) {
|
|
524
|
+
if (a.ns !== '' || (a.local !== 'FixedPage.NavigateUri' && a.local !== 'OutlineTarget'))
|
|
525
|
+
continue;
|
|
526
|
+
if ((0, links_1.isFragment)(a.value) || !(0, links_1.isAbsoluteTarget)(a.value))
|
|
527
|
+
continue;
|
|
528
|
+
const data = { element: (0, context_1.clip)(el.qname) };
|
|
529
|
+
let r = this.ctx.link(a.value, this.part, data);
|
|
530
|
+
// The characters of a Glyphs element are what the reader sees.
|
|
531
|
+
if (r.action !== 'strip' && el.local === 'Glyphs')
|
|
532
|
+
r = this.ctx.labelMismatch(a.value, [(0, xml_1.attr)(el, '', 'UnicodeString')], this.part, data) ?? r;
|
|
533
|
+
if (r.action === 'strip')
|
|
534
|
+
this.ctx.plan.element(this.part, el.index, { strip: new Set([(0, plan_1.attributeKey)('', a.local)]) });
|
|
535
|
+
}
|
|
536
|
+
for (const [name, uri] of xpsResources(el)) {
|
|
537
|
+
if (!(0, links_1.isAbsoluteTarget)(uri)) {
|
|
538
|
+
this.refs.add(resolvePartRef(this.part, uri));
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
541
|
+
const detail = name === 'ImageSource' ? types_1.OfficeDetail.Image : name === 'Source' ? types_1.OfficeDetail.Include : types_1.OfficeDetail.Other;
|
|
542
|
+
this.remove(this.ctx.external(detail, uri, this.part, { element: (0, context_1.clip)(el.qname), attribute: name }, name), el);
|
|
543
|
+
}
|
|
328
544
|
}
|
|
329
545
|
}
|
|
330
|
-
/**
|
|
331
|
-
|
|
332
|
-
* Then each part is read in archive order, followed by its relationships, which are reported as they are read and
|
|
333
|
-
* leave behind only which parts they reach and how.
|
|
334
|
-
*/
|
|
335
|
-
async function inspectOoxml(ctx, contentTypes) {
|
|
546
|
+
/** Reads [Content_Types].xml and types each part by it. Directory entries are neither parts nor relationships parts. */
|
|
547
|
+
async function readParts(ctx, contentTypes) {
|
|
336
548
|
const { defaults, overrides } = await readContentTypes(ctx, contentTypes);
|
|
337
549
|
const parts = new Map();
|
|
338
550
|
const relsOf = new Map();
|
|
339
551
|
for (const entry of ctx.zip.entries) {
|
|
340
|
-
if (entry === contentTypes)
|
|
341
|
-
continue;
|
|
342
|
-
// OPC has no folders. A directory entry is still inflated once, so its bytes are checked like any other.
|
|
343
|
-
if (entry.directory) {
|
|
344
|
-
await ctx.drain(entry);
|
|
552
|
+
if (entry === contentTypes || entry.directory)
|
|
345
553
|
continue;
|
|
346
|
-
|
|
347
|
-
const source = relsSource(entry.name);
|
|
554
|
+
const source = (0, exports.relsSource)(entry.name);
|
|
348
555
|
if (source !== undefined) {
|
|
349
556
|
relsOf.set(source, entry);
|
|
350
557
|
continue;
|
|
@@ -353,59 +560,115 @@ async function inspectOoxml(ctx, contentTypes) {
|
|
|
353
560
|
const ext = /\.([^./]*)$/.exec(entry.name)?.[1]?.toLowerCase();
|
|
354
561
|
parts.set(key, { entry, contentType: overrides.get(key) ?? (ext === undefined ? undefined : defaults.get(ext)), reachedAs: 0, claimed: false, targets: new Set(), ole: false });
|
|
355
562
|
}
|
|
356
|
-
|
|
563
|
+
return { parts, relsOf };
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Reads the package relationships and names the format by the one main part they name: by its content type, or for
|
|
567
|
+
* XPS by the relationship type too, since XPS and OpenXPS share their content types. `each` sees every package
|
|
568
|
+
* relationship as it is read. Throws when no main part, or more than one, is named, or its format is unknown.
|
|
569
|
+
*/
|
|
570
|
+
async function readMain(ctx, { parts, relsOf }, each) {
|
|
571
|
+
const packageRels = relsOf.get('');
|
|
357
572
|
const mains = [];
|
|
573
|
+
let strict = false;
|
|
574
|
+
let mainType = '';
|
|
575
|
+
if (packageRels) {
|
|
576
|
+
await readRelationships(ctx, packageRels, r => {
|
|
577
|
+
if (exports.REL.main.has(r.type)) {
|
|
578
|
+
mains.push(r.external ? undefined : parts.get(resolveTarget(packageRels.name, r.target)));
|
|
579
|
+
strict = r.type.startsWith(S);
|
|
580
|
+
mainType = r.type;
|
|
581
|
+
}
|
|
582
|
+
each?.(packageRels, r);
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
if (mains.length > 1)
|
|
586
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, 'The package names more than one main part', { reason: 'more than one main part' });
|
|
587
|
+
const main = mains[0];
|
|
588
|
+
if (!main)
|
|
589
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The package has no main part', { reason: 'no main document part' });
|
|
590
|
+
const xps = mainType === exports.XPS_MAIN.xps || mainType === exports.XPS_MAIN.oxps;
|
|
591
|
+
const format = xps ? (main.contentType === formats_1.XPS_SEQUENCE ? formats_1.XPS_FORMATS[mainType === exports.XPS_MAIN.oxps ? 'oxps' : 'xps'] : undefined) : formats_1.OOXML_FORMATS.get(main.contentType ?? '');
|
|
592
|
+
if (!format)
|
|
593
|
+
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The main part has an unknown content type', { contentType: (0, context_1.clip)(main.contentType ?? '') });
|
|
594
|
+
return { format, strict, xps };
|
|
595
|
+
}
|
|
596
|
+
/** Names an OOXML package's format as its inspection does, reading only [Content_Types].xml and the package relationships. */
|
|
597
|
+
async function nameOoxml(ctx, contentTypes) {
|
|
598
|
+
return (await readMain(ctx, await readParts(ctx, contentTypes))).format;
|
|
599
|
+
}
|
|
600
|
+
/**
|
|
601
|
+
* Inspects an OOXML package. The package relationships come first, since they name the main part and so the format.
|
|
602
|
+
* Then each part is read in archive order, followed by its relationships, which are reported as they are read and
|
|
603
|
+
* leave behind only which parts they reach and how. The run names `outer` as the format when given, as for a Flat
|
|
604
|
+
* OPC package unpacked into a ZIP archive, and the package's own format is returned.
|
|
605
|
+
*/
|
|
606
|
+
async function inspectOoxml(ctx, contentTypes, outer) {
|
|
607
|
+
const typed = await readParts(ctx, contentTypes);
|
|
608
|
+
const { parts, relsOf } = typed;
|
|
609
|
+
const plan = ctx.plan;
|
|
610
|
+
const roots = new Set();
|
|
611
|
+
/** The labels each part gives its hyperlinks, by part key and relationship Id. */
|
|
612
|
+
const labelsOf = new Map();
|
|
358
613
|
const relate = (rels, from, r) => {
|
|
359
614
|
const location = `${rels.name}, ${(0, context_1.clip)(r.id)}`;
|
|
360
615
|
const part = r.external ? undefined : parts.get(resolveTarget(rels.name, r.target));
|
|
361
616
|
if (part) {
|
|
362
617
|
part.reachedAs |= REACHED_AS.get(r.type) ?? 0;
|
|
363
|
-
if (TYPE.control.has(from?.contentType ?? ''))
|
|
618
|
+
if (exports.TYPE.control.has(from?.contentType ?? ''))
|
|
364
619
|
part.claimed = true;
|
|
365
620
|
(from?.targets ?? roots).add(part);
|
|
366
621
|
}
|
|
367
|
-
// A hyperlink to a part of the package, or to a place in the document, goes nowhere else.
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
622
|
+
// A hyperlink to a part of the package, or to a place in the document, goes nowhere else. A relationship a strip
|
|
623
|
+
// finding names goes, and so does every element that refers to it.
|
|
624
|
+
let reported;
|
|
625
|
+
if (exports.REL.hyperlink.has(r.type)) {
|
|
626
|
+
if (!part && !(0, links_1.isFragment)(r.target)) {
|
|
627
|
+
reported = ctx.link(r.target, location);
|
|
628
|
+
const labels = from ? labelsOf.get((0, zip_1.nameKey)(from.entry.name))?.get(r.id) : undefined;
|
|
629
|
+
if (labels && reported.action !== 'strip')
|
|
630
|
+
reported = ctx.labelMismatch(r.target, labels, location) ?? reported;
|
|
631
|
+
}
|
|
371
632
|
}
|
|
372
633
|
else if (r.external) {
|
|
373
634
|
const kind = EXTERNAL_KINDS.find(([types]) => types.has(r.type))?.[1] ?? types_1.OfficeDetail.Other;
|
|
374
|
-
ctx.external(kind, r.target, location, { type: (0, context_1.clip)(r.type) }, (0, context_1.clip)(r.type.slice(r.type.lastIndexOf('/') + 1)));
|
|
635
|
+
reported = ctx.external(kind, r.target, location, { type: (0, context_1.clip)(r.type) }, (0, context_1.clip)(r.type.slice(r.type.lastIndexOf('/') + 1)));
|
|
375
636
|
}
|
|
637
|
+
if (reported?.action === 'strip')
|
|
638
|
+
plan.dropRelationship(rels.name, r.id);
|
|
376
639
|
};
|
|
377
|
-
const
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
});
|
|
384
|
-
}
|
|
385
|
-
if (mains.length > 1)
|
|
386
|
-
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.AmbiguousFormat, 'The package names more than one main part', { reason: 'more than one main part' });
|
|
387
|
-
const main = mains[0];
|
|
388
|
-
if (!main)
|
|
389
|
-
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The package has no main part', { reason: 'no main document part' });
|
|
390
|
-
const format = formats_1.OOXML_FORMATS.get(main.contentType ?? '');
|
|
391
|
-
if (!format)
|
|
392
|
-
throw new errors_1.StopError(types_1.OfficeCategory.Package, types_1.OfficeDetail.Unrecognized, 'The main part has an unknown content type', { contentType: (0, context_1.clip)(main.contentType ?? '') });
|
|
393
|
-
ctx.format = format;
|
|
640
|
+
const { format, strict, xps } = await readMain(ctx, typed, (rels, r) => relate(rels, undefined, r));
|
|
641
|
+
ctx.format = outer ?? format;
|
|
642
|
+
// OPC has no folders. A directory entry is still inflated once, so its bytes are checked like any other.
|
|
643
|
+
for (const entry of ctx.zip.entries)
|
|
644
|
+
if (entry.directory)
|
|
645
|
+
await ctx.drain(entry);
|
|
394
646
|
for (const [key, p] of parts) {
|
|
395
647
|
const ct = p.contentType;
|
|
396
648
|
if (ct === undefined) {
|
|
397
|
-
ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UntypedPart, p.entry.name, { part: p.entry.name })
|
|
649
|
+
if (ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UntypedPart, p.entry.name, { part: p.entry.name }).action === 'strip')
|
|
650
|
+
plan.drop.add(key);
|
|
398
651
|
p.ole = await ctx.drain(p.entry);
|
|
399
652
|
}
|
|
400
|
-
else if (
|
|
401
|
-
|
|
653
|
+
else if ((0, exports.isXmlType)(ct)) {
|
|
654
|
+
const rules = new PartRules(ctx, p.entry.name);
|
|
655
|
+
await ctx.parse(p.entry, rules, ct === exports.TYPE.vml);
|
|
656
|
+
if (rules.labels.size)
|
|
657
|
+
labelsOf.set(key, rules.labels);
|
|
658
|
+
for (const k of rules.refs) {
|
|
659
|
+
const t = parts.get(k);
|
|
660
|
+
if (t && t !== p)
|
|
661
|
+
p.targets.add(t);
|
|
662
|
+
}
|
|
402
663
|
}
|
|
403
|
-
else if (ct === TYPE.xlsbExternalLink) {
|
|
664
|
+
else if (ct === exports.TYPE.xlsbExternalLink) {
|
|
404
665
|
// The binary form of an Excel ddeLink.
|
|
405
666
|
const links = new xlsb_1.XlsbLinks();
|
|
406
667
|
p.ole = await ctx.drain(p.entry, c => links.write(c));
|
|
668
|
+
// The binary part is the link, so it goes whole.
|
|
407
669
|
for (const l of links.dde)
|
|
408
|
-
ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, p.entry.name, { application: (0, context_1.clip)(l.application), topic: (0, context_1.clip)(l.topic) })
|
|
670
|
+
if (ctx.report(types_1.OfficeCategory.Dde, types_1.OfficeDetail.Link, p.entry.name, { application: (0, context_1.clip)(l.application), topic: (0, context_1.clip)(l.topic) }).action === 'strip')
|
|
671
|
+
plan.drop.add(key);
|
|
409
672
|
}
|
|
410
673
|
else
|
|
411
674
|
p.ole = await ctx.drain(p.entry);
|
|
@@ -418,32 +681,51 @@ async function inspectOoxml(ctx, contentTypes) {
|
|
|
418
681
|
if (source !== '' && !parts.has(source))
|
|
419
682
|
await readRelationships(ctx, rels, r => relate(rels, undefined, r));
|
|
420
683
|
for (const p of parts.values())
|
|
421
|
-
partRules(ctx, p);
|
|
684
|
+
await partRules(ctx, p);
|
|
422
685
|
unreferenced(ctx, parts, relsOf, roots);
|
|
686
|
+
plan.graph = { contentTypes, parts, relsOf, roots, strict, xps };
|
|
687
|
+
return format;
|
|
423
688
|
}
|
|
424
|
-
/**
|
|
425
|
-
|
|
689
|
+
/**
|
|
690
|
+
* Findings about a whole part, from its content type and the relationships that reach it. The files inside the
|
|
691
|
+
* package, its OLE parts and embedded packages, go to the plugins first. A plugin's verdict decides whether such a
|
|
692
|
+
* part stays, and when no plugin takes it, the findings of this function do.
|
|
693
|
+
*/
|
|
694
|
+
async function partRules(ctx, p) {
|
|
426
695
|
const ct = p.contentType ?? '';
|
|
427
696
|
const loc = p.entry.name;
|
|
697
|
+
const key = (0, zip_1.nameKey)(loc);
|
|
428
698
|
const data = { part: loc, contentType: (0, context_1.clip)(ct) };
|
|
429
699
|
const as = (bit) => (p.reachedAs & bit) !== 0;
|
|
430
|
-
const vba = TYPE.vba.has(ct) || as(AS.vba);
|
|
431
|
-
const ole = TYPE.ole.has(ct) || as(AS.ole);
|
|
432
|
-
const control = TYPE.control.has(ct) || as(AS.control);
|
|
433
|
-
const controlBinary = TYPE.controlBinary.has(ct) || as(AS.controlBinary);
|
|
700
|
+
const vba = exports.TYPE.vba.has(ct) || as(AS.vba);
|
|
701
|
+
const ole = exports.TYPE.ole.has(ct) || as(AS.ole);
|
|
702
|
+
const control = exports.TYPE.control.has(ct) || as(AS.control);
|
|
703
|
+
const controlBinary = exports.TYPE.controlBinary.has(ct) || as(AS.controlBinary);
|
|
434
704
|
const pkg = as(AS.package);
|
|
705
|
+
// Besides OLE parts and packages, every part that is a whole file in a format of its own, such as an image, goes
|
|
706
|
+
// to the plugins. Such a part has no rule of its own, so one no plugin takes stays.
|
|
707
|
+
const wholeFile = !(0, exports.isXmlType)(p.contentType) && !formatBinary(ct);
|
|
708
|
+
const contained = vba || ole || pkg || controlBinary || p.ole || wholeFile;
|
|
709
|
+
const decision = contained ? await (0, contained_1.decideContained)(ctx, { name: loc, declaredType: ct || undefined, location: loc, chunks: () => ctx.zip.read(p.entry) }) : undefined;
|
|
710
|
+
const act = (0, contained_1.presence)(decision);
|
|
711
|
+
const found = [];
|
|
435
712
|
if (vba)
|
|
436
|
-
ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, loc, data);
|
|
437
|
-
|
|
438
|
-
|
|
713
|
+
found.push(ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.VbaProject, loc, data, act));
|
|
714
|
+
// A macro sheet becomes an empty worksheet, so the indexes of the sheets after it stay right.
|
|
715
|
+
if ((exports.TYPE.macrosheet.has(ct) || as(AS.macrosheet)) && ctx.report(types_1.OfficeCategory.Macro, types_1.OfficeDetail.XlmMacrosheet, loc, data).action === 'strip')
|
|
716
|
+
ctx.plan.sheets.add(key);
|
|
439
717
|
if (ole)
|
|
440
|
-
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, loc, data);
|
|
718
|
+
found.push(ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleObject, loc, data, act));
|
|
441
719
|
if (pkg)
|
|
442
|
-
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Package, loc, data);
|
|
720
|
+
found.push(ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.Package, loc, data, act));
|
|
443
721
|
if (control || (controlBinary && !p.claimed))
|
|
444
|
-
ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, loc, data);
|
|
722
|
+
found.push(ctx.report(types_1.OfficeCategory.Control, types_1.OfficeDetail.ActiveX, loc, data, control ? undefined : act));
|
|
445
723
|
if (p.ole && !vba && !ole && !control && !controlBinary && !pkg)
|
|
446
|
-
ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleFile, loc, data);
|
|
724
|
+
found.push(ctx.report(types_1.OfficeCategory.Embedded, types_1.OfficeDetail.OleFile, loc, data, act));
|
|
725
|
+
if (decision ? !decision.keep : found.some(f => f.action === 'strip'))
|
|
726
|
+
ctx.plan.drop.add(key);
|
|
727
|
+
else if (decision?.replacement)
|
|
728
|
+
ctx.plan.replace.set(key, decision.replacement);
|
|
447
729
|
}
|
|
448
730
|
/** Parts that no chain of internal relationships reaches from the package. */
|
|
449
731
|
function unreferenced(ctx, parts, relsOf, roots) {
|
|
@@ -457,12 +739,13 @@ function unreferenced(ctx, parts, relsOf, roots) {
|
|
|
457
739
|
queue.push(t);
|
|
458
740
|
}
|
|
459
741
|
}
|
|
460
|
-
for (const p of parts.values())
|
|
461
|
-
if (!reached.has(p))
|
|
462
|
-
ctx.
|
|
742
|
+
for (const p of parts.values()) {
|
|
743
|
+
if (!reached.has(p) && ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UnreferencedPart, p.entry.name, { part: p.entry.name }).action === 'strip')
|
|
744
|
+
ctx.plan.drop.add((0, zip_1.nameKey)(p.entry.name));
|
|
745
|
+
}
|
|
463
746
|
for (const [source, rels] of relsOf) {
|
|
464
747
|
const owner = parts.get(source);
|
|
465
|
-
if (source !== '' && !(owner && reached.has(owner)))
|
|
466
|
-
ctx.
|
|
748
|
+
if (source !== '' && !(owner && reached.has(owner)) && ctx.report(types_1.OfficeCategory.Package, types_1.OfficeDetail.UnreferencedPart, rels.name, { part: rels.name }).action === 'strip')
|
|
749
|
+
ctx.plan.drop.add((0, zip_1.nameKey)(rels.name));
|
|
467
750
|
}
|
|
468
751
|
}
|