Index: src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java (revision 1882098) +++ src/ooxml/java/org/apache/poi/poifs/crypt/dsig/facets/OOXMLSignatureFacet.java (date 1601506892862) @@ -31,6 +31,7 @@ import java.net.URISyntaxException; import java.util.ArrayList; import java.util.Arrays; +import java.util.Base64; import java.util.Collections; import java.util.Comparator; import java.util.HashSet; @@ -62,6 +63,7 @@ import org.apache.poi.openxml4j.opc.PackageRelationshipCollection; import org.apache.poi.openxml4j.opc.PackagingURIHelper; import org.apache.poi.openxml4j.opc.TargetMode; +import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.poifs.crypt.dsig.SignatureConfig; import org.apache.poi.poifs.crypt.dsig.SignatureInfo; import org.apache.poi.poifs.crypt.dsig.services.RelationshipTransformService; @@ -256,10 +258,21 @@ SignatureInfoV1Document sigV1 = SignatureInfoV1Document.Factory.newInstance(); CTSignatureInfoV1 ctSigV1 = sigV1.addNewSignatureInfoV1(); - ctSigV1.setManifestHashAlgorithm(signatureConfig.getDigestMethodUri()); + if (signatureConfig.getDigestAlgo() != HashAlgorithm.sha1) { + ctSigV1.setManifestHashAlgorithm(signatureConfig.getDigestMethodUri()); + } - if (signatureConfig.getSignatureDescription() != null) { - ctSigV1.setSignatureComments(signatureConfig.getSignatureDescription()); + String desc = signatureConfig.getSignatureDescription(); + if (desc != null) { + ctSigV1.setSignatureComments(desc); + } + + byte[] image = signatureConfig.getSignatureImage(); + if (image != null) { + // libre office is not showing anything without the setup id ... + ctSigV1.setSetupID(signatureConfig.getSignatureImageSetupId().toString()); + ctSigV1.setSignatureImage(image); + ctSigV1.setSignatureType(2); } Element n = (Element)document.importNode(ctSigV1.getDomNode(), true); @@ -282,6 +295,27 @@ Reference reference = newReference(signatureInfo, "#" + objectId, null, XML_DIGSIG_NS+"Object", null, null); references.add(reference); + + Base64.Encoder enc = Base64.getEncoder(); + byte[] imageValid = signatureConfig.getSignatureImageValid(); + if (imageValid != null) { + objectId = "idValidSigLnImg"; + DOMStructure tn = new DOMStructure(document.createTextNode(enc.encodeToString(imageValid))); + objects.add(sigFac.newXMLObject(Collections.singletonList(tn), objectId, null, null)); + + reference = newReference(signatureInfo, "#" + objectId, null, XML_DIGSIG_NS+"Object", null, null); + references.add(reference); + } + + byte[] imageInvalid = signatureConfig.getSignatureImageInvalid(); + if (imageInvalid != null) { + objectId = "idInvalidSigLnImg"; + DOMStructure tn = new DOMStructure(document.createTextNode(enc.encodeToString(imageInvalid))); + objects.add(sigFac.newXMLObject(Collections.singletonList(tn), objectId, null, null)); + + reference = newReference(signatureInfo, "#" + objectId, null, XML_DIGSIG_NS+"Object", null, null); + references.add(reference); + } } protected static String getRelationshipReferenceURI(String zipEntryName) { Index: src/ooxml/java/org/apache/poi/xslf/model/ParagraphPropertyFetcher.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/ooxml/java/org/apache/poi/xslf/model/ParagraphPropertyFetcher.java (revision 1882098) +++ src/ooxml/java/org/apache/poi/xslf/model/ParagraphPropertyFetcher.java (date 1601408159649) @@ -19,6 +19,8 @@ package org.apache.poi.xslf.model; +import static org.apache.poi.ooxml.util.XPathHelper.selectProperty; + import java.util.function.Consumer; import javax.xml.namespace.QName; @@ -113,7 +115,7 @@ static CTTextParagraphProperties select(XSLFShape shape, int level) throws XmlException { QName[] lvlProp = { new QName(DML_NS, "lvl" + (level + 1) + "pPr") }; - return shape.selectProperty( + return selectProperty(shape.getXmlObject(), CTTextParagraphProperties.class, ParagraphPropertyFetcher::parse, TX_BODY, LST_STYLE, lvlProp); } Index: src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java (revision 1882098) +++ src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureConfig.java (date 1601402989662) @@ -45,6 +45,7 @@ import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; import org.apache.poi.EncryptedDocumentException; +import org.apache.poi.hpsf.ClassID; import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.poifs.crypt.HashAlgorithm; import org.apache.poi.poifs.crypt.dsig.facets.KeyInfoSignatureFacet; @@ -89,10 +90,10 @@ ); - private ThreadLocal opcPackage = new ThreadLocal<>(); - private ThreadLocal signatureFactory = new ThreadLocal<>(); - private ThreadLocal keyInfoFactory = new ThreadLocal<>(); - private ThreadLocal provider = new ThreadLocal<>(); + private final ThreadLocal opcPackage = new ThreadLocal<>(); + private final ThreadLocal signatureFactory = new ThreadLocal<>(); + private final ThreadLocal keyInfoFactory = new ThreadLocal<>(); + private final ThreadLocal provider = new ThreadLocal<>(); private List signatureFacets = new ArrayList<>(); private HashAlgorithm digestAlgo = HashAlgorithm.sha256; @@ -165,6 +166,26 @@ */ private String signatureDescription = "Office OpenXML Document"; + /** + * Only applies when working with visual signatures: + * Specifies a GUID which can be cross-referenced with the GUID of the signature line stored in the document content. + * I.e. the signatureline element id attribute in the document/sheet has to be references in the SetupId element. + */ + private ClassID signatureImageSetupId; + + /** + * Provides a signature image for visual signature lines + */ + private byte[] signatureImage; + /** + * The image shown, when the signature is valid + */ + private byte[] signatureImageValid; + /** + * The image shown, when the signature is invalid + */ + private byte[] signatureImageInvalid; + /** * The process of signing includes the marshalling of xml structures. * This also includes the canonicalization. Currently this leads to problems @@ -386,6 +407,29 @@ this.signatureDescription = signatureDescription; } + public byte[] getSignatureImage() { + return signatureImage; + } + + public byte[] getSignatureImageValid() { + return signatureImageValid; + } + + public byte[] getSignatureImageInvalid() { + return signatureImageInvalid; + } + + public ClassID getSignatureImageSetupId() { + return signatureImageSetupId; + } + + public void setSignatureImage(ClassID signatureImageSetupId, byte[] signatureImage, byte[] signatureImageValid, byte[] signatureImageInvalid) { + this.signatureImageSetupId = signatureImageSetupId; + this.signatureImage = signatureImage; + this.signatureImageValid = signatureImageValid; + this.signatureImageInvalid = signatureImageInvalid; + } + /** * @return the default canonicalization method, defaults to INCLUSIVE */ Index: src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureImage.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureImage.java (date 1601422311021) +++ src/ooxml/java/org/apache/poi/poifs/crypt/dsig/SignatureImage.java (date 1601422311021) @@ -0,0 +1,162 @@ +/* ==================================================================== + 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.poi.poifs.crypt.dsig; + +import java.awt.AlphaComposite; +import java.awt.Color; +import java.awt.Font; +import java.awt.GradientPaint; +import java.awt.Graphics2D; +import java.awt.RenderingHints; +import java.awt.Shape; +import java.awt.font.FontRenderContext; +import java.awt.font.LineBreakMeasurer; +import java.awt.font.TextAttribute; +import java.awt.font.TextLayout; +import java.awt.geom.AffineTransform; +import java.awt.geom.Dimension2D; +import java.awt.geom.Rectangle2D; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.text.AttributedCharacterIterator; +import java.text.AttributedString; + +import javax.imageio.ImageIO; + +import org.apache.poi.poifs.filesystem.FileMagic; +import org.apache.poi.sl.draw.DrawPictureShape; +import org.apache.poi.sl.draw.ImageRenderer; +import org.apache.poi.sl.usermodel.PictureData.PictureType; +import org.apache.poi.util.Beta; + +/** + * Makeshift helper class to generate signature images, feel free to provide your own + */ +@Beta +public class SignatureImage { + /** + * Generate the image for a signature line + * @param caption three lines separated by "\n" - usually something like "First name Last name\nRole\nname of the key" + * @param inputImage the plain signature - supported formats are PNG,GIF,JPEG,(SVG),EMF,WMF. + * for SVG,EMF,WMF poi-scratchpad needs to be in the class-/modulepath + * if {@code null}, the inputImage is not rendered + * @param invalidText for invalid signature images, use the given text + * @return the signature image in PNG format as byte array + */ + public static byte[] generateImage(String caption, byte[] inputImage, String invalidText) throws IOException { + BufferedImage bi = new BufferedImage(400, 150, BufferedImage.TYPE_INT_ARGB); + Graphics2D gfx = bi.createGraphics(); + gfx.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON); + gfx.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY); + gfx.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON); + gfx.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON); + + String markX = "X\n"; + String lineX = (new String(new char[500]).replace("\0", " ")) +"\n"; + String text = markX+lineX+caption.replaceAll("(?m)^", " "); + + AttributedString as = new AttributedString(text); + as.addAttribute(TextAttribute.FAMILY, Font.SANS_SERIF); + as.addAttribute(TextAttribute.UNDERLINE, TextAttribute.UNDERLINE_ON, markX.length(), text.indexOf('\n', markX.length())); + + as.addAttribute(TextAttribute.SIZE, 15, 0, markX.length()); + as.addAttribute(TextAttribute.SIZE, 12, markX.length(), text.length()); + + gfx.setColor(Color.BLACK); + + AttributedCharacterIterator chIter = as.getIterator(); + FontRenderContext frc = gfx.getFontRenderContext(); + LineBreakMeasurer measurer = new LineBreakMeasurer(chIter, frc); + float y = 80, x = 5; + for (int lineNr = 0; measurer.getPosition() < chIter.getEndIndex(); lineNr++) { + int mpos = measurer.getPosition(); + int limit = text.indexOf('\n', mpos); + limit = (limit == -1) ? text.length() : limit+1; + TextLayout textLayout = measurer.nextLayout(bi.getWidth()-10, limit, false); + if (lineNr != 1) { + y += textLayout.getAscent(); + } + textLayout.draw(gfx, x, y); + y += textLayout.getDescent() + textLayout.getLeading(); + } + + if (inputImage != null) { + FileMagic fm = FileMagic.valueOf(inputImage); + String contentType; + switch (fm) { + case GIF: + contentType = PictureType.GIF.contentType; + break; + case PNG: + contentType = PictureType.PNG.contentType; + break; + case JPEG: + contentType = PictureType.JPEG.contentType; + break; + case XML: + contentType = PictureType.SVG.contentType; + break; + case EMF: + contentType = PictureType.EMF.contentType; + break; + case WMF: + contentType = PictureType.WMF.contentType; + break; + default: + throw new RuntimeException("unknown image type"); + } + + ImageRenderer renderer = DrawPictureShape.getImageRenderer(gfx, contentType); + + renderer.loadImage(inputImage, contentType); + + double targetX = 10; + double targetY = 100; + double targetWidth = bi.getWidth() - targetX; + double targetHeight = targetY - 5; + Dimension2D dim = renderer.getDimension(); + double scale = Math.min(targetWidth / dim.getWidth(), targetHeight / dim.getHeight()); + double effWidth = dim.getWidth() * scale; + double effHeight = dim.getHeight() * scale; + + renderer.drawImage(gfx, new Rectangle2D.Double(targetX + ((bi.getWidth() - effWidth) / 2), targetY - effHeight, effWidth, effHeight)); + } + + if (invalidText != null) { + gfx.setFont(new Font("Lucida Bright", Font.ITALIC, 60)); + gfx.rotate(Math.toRadians(-15), bi.getWidth()/2., bi.getHeight()/2.); + TextLayout tl = new TextLayout(invalidText, gfx.getFont(), gfx.getFontRenderContext()); + Rectangle2D bounds = tl.getBounds(); + x = (float)((bi.getWidth()-bounds.getWidth())/2 - bounds.getX()); + y = (float)((bi.getHeight()-bounds.getHeight())/2 - bounds.getY()); + Shape outline = tl.getOutline(AffineTransform.getTranslateInstance(x+2, y+1)); + gfx.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.3f)); + gfx.setPaint(Color.RED); + gfx.draw(outline); + gfx.setPaint(new GradientPaint(0, 0, Color.RED, 30, 20, new Color(128, 128, 255), true)); + tl.draw(gfx, x, y); + } + + gfx.dispose(); + + ByteArrayOutputStream bos = new ByteArrayOutputStream(); + ImageIO.write(bi, "PNG", bos); + return bos.toByteArray(); + } +} Index: src/ooxml/testcases/org/apache/poi/poifs/crypt/dsig/TestSignatureInfo.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/ooxml/testcases/org/apache/poi/poifs/crypt/dsig/TestSignatureInfo.java (revision 1882098) +++ src/ooxml/testcases/org/apache/poi/poifs/crypt/dsig/TestSignatureInfo.java (date 1601506892882) @@ -98,8 +98,11 @@ import org.apache.poi.util.LocaleUtil; import org.apache.poi.util.POILogFactory; import org.apache.poi.util.POILogger; +import org.apache.poi.util.TempFile; import org.apache.poi.xssf.streaming.SXSSFWorkbook; import org.apache.poi.xssf.usermodel.XSSFWorkbook; +import org.apache.poi.xwpf.usermodel.XWPFDocument; +import org.apache.poi.xwpf.usermodel.XWPFSignatureLine; import org.apache.xmlbeans.SystemProperties; import org.apache.xmlbeans.XmlObject; import org.bouncycastle.asn1.DEROctetString; @@ -855,6 +858,53 @@ assertEquals(CanonicalizationMethod.INCLUSIVE, sic.getCanonicalizationMethod()); } + @Test + public void testSignatureImage() throws Exception { + File signDoc = TempFile.createTempFile("visual-signature",".docx"); + XWPFSignatureLine line = new XWPFSignatureLine(); + line.setSuggestedSigner("Jack Sparrow"); + line.setSuggestedSigner2("Captain"); + line.setSuggestedSignerEmail("jack.bl@ck.perl"); + + try (XWPFDocument doc = new XWPFDocument(); + FileOutputStream fos = new FileOutputStream(signDoc)) { + line.addSignatureLine(doc); + doc.write(fos); + } + + initKeyPair(); + try (OPCPackage pkg = OPCPackage.open(signDoc, PackageAccess.READ_WRITE)) { + SignatureConfig sic = new SignatureConfig(); + sic.setKey(keyPair.getPrivate()); + sic.setSigningCertificateChain(Collections.singletonList(x509)); + + byte[] plainSign = testdata.readFile("jack-sign.emf"); + String caption = line.getSuggestedSigner()+"\n"+line.getSuggestedSigner2()+"\n"+line.getSuggestedSignerEmail(); + byte[] signValid = SignatureImage.generateImage(caption, plainSign, null); + byte[] signInvalid = SignatureImage.generateImage(caption, plainSign, "Bungling!"); + + sic.setSignatureImage(line.getSetupId(), plainSign, signValid, signInvalid); + sic.setDigestAlgo(HashAlgorithm.sha1); + SignatureInfo si = new SignatureInfo(); + si.setOpcPackage(pkg); + si.setSignatureConfig(sic); + // hash > sha1 doesn't work in excel viewer ... + si.confirmSignature(); + } + + try (OPCPackage pkg = OPCPackage.open(signDoc, PackageAccess.READ)) { + XWPFDocument doc = new XWPFDocument(pkg); + XWPFSignatureLine line2 = new XWPFSignatureLine(); + line2.parseSignatureLine(doc); + + assertEquals(line.getSuggestedSigner(), line2.getSuggestedSigner()); + assertEquals(line.getSuggestedSigner2(), line2.getSuggestedSigner2()); + assertEquals(line.getSuggestedSignerEmail(), line2.getSuggestedSignerEmail()); + + pkg.revert(); + } + } + private SignatureConfig prepareConfig(String pfxInput) throws Exception { initKeyPair(pfxInput); Index: src/ooxml/java/org/apache/poi/xslf/model/TextBodyPropertyFetcher.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/ooxml/java/org/apache/poi/xslf/model/TextBodyPropertyFetcher.java (revision 1882098) +++ src/ooxml/java/org/apache/poi/xslf/model/TextBodyPropertyFetcher.java (date 1601408159641) @@ -19,6 +19,7 @@ package org.apache.poi.xslf.model; +import static org.apache.poi.ooxml.util.XPathHelper.selectProperty; import static org.apache.poi.xslf.model.ParagraphPropertyFetcher.DML_NS; import static org.apache.poi.xslf.model.ParagraphPropertyFetcher.PML_NS; @@ -37,7 +38,7 @@ public boolean fetch(XSLFShape shape) { CTTextBodyProperties props = null; try { - props = shape.selectProperty( + props = selectProperty(shape.getXmlObject(), CTTextBodyProperties.class, TextBodyPropertyFetcher::parse, TX_BODY, BODY_PR); return (props != null) && fetch(props); } catch (XmlException e) { Index: src/ooxml/java/org/apache/poi/ooxml/util/XPathHelper.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/ooxml/java/org/apache/poi/ooxml/util/XPathHelper.java (revision 1882098) +++ src/ooxml/java/org/apache/poi/ooxml/util/XPathHelper.java (date 1601506892898) @@ -17,14 +17,35 @@ package org.apache.poi.ooxml.util; -import org.apache.poi.util.POILogFactory; -import org.apache.poi.util.POILogger; +import java.util.Locale; import javax.xml.XMLConstants; +import javax.xml.namespace.QName; import javax.xml.xpath.XPathFactory; +import com.microsoft.schemas.compatibility.AlternateContentDocument; +import org.apache.poi.util.Internal; +import org.apache.poi.util.POILogFactory; +import org.apache.poi.util.POILogger; +import org.apache.poi.xslf.usermodel.XSLFShape; +import org.apache.xmlbeans.XmlCursor; +import org.apache.xmlbeans.XmlException; +import org.apache.xmlbeans.XmlObject; +import org.apache.xmlbeans.impl.values.XmlAnyTypeImpl; + public final class XPathHelper { - private static POILogger logger = POILogFactory.getLogger(XPathHelper.class); + private static final POILogger LOG = POILogFactory.getLogger(XPathHelper.class); + + private static final String OSGI_ERROR = + "Schemas (*.xsb) for can't be loaded - usually this happens when OSGI " + + "loading is used and the thread context classloader has no reference to " + + "the xmlbeans classes - please either verify if the .xsb is on the " + + "classpath or alternatively try to use the full ooxml-schemas-x.x.jar"; + + private static final String MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006"; + private static final String MAC_DML_NS = "http://schemas.microsoft.com/office/mac/drawingml/2008/main"; + private static final QName ALTERNATE_CONTENT_TAG = new QName(MC_NS, "AlternateContent"); + // AlternateContentDocument.AlternateContent.type.getName(); private XPathHelper() {} @@ -41,9 +62,165 @@ try { xpf.setFeature(feature, enabled); } catch (Exception e) { - logger.log(POILogger.WARN, "XPathFactory Feature unsupported", feature, e); + LOG.log(POILogger.WARN, "XPathFactory Feature unsupported", feature, e); } catch (AbstractMethodError ame) { - logger.log(POILogger.WARN, "Cannot set XPathFactory feature because outdated XML parser in classpath", feature, ame); + LOG.log(POILogger.WARN, "Cannot set XPathFactory feature because outdated XML parser in classpath", feature, ame); } } + + + + /** + * Internal code - API may change any time! + *

+ * The {@link #selectProperty(Class, String)} xquery method has some performance penalties, + * which can be workaround by using {@link XmlCursor}. This method also takes into account + * that {@code AlternateContent} tags can occur anywhere on the given path. + *

+ * It returns the first element found - the search order is: + *

    + *
  • searching for a direct child
  • + *
  • searching for a AlternateContent.Choice child
  • + *
  • searching for a AlternateContent.Fallback child
  • + *
+ * Currently POI OOXML is based on the first edition of the ECMA 376 schema, which doesn't + * allow AlternateContent tags to show up everywhere. The factory flag is + * a workaround to process files based on a later edition. But it comes with the drawback: + * any change on the returned XmlObject aren't saved back to the underlying document - + * so it's a non updatable clone. If factory is null, a XmlException is + * thrown if the AlternateContent is not allowed by the surrounding element or if the + * extracted object is of the generic type XmlAnyTypeImpl. + * + * @param resultClass the requested result class + * @param factory a factory parse method reference to allow reparsing of elements + * extracted from AlternateContent elements. Usually the enclosing XmlBeans type needs to be used + * to parse the stream + * @param path the elements path, each array must contain at least 1 QName, + * but can contain additional alternative tags + * @return the xml object at the path location, or null if not found + * + * @throws XmlException If factory is null, a XmlException is + * thrown if the AlternateContent is not allowed by the surrounding element or if the + * extracted object is of the generic type XmlAnyTypeImpl. + * + * @since POI 4.1.2 + */ + @SuppressWarnings("unchecked") + @Internal + public static T selectProperty(XmlObject startObject, Class resultClass, XSLFShape.ReparseFactory factory, QName[]... path) + throws XmlException { + XmlObject xo = startObject; + XmlCursor cur = xo.newCursor(); + XmlCursor innerCur = null; + try { + innerCur = selectProperty(cur, path, 0, factory != null, false); + if (innerCur == null) { + return null; + } + + // Pesky XmlBeans bug - see Bugzilla #49934 + // it never happens when using the full ooxml-schemas jar but may happen with the abridged poi-ooxml-schemas + xo = innerCur.getObject(); + if (xo instanceof XmlAnyTypeImpl) { + String errorTxt = OSGI_ERROR + .replace("", resultClass.getSimpleName()) + .replace("", resultClass.getSimpleName().toLowerCase(Locale.ROOT)+"*"); + if (factory == null) { + throw new XmlException(errorTxt); + } else { + xo = factory.parse(innerCur.newXMLStreamReader()); + } + } + + return (T)xo; + } finally { + cur.dispose(); + if (innerCur != null) { + innerCur.dispose(); + } + } + } + + private static XmlCursor selectProperty(final XmlCursor cur, final QName[][] path, final int offset, final boolean reparseAlternate, final boolean isAlternate) + throws XmlException { + // first try the direct children + for (QName qn : path[offset]) { + for (boolean found = cur.toChild(qn); found; found = cur.toNextSibling(qn)) { + if (offset == path.length-1) { + return cur; + } + cur.push(); + XmlCursor innerCur = selectProperty(cur, path, offset+1, reparseAlternate, false); + if (innerCur != null) { + return innerCur; + } + cur.pop(); + } + } + // if we were called inside an alternate content handling don't look for alternates again + if (isAlternate || !cur.toChild(ALTERNATE_CONTENT_TAG)) { + return null; + } + + // otherwise check first the choice then the fallback content + XmlObject xo = cur.getObject(); + AlternateContentDocument.AlternateContent alterCont; + if (xo instanceof AlternateContentDocument.AlternateContent) { + alterCont = (AlternateContentDocument.AlternateContent)xo; + } else { + // Pesky XmlBeans bug - see Bugzilla #49934 + // it never happens when using the full ooxml-schemas jar but may happen with the abridged poi-ooxml-schemas + if (!reparseAlternate) { + throw new XmlException(OSGI_ERROR + .replace("", "AlternateContent") + .replace("", "alternatecontentelement") + ); + } + try { + AlternateContentDocument acd = AlternateContentDocument.Factory.parse(cur.newXMLStreamReader()); + alterCont = acd.getAlternateContent(); + } catch (XmlException e) { + throw new XmlException("unable to parse AlternateContent element", e); + } + } + + final int choices = alterCont.sizeOfChoiceArray(); + for (int i=0; iUTF-8 =================================================================== --- src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFObjectShape.java (revision 1882098) +++ src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFObjectShape.java (date 1601408159601) @@ -30,6 +30,7 @@ import org.apache.poi.hpsf.ClassID; import org.apache.poi.ooxml.POIXMLDocumentPart.RelationPart; import org.apache.poi.ooxml.POIXMLException; +import org.apache.poi.ooxml.util.XPathHelper; import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.openxml4j.opc.OPCPackage; import org.apache.poi.openxml4j.opc.PackagePart; @@ -76,7 +77,7 @@ // select oleObj potentially under AlternateContent // usually the mc:Choice element will be selected first try { - _oleObject = selectProperty(CTOleObject.class, null, GRAPHIC, GRAPHIC_DATA, OLE_OBJ); + _oleObject = XPathHelper.selectProperty(getXmlObject(), CTOleObject.class, null, GRAPHIC, GRAPHIC_DATA, OLE_OBJ); } catch (XmlException e) { // ole objects should be also inside AlternateContent tags, even with ECMA 376 edition 1 throw new IllegalStateException(e); @@ -146,8 +147,8 @@ protected CTBlipFillProperties getBlipFill() { try { - CTPicture pic = selectProperty - (CTPicture.class, XSLFObjectShape::parse, GRAPHIC, GRAPHIC_DATA, OLE_OBJ, CT_PICTURE); + CTPicture pic = XPathHelper.selectProperty + (getXmlObject(), CTPicture.class, XSLFObjectShape::parse, GRAPHIC, GRAPHIC_DATA, OLE_OBJ, CT_PICTURE); return (pic != null) ? pic.getBlipFill() : null; } catch (XmlException e) { return null; Index: src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPictureShape.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPictureShape.java (revision 1882098) +++ src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPictureShape.java (date 1601408159617) @@ -35,6 +35,7 @@ import javax.xml.namespace.QName; import javax.xml.stream.XMLStreamReader; +import org.apache.poi.ooxml.util.XPathHelper; import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.openxml4j.opc.PackageRelationship; import org.apache.poi.sl.usermodel.PictureData; @@ -175,7 +176,7 @@ } try { - return selectProperty(CTBlipFillProperties.class, XSLFPictureShape::parse, BLIP_FILL); + return XPathHelper.selectProperty(getXmlObject(), CTBlipFillProperties.class, XSLFPictureShape::parse, BLIP_FILL); } catch (XmlException xe) { return null; } Index: src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPlaceholderDetails.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPlaceholderDetails.java (revision 1882098) +++ src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFPlaceholderDetails.java (date 1601408159629) @@ -17,6 +17,7 @@ package org.apache.poi.xslf.usermodel; +import static org.apache.poi.ooxml.util.XPathHelper.selectProperty; import static org.apache.poi.xslf.usermodel.XSLFShape.PML_NS; import java.util.function.Consumer; @@ -220,7 +221,7 @@ private CTApplicationNonVisualDrawingProps getNvProps() { try { - return shape.selectProperty(CTApplicationNonVisualDrawingProps.class, null, NV_CONTAINER, NV_PROPS); + return selectProperty(shape.getXmlObject(), CTApplicationNonVisualDrawingProps.class, null, NV_CONTAINER, NV_PROPS); } catch (XmlException e) { return null; } Index: src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java (revision 1882098) +++ src/ooxml/java/org/apache/poi/xslf/usermodel/XSLFShape.java (date 1601407356661) @@ -21,13 +21,11 @@ import java.awt.Graphics2D; import java.awt.geom.Rectangle2D; -import java.util.Locale; import javax.xml.namespace.QName; import javax.xml.stream.XMLStreamReader; -import com.microsoft.schemas.compatibility.AlternateContentDocument; -import com.microsoft.schemas.compatibility.AlternateContentDocument.AlternateContent; +import org.apache.poi.ooxml.util.XPathHelper; import org.apache.poi.openxml4j.opc.PackagePart; import org.apache.poi.sl.draw.DrawFactory; import org.apache.poi.sl.draw.DrawPaint; @@ -45,7 +43,6 @@ import org.apache.xmlbeans.XmlCursor; import org.apache.xmlbeans.XmlException; import org.apache.xmlbeans.XmlObject; -import org.apache.xmlbeans.impl.values.XmlAnyTypeImpl; import org.openxmlformats.schemas.drawingml.x2006.main.CTBlipFillProperties; import org.openxmlformats.schemas.drawingml.x2006.main.CTGradientFillProperties; import org.openxmlformats.schemas.drawingml.x2006.main.CTGroupShapeProperties; @@ -76,10 +73,6 @@ static final String DML_NS = "http://schemas.openxmlformats.org/drawingml/2006/main"; static final String PML_NS = "http://schemas.openxmlformats.org/presentationml/2006/main"; - private static final String MC_NS = "http://schemas.openxmlformats.org/markup-compatibility/2006"; - private static final String MAC_DML_NS = "http://schemas.microsoft.com/office/mac/drawingml/2008/main"; - - private static final QName ALTERNATE_CONTENT_TAG = new QName(MC_NS, "AlternateContent"); private static final QName[] NV_CONTAINER = { new QName(PML_NS, "nvSpPr"), @@ -93,12 +86,6 @@ new QName(PML_NS, "cNvPr") }; - private static final String OSGI_ERROR = - "Schemas (*.xsb) for can't be loaded - usually this happens when OSGI " + - "loading is used and the thread context classloader has no reference to " + - "the xmlbeans classes - please either verify if the .xsb is on the " + - "classpath or alternatively try to use the full ooxml-schemas-x.x.jar"; - private final XmlObject _shape; private final XSLFSheet _sheet; private XSLFShapeContainer _parent; @@ -239,7 +226,7 @@ protected CTNonVisualDrawingProps getCNvPr() { try { if (_nvPr == null) { - _nvPr = selectProperty(CTNonVisualDrawingProps.class, null, NV_CONTAINER, CNV_PROPS); + _nvPr = XPathHelper.selectProperty(getXmlObject(), CTNonVisualDrawingProps.class, null, NV_CONTAINER, CNV_PROPS); } return _nvPr; } catch (XmlException e) { @@ -322,160 +309,6 @@ return (resultClass.isInstance(rs[0])) ? (T)rs[0] : null; } - /** - * Internal code - API may change any time! - *

- * The {@link #selectProperty(Class, String)} xquery method has some performance penalties, - * which can be workaround by using {@link XmlCursor}. This method also takes into account - * that {@code AlternateContent} tags can occur anywhere on the given path. - *

- * It returns the first element found - the search order is: - *

    - *
  • searching for a direct child
  • - *
  • searching for a AlternateContent.Choice child
  • - *
  • searching for a AlternateContent.Fallback child
  • - *
- * Currently POI OOXML is based on the first edition of the ECMA 376 schema, which doesn't - * allow AlternateContent tags to show up everywhere. The factory flag is - * a workaround to process files based on a later edition. But it comes with the drawback: - * any change on the returned XmlObject aren't saved back to the underlying document - - * so it's a non updatable clone. If factory is null, a XmlException is - * thrown if the AlternateContent is not allowed by the surrounding element or if the - * extracted object is of the generic type XmlAnyTypeImpl. - * - * @param resultClass the requested result class - * @param factory a factory parse method reference to allow reparsing of elements - * extracted from AlternateContent elements. Usually the enclosing XmlBeans type needs to be used - * to parse the stream - * @param path the elements path, each array must contain at least 1 QName, - * but can contain additional alternative tags - * @return the xml object at the path location, or null if not found - * - * @throws XmlException If factory is null, a XmlException is - * thrown if the AlternateContent is not allowed by the surrounding element or if the - * extracted object is of the generic type XmlAnyTypeImpl. - * - * @since POI 4.1.2 - */ - @SuppressWarnings("unchecked") - @Internal - public T selectProperty(Class resultClass, ReparseFactory factory, QName[]... path) - throws XmlException { - XmlObject xo = getXmlObject(); - XmlCursor cur = xo.newCursor(); - XmlCursor innerCur = null; - try { - innerCur = selectProperty(cur, path, 0, factory != null, false); - if (innerCur == null) { - return null; - } - - // Pesky XmlBeans bug - see Bugzilla #49934 - // it never happens when using the full ooxml-schemas jar but may happen with the abridged poi-ooxml-schemas - xo = innerCur.getObject(); - if (xo instanceof XmlAnyTypeImpl) { - String errorTxt = OSGI_ERROR - .replace("", resultClass.getSimpleName()) - .replace("", resultClass.getSimpleName().toLowerCase(Locale.ROOT)+"*"); - if (factory == null) { - throw new XmlException(errorTxt); - } else { - xo = factory.parse(innerCur.newXMLStreamReader()); - } - } - - return (T)xo; - } finally { - cur.dispose(); - if (innerCur != null) { - innerCur.dispose(); - } - } - } - - private XmlCursor selectProperty(final XmlCursor cur, final QName[][] path, final int offset, final boolean reparseAlternate, final boolean isAlternate) - throws XmlException { - // first try the direct children - for (QName qn : path[offset]) { - if (cur.toChild(qn)) { - if (offset == path.length-1) { - return cur; - } - cur.push(); - XmlCursor innerCur = selectProperty(cur, path, offset+1, reparseAlternate, false); - if (innerCur != null) { - return innerCur; - } - cur.pop(); - } - } - // if we were called inside an alternate content handling don't look for alternates again - if (isAlternate || !cur.toChild(ALTERNATE_CONTENT_TAG)) { - return null; - } - - // otherwise check first the choice then the fallback content - XmlObject xo = cur.getObject(); - AlternateContent alterCont; - if (xo instanceof AlternateContent) { - alterCont = (AlternateContent)xo; - } else { - // Pesky XmlBeans bug - see Bugzilla #49934 - // it never happens when using the full ooxml-schemas jar but may happen with the abridged poi-ooxml-schemas - if (!reparseAlternate) { - throw new XmlException(OSGI_ERROR - .replace("", "AlternateContent") - .replace("", "alternatecontentelement") - ); - } - try { - AlternateContentDocument acd = AlternateContentDocument.Factory.parse(cur.newXMLStreamReader()); - alterCont = acd.getAlternateContent(); - } catch (XmlException e) { - throw new XmlException("unable to parse AlternateContent element", e); - } - } - - final int choices = alterCont.sizeOfChoiceArray(); - for (int i=0; i * Index: src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSignatureLine.java IDEA additional info: Subsystem: com.intellij.openapi.diff.impl.patch.CharsetEP <+>UTF-8 =================================================================== --- src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSignatureLine.java (date 1601506892846) +++ src/ooxml/java/org/apache/poi/xwpf/usermodel/XWPFSignatureLine.java (date 1601506892846) @@ -0,0 +1,179 @@ +/* ==================================================================== + 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.poi.xwpf.usermodel; + +import java.io.IOException; +import java.util.UUID; + +import javax.xml.namespace.QName; + +import com.microsoft.schemas.office.office.CTSignatureLine; +import com.microsoft.schemas.office.office.STTrueFalse; +import com.microsoft.schemas.vml.CTGroup; +import com.microsoft.schemas.vml.CTImageData; +import com.microsoft.schemas.vml.CTShape; +import com.microsoft.schemas.vml.STExt; +import org.apache.poi.hpsf.ClassID; +import org.apache.poi.ooxml.POIXMLException; +import org.apache.poi.ooxml.util.XPathHelper; +import org.apache.poi.openxml4j.exceptions.InvalidFormatException; +import org.apache.poi.poifs.crypt.dsig.SignatureImage; +import org.apache.xmlbeans.XmlCursor; +import org.apache.xmlbeans.XmlException; +import org.openxmlformats.schemas.wordprocessingml.x2006.main.CTPicture; + +public class XWPFSignatureLine { + static final String NS_OOXML_WP_MAIN = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"; + private static final String MS_VML_URN = "urn:schemas-microsoft-com:vml"; + private static final String MS_OFFICE_URN = "urn:schemas-microsoft-com:office:office"; + + private ClassID setupId; + private Boolean allowComments; + private String signingInstructions = "Before signing the document, verify that the content you are signing is correct."; + private String suggestedSigner; + private String suggestedSigner2; + private String suggestedSignerEmail; + + public ClassID getSetupId() { + return setupId; + } + + public void setSetupId(ClassID setupId) { + this.setupId = setupId; + } + + public Boolean getAllowComments() { + return allowComments; + } + + public void setAllowComments(Boolean allowComments) { + this.allowComments = allowComments; + } + + public String getSigningInstructions() { + return signingInstructions; + } + + public void setSigningInstructions(String signingInstructions) { + this.signingInstructions = signingInstructions; + } + + public String getSuggestedSigner() { + return suggestedSigner; + } + + public void setSuggestedSigner(String suggestedSigner) { + this.suggestedSigner = suggestedSigner; + } + + public String getSuggestedSigner2() { + return suggestedSigner2; + } + + public void setSuggestedSigner2(String suggestedSigner2) { + this.suggestedSigner2 = suggestedSigner2; + } + + public String getSuggestedSignerEmail() { + return suggestedSignerEmail; + } + + public void setSuggestedSignerEmail(String suggestedSignerEmail) { + this.suggestedSignerEmail = suggestedSignerEmail; + } + + public void parseSignatureLine(XWPFDocument doc) throws XmlException { + CTSignatureLine line = XPathHelper.selectProperty(doc.getDocument(), CTSignatureLine.class, null, + new QName[]{new QName(NS_OOXML_WP_MAIN, "body")}, + new QName[]{new QName(NS_OOXML_WP_MAIN, "p")}, + new QName[]{new QName(NS_OOXML_WP_MAIN, "r")}, + new QName[]{new QName(NS_OOXML_WP_MAIN, "pict")}, + new QName[]{new QName(MS_VML_URN, "shape")}, + new QName[]{new QName(MS_OFFICE_URN, "signatureline")}); + if (line == null) { + return; + } + setupId = new ClassID(line.getId()); + allowComments = line.isSetAllowcomments() ? STTrueFalse.TRUE.equals(line.getAllowcomments()) : null; + suggestedSigner = line.getSuggestedsigner(); + suggestedSigner2 = line.getSuggestedsigner2(); + suggestedSignerEmail = line.getSuggestedsigneremail(); + XmlCursor cur = line.newCursor(); + try { + // the signinginstructions are actually qualified, but our schema version is too old + signingInstructions = cur.getAttributeText(new QName(MS_OFFICE_URN, "signinginstructions")); + } finally { + cur.dispose(); + } + } + + public void addSignatureLine(XWPFDocument document) { + String caption = suggestedSigner+"\n"+suggestedSigner2+"\n"+suggestedSignerEmail; + byte[] inputImage; + try { + inputImage = SignatureImage.generateImage(caption, null, null); + XWPFRun r = document.createParagraph().createRun(); + CTPicture pict = r.getCTR().addNewPict(); + + CTGroup grp = CTGroup.Factory.newInstance(); + CTShape shape = grp.addNewShape(); + shape.setAlt("Microsoft Office Signature Line..."); + shape.setStyle("width:191.95pt;height:96.05pt"); + shape.setType("rect"); + + String relationId = document.addPictureData(inputImage, Document.PICTURE_TYPE_PNG); + CTImageData imgData = shape.addNewImagedata(); + imgData.setId2(relationId); + imgData.setTitle(""); + + CTSignatureLine xsl = shape.addNewSignatureline(); + if (suggestedSigner != null) { + xsl.setSuggestedsigner(suggestedSigner); + } + if (suggestedSigner2 != null) { + xsl.setSuggestedsigner2(suggestedSigner2); + } + if (suggestedSignerEmail != null) { + xsl.setSuggestedsigneremail(suggestedSignerEmail); + } + if (setupId == null) { + setupId = new ClassID("{"+UUID.randomUUID().toString()+"}"); + } + xsl.setId(setupId.toString()); + xsl.setAllowcomments(STTrueFalse.T); + xsl.setIssignatureline(STTrueFalse.T); + xsl.setProvid("{00000000-0000-0000-0000-000000000000}"); + xsl.setExt(STExt.EDIT); + xsl.setSigninginstructionsset(STTrueFalse.T); + XmlCursor cur = xsl.newCursor(); + cur.setAttributeText(new QName(MS_OFFICE_URN, "signinginstructions"), signingInstructions); + cur.dispose(); + + XmlCursor thisC = pict.newCursor(); + thisC.toEndToken(); + XmlCursor otherC = grp.newCursor(); + otherC.copyXmlContents(thisC); + otherC.dispose(); + thisC.dispose(); + } catch (IOException | InvalidFormatException e) { + // shouldn't happen ... + throw new POIXMLException("Can't generate signature line image", e); + } + } +} diff --git test-data/xmldsign/jack-sign.emf test-data/xmldsign/jack-sign.emf new file mode 100644 index 0000000000000000000000000000000000000000..dafd361fb3adf5279fcde664dc187e0dff33ebb1 GIT binary patch literal 29868 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 R_j{FvJ(&y0{NwMA{vWv@NHG8a