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