This is an automated email from the ASF dual-hosted git repository.
tballison pushed a commit to branch 3x-improve-sax-ooxml
in repository https://gitbox.apache.org/repos/asf/tika.git
The following commit(s) were added to refs/heads/3x-improve-sax-ooxml by this
push:
new eed121aac8 larger sax->ooxml improvements
eed121aac8 is described below
commit eed121aac82facf124da180e2518adbea79751d2
Author: tallison <[email protected]>
AuthorDate: Mon Jul 13 11:44:54 2026 -0400
larger sax->ooxml improvements
---
.../tika/parser/microsoft/AbstractListManager.java | 6 ++
.../microsoft/ooxml/AbstractOOXMLExtractor.java | 48 +++++++++--
.../parser/microsoft/ooxml/MetadataExtractor.java | 32 ++++---
.../microsoft/ooxml/SAXBasedMetadataExtractor.java | 12 ++-
.../ooxml/SXSLFPowerPointExtractorDecorator.java | 49 +++++++++--
.../ooxml/SXWPFWordExtractorDecorator.java | 67 ++++++++++++---
.../microsoft/ooxml/VSDXExtractorDecorator.java | 4 +-
.../ooxml/XSSFExcelExtractorDecorator.java | 8 +-
.../microsoft/ooxml/XSSFSharedStringsShim.java | 7 ++
.../microsoft/ooxml/xwpf/XWPFFeatureExtractor.java | 98 ++++++++++++++++++++++
10 files changed, 274 insertions(+), 57 deletions(-)
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractListManager.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractListManager.java
index 2e5f8f21c4..06d3e98b1c 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractListManager.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/AbstractListManager.java
@@ -60,6 +60,12 @@ public abstract class AbstractListManager {
*/
public String incrementLevel(int levelNumber, LevelTuple[]
overrideLevelTuples) {
+ //a level beyond the defined levels is non-standard; return ""
rather than
+ //iterating up to it
+ if (levelNumber < 0 || levelNumber >= levelTuples.length) {
+ return "";
+ }
+
for (int i = lastLevel + 1; i < levelNumber; i++) {
if (i >= counts.size()) {
int val = getStart(i, overrideLevelTuples);
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/AbstractOOXMLExtractor.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/AbstractOOXMLExtractor.java
index f483b23b02..769775a492 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/AbstractOOXMLExtractor.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/AbstractOOXMLExtractor.java
@@ -65,6 +65,7 @@ import
org.apache.tika.parser.microsoft.OfficeParser.POIFSDocumentType;
import org.apache.tika.parser.microsoft.OfficeParserConfig;
import org.apache.tika.parser.microsoft.SummaryExtractor;
import org.apache.tika.sax.EmbeddedContentHandler;
+import org.apache.tika.sax.XHTMLBalancingHandler;
import org.apache.tika.sax.XHTMLContentHandler;
import org.apache.tika.utils.ExceptionUtils;
import org.apache.tika.utils.StringUtils;
@@ -512,8 +513,8 @@ public abstract class AbstractOOXMLExtractor implements
OOXMLExtractor {
}
/**
- * Emits an external reference as an anchor element with appropriate class.
- * Used for detecting external resources that could be security risks.
+ * Emits an external reference as an anchor element with appropriate class,
+ * surfacing external resources for downstream analysis.
*/
private void emitExternalRef(XHTMLContentHandler xhtml, String refType,
String url)
throws SAXException {
@@ -651,20 +652,28 @@ public abstract class AbstractOOXMLExtractor implements
OOXMLExtractor {
relatedPartPRC.getRelationship(i);
try {
PackagePart relatedPartPart =
-
parentPart.getRelatedPart(relatedPartPackageRelationship);
+ safeGetRelatedPart(parentPart,
relatedPartPackageRelationship);
+ if (relatedPartPart == null) {
+ continue;
+ }
+ //balance tags a failed sub-parse left open, else the
</div> below is misnested
+ XHTMLBalancingHandler balancer =
+ new XHTMLBalancingHandler(contentHandler);
try (InputStream stream =
relatedPartPart.getInputStream()) {
XMLReaderUtils.parseSAX(stream,
- new EmbeddedContentHandler(contentHandler),
context);
+ new EmbeddedContentHandler(balancer), context);
} catch (IOException | TikaException e) {
+ balancer.drainOpenElements();
+
parentMetadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
+ ExceptionUtils.getStackTrace(e));
+ } catch (SAXException e) {
+ balancer.drainOpenElements();
+ WriteLimitReachedException.throwIfWriteLimitReached(e);
parentMetadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
ExceptionUtils.getStackTrace(e));
}
- } catch (InvalidFormatException | IllegalArgumentException e) {
- //getRelatedPart throws an unchecked
IllegalArgumentException when the
- //target part is missing -- common in truncated/damaged
packages. Record
- //it and carry on so that one unreachable part does not
abandon the
- //remaining slides/parts.
+ } catch (InvalidFormatException e) {
parentMetadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
ExceptionUtils.getStackTrace(e));
}
@@ -674,4 +683,25 @@ public abstract class AbstractOOXMLExtractor implements
OOXMLExtractor {
}
+ /**
+ * Like {@code getRelatedPart} but returns null instead of throwing an
unchecked
+ * IllegalArgumentException when the target part is missing (common in
truncated files).
+ */
+ public static PackagePart safeGetRelatedPart(PackagePart source,
+ PackageRelationship
relationship)
+ throws InvalidFormatException {
+ if (source == null || relationship == null) {
+ return null;
+ }
+ if (!source.isRelationshipExists(relationship)) {
+ return null;
+ }
+ try {
+ return source.getRelatedPart(relationship);
+ } catch (IllegalArgumentException e) {
+ // Relationship exists but target part is missing from the package
+ return null;
+ }
+ }
+
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/MetadataExtractor.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/MetadataExtractor.java
index 8298e84095..9fcc518627 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/MetadataExtractor.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/MetadataExtractor.java
@@ -65,11 +65,9 @@ public class MetadataExtractor {
/**
* Hard cap on the accumulated text-content of a single property element
* inside docProps/custom.xml. Real OOXML property values are at most a few
- * hundred bytes; anything beyond this is either corruption or an attacker
- * trying to drive memory or CPU pressure (cf. the {@code <vt:decimal>}
- * BigDecimal DoS where a 1M-digit literal compresses ~1000:1 in deflate).
- * 64 KB leaves headroom for any legitimate value while bounding the
- * slow-path inputs decisively.
+ * hundred bytes; anything beyond this is malformed (e.g. a {@code
<vt:decimal>}
+ * with a 1M-digit literal). 64 KB leaves headroom for any legitimate value
+ * while bounding the slow-path inputs.
*/
static final int MAX_TEXT_BUFFER_LENGTH = 64 * 1024;
@@ -106,10 +104,9 @@ public class MetadataExtractor {
extractMetadata(extractor.getExtendedProperties(), metadata);
// Custom properties are read via SAX directly from the OPC part
// rather than through POI/XMLBeans. The XMLBeans path materializes
- // an attacker-controlled <vt:decimal> through BigDecimal(String),
- // which is O(n²) on JDK 17 -- a 3 KB crafted carrier with a
- // 1,000,000-digit literal burns ~25 s of CPU before this method
- // even returns. See ooxml-bigdecimal-dos.
+ // a <vt:decimal> through BigDecimal(String), which is O(n²) on
+ // JDK 17 -- a 3 KB carrier with a 1,000,000-digit literal burns
+ // ~25 s of CPU before this method even returns.
extractCustomPropertiesViaSAX(extractor.getPackage(), metadata);
}
}
@@ -204,12 +201,12 @@ public class MetadataExtractor {
/**
* Parse {@code docProps/custom.xml} directly via SAX, bypassing
- * POI/XMLBeans. The XMLBeans path materializes an attacker-controlled
- * {@code <vt:decimal>} through {@link BigDecimal#BigDecimal(String)}
- * during XML deserialization, which is O(n²) in the digit count on
- * JDK 17. By reading the part ourselves we can cap both the buffered
- * text content ({@link #MAX_TEXT_BUFFER_LENGTH}) and the decimal
- * literal length ({@link #MAX_DECIMAL_LENGTH}) before any slow parse
+ * POI/XMLBeans. The XMLBeans path materializes a {@code <vt:decimal>}
+ * through {@link BigDecimal#BigDecimal(String)} during XML
+ * deserialization, which is O(n²) in the digit count on JDK 17. By
+ * reading the part ourselves we can cap both the buffered text content
+ * ({@link #MAX_TEXT_BUFFER_LENGTH}) and the decimal literal length
+ * ({@link #MAX_DECIMAL_LENGTH}) before any slow parse
* runs.
*/
private void extractCustomPropertiesViaSAX(OPCPackage opcPackage, Metadata
metadata) {
@@ -349,9 +346,8 @@ public class MetadataExtractor {
break;
case "decimal":
// BigDecimal(String) is O(n²) on JDK 17; cap the input
- // length to keep an attacker-controlled <vt:decimal>
- // from burning CPU. Real values are < 50 chars; 256 is
- // generous. See ooxml-bigdecimal-dos.
+ // length so a large <vt:decimal> can't burn CPU. Real
+ // values are < 50 chars; 256 is generous.
if (trimmed.length() > MAX_DECIMAL_LENGTH) {
break;
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SAXBasedMetadataExtractor.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SAXBasedMetadataExtractor.java
index 941ad12906..4b7189a9fd 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SAXBasedMetadataExtractor.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SAXBasedMetadataExtractor.java
@@ -59,10 +59,9 @@ class SAXBasedMetadataExtractor extends MetadataExtractor {
/**
* Hard cap on the accumulated text-content of a single property element.
* Real OOXML property values are at most a few hundred bytes; anything
beyond
- * this is either corruption or an attacker trying to drive memory or CPU
- * pressure (cf. the {@code <vt:decimal>} BigDecimal DoS where a 1M-digit
- * literal compresses ~1000:1 in deflate). 64 KB leaves headroom for any
- * legitimate value while bounding the slow-path inputs decisively.
+ * this is malformed (e.g. a {@code <vt:decimal>} with a 1M-digit literal).
+ * 64 KB leaves headroom for any legitimate value while bounding the
+ * slow-path inputs.
*/
static final int MAX_TEXT_BUFFER_LENGTH = 64 * 1024;
@@ -482,9 +481,8 @@ class SAXBasedMetadataExtractor extends MetadataExtractor {
break;
case "decimal":
// BigDecimal(String) is O(n²) on JDK 17; cap the input
- // length to keep an attacker-controlled <vt:decimal>
- // from burning CPU. Real values are < 50 chars; 256 is
- // generous. See ooxml-bigdecimal-dos.
+ // length so a large <vt:decimal> can't burn CPU. Real
+ // values are < 50 chars; 256 is generous.
if (trimmed.length() > MAX_DECIMAL_LENGTH) {
break;
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXSLFPowerPointExtractorDecorator.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXSLFPowerPointExtractorDecorator.java
index 10124999a1..3c5d3f05fc 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXSLFPowerPointExtractorDecorator.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXSLFPowerPointExtractorDecorator.java
@@ -40,6 +40,7 @@ import org.xml.sax.SAXException;
import org.xml.sax.helpers.DefaultHandler;
import org.apache.tika.exception.TikaException;
+import org.apache.tika.exception.WriteLimitReachedException;
import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.Office;
import org.apache.tika.metadata.TikaCoreProperties;
@@ -48,6 +49,7 @@ import
org.apache.tika.parser.microsoft.ooxml.xslf.XSLFEventBasedPowerPointExtra
import org.apache.tika.sax.EmbeddedContentHandler;
import org.apache.tika.sax.XHTMLContentHandler;
import org.apache.tika.utils.ExceptionUtils;
+import org.apache.tika.utils.StringUtils;
import org.apache.tika.utils.XMLReaderUtils;
/**
@@ -100,6 +102,7 @@ public class SXSLFPowerPointExtractorDecorator extends
AbstractOOXMLExtractor {
protected void buildXHTML(XHTMLContentHandler xhtml) throws SAXException,
IOException {
loadCommentAuthors();
+ addCommentAuthorMetadata();
PackageRelationshipCollection slidesPRC = null;
@@ -110,17 +113,25 @@ public class SXSLFPowerPointExtractorDecorator extends
AbstractOOXMLExtractor {
ExceptionUtils.getStackTrace(e));
}
+ int hiddenSlideCount = 0;
if (slidesPRC != null && slidesPRC.size() > 0) {
for (int i = 0; i < slidesPRC.size(); i++) {
try {
-
handleSlidePart(mainDocument.getRelatedPart(slidesPRC.getRelationship(i)),
- xhtml);
+ PackagePart slidePart =
+ safeGetRelatedPart(mainDocument,
slidesPRC.getRelationship(i));
+ if (slidePart == null) {
+ continue;
+ }
+ hiddenSlideCount += handleSlidePart(slidePart, xhtml);
} catch (InvalidFormatException | ZipException |
IllegalArgumentException e) {
metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
ExceptionUtils.getStackTrace(e));
}
}
}
+ if (hiddenSlideCount > 0) {
+ metadata.set(Office.NUM_HIDDEN_SLIDES, hiddenSlideCount);
+ }
if (config.isIncludeSlideMasterContent()) {
handleGeneralTextContainingPart(XSLFRelation.SLIDE_MASTER.getRelation(),
"slide-master",
@@ -171,27 +182,50 @@ public class SXSLFPowerPointExtractorDecorator extends
AbstractOOXMLExtractor {
}
- private void handleSlidePart(PackagePart slidePart, XHTMLContentHandler
xhtml)
+ private void addCommentAuthorMetadata() {
+ for (String name : commentAuthors.nameMap.values()) {
+ if (!StringUtils.isBlank(name)) {
+ metadata.add(Office.COMMENT_PERSONS, name);
+ }
+ }
+ }
+
+ /**
+ * @return 1 if the slide is hidden, 0 otherwise
+ */
+ private int handleSlidePart(PackagePart slidePart, XHTMLContentHandler
xhtml)
throws IOException, SAXException {
Map<String, String> linkedRelationships =
loadLinkedRelationships(slidePart, false, metadata);
// Map<String, String> hyperlinks =
loadHyperlinkRelationships(packagePart);
+ int hidden = 0;
xhtml.startElement("div", "class", "slide-content");
//pass metadata so the body handler can emit HAS_* signals for the
slide, and keep a
//reference so we can flag hidden slides after the parse
- OOXMLWordAndPowerPointTextHandler wordAndPPTHandler =
- new OOXMLWordAndPowerPointTextHandler(
- new OOXMLTikaBodyPartHandler(xhtml, metadata),
linkedRelationships);
+ OOXMLTikaBodyPartHandler bodyHandler = new
OOXMLTikaBodyPartHandler(xhtml, metadata);
try (InputStream stream = slidePart.getInputStream()) {
+ OOXMLWordAndPowerPointTextHandler wordAndPPTHandler =
+ new OOXMLWordAndPowerPointTextHandler(bodyHandler,
linkedRelationships);
XMLReaderUtils.parseSAX(CloseShieldInputStream.wrap(stream),
new EmbeddedContentHandler(wordAndPPTHandler), context);
if (wordAndPPTHandler.isHiddenSlide()) {
metadata.set(Office.HAS_HIDDEN_SLIDES, true);
+ hidden = 1;
+ }
+ if (wordAndPPTHandler.hasAnimations()) {
+ metadata.set(Office.HAS_ANIMATIONS, true);
}
+ } catch (SAXException e) {
+ //don't let one bad slide abort the rest; close any open tags first
+ WriteLimitReachedException.throwIfWriteLimitReached(e);
+ metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
+ ExceptionUtils.getStackTrace(e));
+ bodyHandler.closeAnyPending();
} catch (TikaException | IOException e) {
metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
ExceptionUtils.getStackTrace(e));
+ bodyHandler.closeAnyPending();
}
xhtml.endElement("div");
@@ -226,6 +260,7 @@ public class SXSLFPowerPointExtractorDecorator extends
AbstractOOXMLExtractor {
handleGeneralTextContainingPart(XSLFRelation.CHART.getRelation(),
"chart", slidePart,
metadata, new OOXMLWordAndPowerPointTextHandler(new
OOXMLTikaBodyPartHandler(xhtml),
linkedRelationships));
+ return hidden;
}
/**
@@ -274,7 +309,7 @@ public class SXSLFPowerPointExtractorDecorator extends
AbstractOOXMLExtractor {
for (int i = 0; i < prc.size(); i++) {
PackagePart pp = null;
try {
- pp =
mainDocument.getRelatedPart(prc.getRelationship(i));
+ pp = safeGetRelatedPart(mainDocument,
prc.getRelationship(i));
} catch (InvalidFormatException e) {
metadata.add(TikaCoreProperties.TIKA_META_EXCEPTION_WARNING,
ExceptionUtils.getStackTrace(e));
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXWPFWordExtractorDecorator.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXWPFWordExtractorDecorator.java
index 746a4deaf1..08948aca21 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXWPFWordExtractorDecorator.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/SXWPFWordExtractorDecorator.java
@@ -21,6 +21,7 @@ import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Collections;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -48,12 +49,16 @@ import org.apache.tika.metadata.Metadata;
import org.apache.tika.metadata.Office;
import org.apache.tika.metadata.TikaCoreProperties;
import org.apache.tika.parser.ParseContext;
+import org.apache.tika.parser.microsoft.EMFParser;
import org.apache.tika.parser.microsoft.ooxml.xwpf.XWPFEventBasedWordExtractor;
+import org.apache.tika.parser.microsoft.ooxml.xwpf.XWPFFeatureExtractor;
import org.apache.tika.parser.microsoft.ooxml.xwpf.XWPFNumberingShim;
import org.apache.tika.parser.microsoft.ooxml.xwpf.XWPFStylesShim;
import org.apache.tika.sax.EmbeddedContentHandler;
+import org.apache.tika.sax.ToTextContentHandler;
import org.apache.tika.sax.XHTMLContentHandler;
import org.apache.tika.utils.ExceptionUtils;
+import org.apache.tika.utils.StringUtils;
import org.apache.tika.utils.XMLReaderUtils;
/**
@@ -70,7 +75,6 @@ import org.apache.tika.utils.XMLReaderUtils;
public class SXWPFWordExtractorDecorator extends AbstractOOXMLExtractor {
- //include all parts that might have embedded objects
//include all parts that might have embedded objects
private final static String[] MAIN_PART_RELATIONS =
new String[]{XWPFRelation.HEADER.getRelation(),
XWPFRelation.FOOTER.getRelation(),
@@ -99,6 +103,9 @@ public class SXWPFWordExtractorDecorator extends
AbstractOOXMLExtractor {
};
private final OPCPackage opcPackage;
+
+ //accumulated across all body parts as they're parsed; consumed by
handleEmbeddedParts()
+ private final Map<String, EmbeddedPartMetadata> embeddedPartMetadataMap =
new HashMap<>();
private final ParseContext context;
private final Metadata metadata;
@@ -159,6 +166,9 @@ public class SXWPFWordExtractorDecorator extends
AbstractOOXMLExtractor {
* subdocuments, and framesets.
*/
private void detectSecurityFeatures(PackagePart documentPart,
XHTMLContentHandler xhtml) {
+ //hidden text, track changes, comments, comment persons
+ new XWPFFeatureExtractor().process(documentPart, metadata, context);
+
// Check for attached template (external template reference)
try {
PackageRelationshipCollection templateRels =
@@ -359,9 +369,55 @@ public class SXWPFWordExtractorDecorator extends
AbstractOOXMLExtractor {
ExceptionUtils.getStackTrace(e));
bodyHandler.closeAnyPending();
}
+ //the body handler collects progId + the emf relationship id for
embedded OLE refs as
+ //it parses. Resolve the display name from the emf, then hand the map
to
+ //handleEmbeddedParts() via getEmbeddedPartMetadataMap() below.
+ Map<String, EmbeddedPartMetadata> partMap =
bodyHandler.getEmbeddedPartMetadataMap();
+ for (EmbeddedPartMetadata embeddedPartMetadata : partMap.values()) {
+ tryToParseEmbeddedName(packagePart, embeddedPartMetadata);
+ }
+ embeddedPartMetadataMap.putAll(partMap);
return bodyHandler;
}
+ @Override
+ protected Map<String, EmbeddedPartMetadata> getEmbeddedPartMetadataMap() {
+ return embeddedPartMetadataMap;
+ }
+
+ /**
+ * The OLE object's display name (e.g. "Acrobat Document") is stored in a
comment field in
+ * the emf that renders it. This mirrors
XWPFWordExtractorDecorator#tryToParseEmbeddedName.
+ */
+ private void tryToParseEmbeddedName(PackagePart sourcePart,
+ EmbeddedPartMetadata
embeddedPartMetadata) {
+ String rid = embeddedPartMetadata.getEmfRelationshipId();
+ if (StringUtils.isBlank(rid)) {
+ return;
+ }
+ try {
+ PackageRelationship rel = sourcePart.getRelationship(rid);
+ if (rel == null) {
+ return;
+ }
+ PackagePart emfPart = safeGetRelatedPart(sourcePart, rel);
+ if (emfPart == null ||
!"image/x-emf".equals(emfPart.getContentType())) {
+ return;
+ }
+ try (InputStream is = emfPart.getInputStream()) {
+ Metadata m = new Metadata();
+ ToTextContentHandler toTextContentHandler = new
ToTextContentHandler();
+ new EMFParser().parse(is, toTextContentHandler, m, new
ParseContext());
+
embeddedPartMetadata.setRenderedName(toTextContentHandler.toString().trim());
+
embeddedPartMetadata.setFullName(m.get(EMFParser.EMF_ICON_STRING));
+ }
+ } catch (SecurityException e) {
+ throw e;
+ } catch (Exception e) {
+ //we tried
+ }
+ }
+
/**
* Dumps comments that were not inlined at a {@code commentReference} in
the main document
* (matching the DOM parser, which appends orphaned comments at the end).
@@ -447,15 +503,6 @@ public class SXWPFWordExtractorDecorator extends
AbstractOOXMLExtractor {
}
}
- private PackagePart safeGetRelatedPart(PackagePart parent,
PackageRelationship rel) {
- try {
- return parent.getRelatedPart(rel);
- } catch (Exception e) {
- return null;
- }
- }
-
-
private XWPFStylesShim loadStyles(PackagePart packagePart)
throws InvalidFormatException, TikaException, IOException,
SAXException {
PackageRelationshipCollection stylesParts =
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/VSDXExtractorDecorator.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/VSDXExtractorDecorator.java
index 197ccf6eda..0447c8fe0f 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/VSDXExtractorDecorator.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/VSDXExtractorDecorator.java
@@ -96,7 +96,7 @@ public class VSDXExtractorDecorator extends
AbstractOOXMLExtractor {
PackageRelationshipCollection pageRels =
pagesPart.getRelationshipsByType(VISIO_PAGE_REL);
for (PackageRelationship rel : pageRels) {
- PackagePart pagePart = pagesPart.getRelatedPart(rel);
+ PackagePart pagePart = safeGetRelatedPart(pagesPart, rel);
if (pagePart != null) {
pageParts.add(pagePart);
}
@@ -119,7 +119,7 @@ public class VSDXExtractorDecorator extends
AbstractOOXMLExtractor {
if (rels.isEmpty()) {
return null;
}
- return part.getRelatedPart(rels.getRelationship(0));
+ return safeGetRelatedPart(part, rels.getRelationship(0));
}
@Override
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFExcelExtractorDecorator.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFExcelExtractorDecorator.java
index 194d206a0e..26ad974d46 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFExcelExtractorDecorator.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFExcelExtractorDecorator.java
@@ -283,7 +283,7 @@ public class XSSFExcelExtractorDecorator extends
AbstractOOXMLExtractor {
//swallow
}
- // Extract external data sources (HIGH security risk - can hide
malicious URLs)
+ // Extract external data sources (surface external URLs for downstream
analysis)
try {
extractExternalDataSources(container, xhtml);
} catch (InvalidFormatException | TikaException | IOException |
SAXException e) {
@@ -526,7 +526,7 @@ public class XSSFExcelExtractorDecorator extends
AbstractOOXMLExtractor {
emitExternalRef(xhtml, "oleLink", "relationship:" + rId);
}
}
- // DDE links - security risk: can execute commands
+ // DDE links
if ("ddeLink".equals(localName)) {
foundDdeLink = true;
String ddeService = atts.getValue("ddeService");
@@ -676,7 +676,7 @@ public class XSSFExcelExtractorDecorator extends
AbstractOOXMLExtractor {
return;
}
for (PackageRelationship rel : coll) {
- PackagePart threadedCommentPart = sheetPart.getRelatedPart(rel);
+ PackagePart threadedCommentPart = safeGetRelatedPart(sheetPart,
rel);
if (threadedCommentPart == null) {
continue;
}
@@ -703,7 +703,7 @@ public class XSSFExcelExtractorDecorator extends
AbstractOOXMLExtractor {
return;
}
for (PackageRelationship rel : coll) {
- PackagePart personsPart = workbookPart.getRelatedPart(rel);
+ PackagePart personsPart = safeGetRelatedPart(workbookPart, rel);
if (personsPart == null) {
continue;
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFSharedStringsShim.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFSharedStringsShim.java
index 8556d0fbb3..a5b2b518c6 100644
---
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFSharedStringsShim.java
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/XSSFSharedStringsShim.java
@@ -60,6 +60,13 @@ class XSSFSharedStringsShim {
}
String getItemAt(int idx) {
+ //a workbook whose sharedStrings part or relationship is missing still
has cells
+ //carrying t="s" indices. The string table is then empty and every
index is out of
+ //range. Return null (a supported outcome for the caller) so the
numeric cells and
+ //the rest of the workbook survive.
+ if (idx < 0 || idx >= strings.size()) {
+ return null;
+ }
return strings.get(idx);
}
diff --git
a/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xwpf/XWPFFeatureExtractor.java
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xwpf/XWPFFeatureExtractor.java
new file mode 100644
index 0000000000..3cb1935cc6
--- /dev/null
+++
b/tika-parsers/tika-parsers-standard/tika-parsers-standard-modules/tika-parser-microsoft-module/src/main/java/org/apache/tika/parser/microsoft/ooxml/xwpf/XWPFFeatureExtractor.java
@@ -0,0 +1,98 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.tika.parser.microsoft.ooxml.xwpf;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.apache.poi.openxml4j.opc.PackagePart;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import org.apache.tika.exception.TikaException;
+import org.apache.tika.metadata.Metadata;
+import org.apache.tika.metadata.Office;
+import org.apache.tika.parser.ParseContext;
+import org.apache.tika.utils.StringUtils;
+import org.apache.tika.utils.XMLReaderUtils;
+
+/**
+ * This is designed to extract features that are useful for forensics,
e-discovery and digital preservation.
+ * Specifically, the presence of: tracked changes, hidden text, comments and
comment authors. Because several of these
+ * features can be placed on run properties, which can be in lots of places,
we're scraping
+ * the document xml
+ */
+public class XWPFFeatureExtractor {
+
+ public void process(XWPFDocument xwpfDocument, Metadata metadata,
ParseContext parseContext) {
+ process(xwpfDocument.getPackagePart(), metadata, parseContext);
+ }
+
+ public void process(PackagePart packagePart, Metadata metadata,
ParseContext parseContext) {
+ try (InputStream is = packagePart.getInputStream()) {
+ FeatureHandler featureHandler = new FeatureHandler();
+ XMLReaderUtils.parseSAX(is, featureHandler, parseContext);
+ if (featureHandler.hasComments) {
+ metadata.set(Office.HAS_COMMENTS, true);
+ }
+ if (featureHandler.hasHidden) {
+ metadata.set(Office.HAS_HIDDEN_TEXT, true);
+ }
+ if (featureHandler.hasTrackChanges) {
+ metadata.set(Office.HAS_TRACK_CHANGES, true);
+ }
+ if (! featureHandler.authors.isEmpty()) {
+ for (String author : featureHandler.authors) {
+ metadata.add(Office.COMMENT_PERSONS, author);
+ }
+ }
+ } catch (IOException | TikaException | SAXException e) {
+ //swallow
+ }
+ }
+
+ private static class FeatureHandler extends DefaultHandler {
+ //see:
https://www.ericwhite.com/blog/using-xml-dom-to-detect-tracked-revisions-in-an-open-xml-wordprocessingml-document/
+ private static final Set<String> TRACK_CHANGES = Set.of("ins", "del",
"moveFrom", "moveTo");
+ private final Set<String> authors = new HashSet<>();
+ private boolean hasHidden = false;
+ private boolean hasTrackChanges = false;
+ private boolean hasComments = false;
+
+ @Override
+ public void startElement(String uri, String localName, String qName,
Attributes atts)
+ throws SAXException {
+ //we could check to ensure that the vanish element actually
surrounds text
+ //the current check could lead to false positives where
<w:vanish/> is around a space or no text.
+ if ("vanish".equals(localName)) {
+ hasHidden = true;
+ } else if (TRACK_CHANGES.contains(localName)) {
+ String trackChangesAuthor =
XMLReaderUtils.getAttrValue("author", atts);
+ if (!StringUtils.isBlank(trackChangesAuthor)) {
+ authors.add(trackChangesAuthor);
+ }
+ hasTrackChanges = true;
+ } else if ("commentReference".equals(localName) ||
"commentRangeStart".equals(localName)) {
+ hasComments = true;
+ }
+ }
+ }
+}