/*
* eID Applet Project.
* Copyright (C) 2009 FedICT.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License version
* 3.0 as published by the Free Software Foundation.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, see
* http://www.gnu.org/licenses/.
*/
package be.fedict.eid.applet.service.signer.odf;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpression;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.w3c.dom.Document;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
/**
* Helper class to remove some code duplication
*
* @author Bart Hanssens
*/
public class ODFUtil {
public static String MANIFEST_FILE = "META-INF/manifest.xml";
public static String MIMETYPE_FILE = "mimetype";
public static String MIMETYPE_START = "application/vnd.oasis.opendocument";
public static String SIGNATURE_FILE = "META-INF/documentsignatures.xml";
public static String SIGNATURE_NS = "urn:oasis:names:tc:opendocument:xmlns:digitalsignature:1.0";
public static String SIGNATURE_ELEMENT = "document-signatures";
private static final Log LOG = LogFactory.getLog(ODFUtil.class);
/**
* Load an XML file from ODF package as a DOM Document
*
* @param documentInputStream
* @return
* @throws ParserConfigurationException
* @throws SAXException
* @throws IOException
*/
public static Document loadDocument(InputStream documentInputStream)
throws ParserConfigurationException, SAXException, IOException {
DocumentBuilder documentBuilder = getNewDocumentBuilder();
Document document = documentBuilder.parse(documentInputStream);
return document;
}
/**
* Return a new DOM Document
*
* @return DOM Document
* @throws ParserConfigurationException
*/
public static Document getNewDocument() throws ParserConfigurationException {
return getNewDocumentBuilder().newDocument();
}
/**
* Return a new DOM Document Builder
*
* @return DOM Document Builder
* @throws ParserConfigurationException
*/
public static DocumentBuilder getNewDocumentBuilder() throws ParserConfigurationException {
LOG.debug("new DOM document builder");
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
documentBuilderFactory.setNamespaceAware(true);
return documentBuilderFactory.newDocumentBuilder();
}
/**
* Read the zipped data in the ODF package and return the inputstream for a
* given file / zip entry
*
* @param inputStream
* @param uri
* @return inputstream for the file / zip entry
* @throws IOException
*/
public static InputStream findDataInputStream(InputStream inputStream, String uri) throws IOException {
ZipInputStream zipInputStream = new ZipInputStream(inputStream);
ZipEntry zipEntry;
while (null != (zipEntry = zipInputStream.getNextEntry())) {
if (zipEntry.getName().equals(uri)) {
return zipInputStream;
}
}
return null;
}
/**
* Checks if a file / zip entry is a content file
*
* @param zipEntry
* @return true if zip entry is a content file
*/
public static boolean isContentFile(ZipEntry zipEntry) {
/*
* should be enough for most simple ODF files and ODF embedded into
* another ODF file (e.g. chart in spreadsheet)
*/
return zipEntry.getName().endsWith("content.xml");
}
/**
* Checks if a file / zip entry is a signature file
*
* @param zipEntry
* @return true if zip entry is a signature file
*/
public static boolean isSignatureFile(ZipEntry zipEntry) {
/*
* for now, only check for document signatures, not the other macro
* signatures or application specific signatures
*/
return zipEntry.getName().equals(SIGNATURE_FILE);
}
/**
* Check if a file / zip entry is to be signed
*
* @param zipEntry
* @return true if zip entry is to be signed
*/
public static boolean isToBeSigned(ZipEntry zipEntry) {
String name = zipEntry.getName();
/* OOo 3.0/3.1 bug: don't sign mimetype stream nor the manifest */
/*
* if (zipEntry.isDirectory() || name.equals(MIMETYPE_FILE) ||
* name.equals(MANIFEST_FILE) ||
*/
/* Corrected in OOo 3.2 */
if (zipEntry.isDirectory() || name.equals(SIGNATURE_FILE)) {
return false;
}
return true;
}
/**
* Get a list of all the files / zip entries in an ODF package
*
* @param odfInputStream
* @return
* @throws IOException
*/
public static List getZipEntriesAsList(InputStream odfInputStream) throws IOException {
ArrayList list = new ArrayList();
ZipInputStream odfZipInputStream = new ZipInputStream(odfInputStream);
ZipEntry zipEntry;
while (null != (zipEntry = odfZipInputStream.getNextEntry())) {
list.add(zipEntry.getName());
}
return list;
}
/**
* Check if an ODF package is self-contained, i.e. content files don't have
* OLE objects linked to external files
*
* @param odfUrl
* @return
* @throws IOException
* @throws ParserConfigurationException
* @throws SAXException
* @throws XPathExpressionException
*/
public static boolean isSelfContained(URL odfUrl)
throws IOException, ParserConfigurationException, SAXException, XPathExpressionException {
InputStream odfInputStream = odfUrl.openStream();
List zipEntries = getZipEntriesAsList(odfInputStream);
odfInputStream = odfUrl.openStream();
ZipInputStream odfZipInputStream = new ZipInputStream(odfInputStream);
ZipEntry zipEntry;
XPathFactory factory = XPathFactory.newInstance();
/* Maybe a bit overkill, but implementations can use other prefixes */
ODFNamespaceContext namespaceContext = new ODFNamespaceContext();
XPath xpath = factory.newXPath();
xpath.setNamespaceContext(namespaceContext);
XPathExpression expression = xpath.compile("//draw:object/@xlink:href|" + "//draw:object-ole/@xlink:href|"
+ "//draw:image/@xlink:href|" + "//draw:floating-frame/@xlink:href");
while (null != (zipEntry = odfZipInputStream.getNextEntry())) {
if (isContentFile(zipEntry)) {
/* TODO: pure SAX is probably more memory-efficient */
Document content = ODFUtil.loadDocument(odfZipInputStream);
NodeList nodes = (NodeList) expression.evaluate(content, XPathConstants.NODESET);
return checkNodes(nodes, zipEntries);
}
}
return true;
}
public static boolean checkNodes(NodeList nodes, List zipEntries) {
for (int i = 0; i < nodes.getLength(); i++) {
String url = nodes.item(i).getNodeValue();
if ("".equals(url)) {
LOG.debug("Skip empty xlink:href");
continue;
}
if (url.startsWith("./")) {
url = url.substring(2);
}
/* check if inside package or not */
if (!zipEntries.contains(url)) {
LOG.debug("Not self-contained: " + url + " outside package");
return false;
}
}
return true;
}
}