OdfPackage.java
/**
* **********************************************************************
*
* <p>DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER
*
* <p>Copyright 2008, 2010 Oracle and/or its affiliates. All rights reserved.
*
* <p>Use is subject to license terms.
*
* <p>Licensed 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. You can also obtain a copy of the License at
* http://odftoolkit.org/docs/license.txt
*
* <p>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.
*
* <p>See the License for the specific language governing permissions and limitations under the
* License.
*
* <p>**********************************************************************
*/
package org.odftoolkit.odfdom.pkg;
import static org.odftoolkit.odfdom.pkg.OdfPackageDocument.ROOT_DOCUMENT_PATH;
import java.io.BufferedInputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.lang.reflect.Constructor;
import java.net.URI;
import java.security.InvalidKeyException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.TreeSet;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import java.util.zip.CRC32;
import java.util.zip.Deflater;
import java.util.zip.Inflater;
import javax.crypto.Cipher;
import javax.crypto.Mac;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.URIResolver;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.xerces.dom.DOMXSImplementationSourceImpl;
import org.odftoolkit.odfdom.doc.OdfDocument;
import org.odftoolkit.odfdom.doc.OdfDocument.OdfMediaType;
import org.odftoolkit.odfdom.pkg.manifest.AlgorithmElement;
import org.odftoolkit.odfdom.pkg.manifest.EncryptionDataElement;
import org.odftoolkit.odfdom.pkg.manifest.FileEntryElement;
import org.odftoolkit.odfdom.pkg.manifest.KeyDerivationElement;
import org.odftoolkit.odfdom.pkg.manifest.ManifestElement;
import org.odftoolkit.odfdom.pkg.manifest.OdfFileEntry;
import org.odftoolkit.odfdom.pkg.manifest.OdfManifestDom;
import org.odftoolkit.odfdom.pkg.manifest.StartKeyGenerationElement;
import org.odftoolkit.odfdom.pkg.rdfa.Util;
import org.odftoolkit.odfdom.type.Base64Binary;
import org.w3c.dom.Document;
import org.w3c.dom.ls.DOMImplementationLS;
import org.w3c.dom.ls.LSOutput;
import org.w3c.dom.ls.LSSerializer;
import org.xml.sax.EntityResolver;
import org.xml.sax.ErrorHandler;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;
/**
* OdfPackage represents the package view to an OpenDocument document. The OdfPackage will be
* created from an ODF document and represents a copy of the loaded document, where files can be
* inserted and deleted. The changes take effect, when the OdfPackage is being made persisted by
* save().
*/
public class OdfPackage implements Closeable {
// Static parts of file references
private static final String DOUBLE_DOT = "..";
private static final String DOT = ".";
private static final String SLASH = "/";
private static final String COLON = ":";
private static final String ENCODED_APOSTROPHE = "'";
private static final String ENCODED_QUOTATION = """;
private static final String EMPTY_STRING = "";
private static final String XML_MEDIA_TYPE = "text/xml";
// Search patterns to be used in RegEx expressions
private static final Pattern BACK_SLASH_PATTERN = Pattern.compile("\\\\");
private static final Pattern DOUBLE_SLASH_PATTERN = Pattern.compile("//");
private static final Pattern QUOTATION_PATTERN = Pattern.compile("\"");
private static final Pattern APOSTROPHE_PATTERN = Pattern.compile("'");
private static final Pattern CONTROL_CHAR_PATTERN = Pattern.compile("\\p{Cntrl}");
private static final Set<String> COMPRESSED_FILETYPES;
private static final byte[] HREF_PATTERN = {
'x', 'l', 'i', 'n', 'k', ':', 'h', 'r', 'e', 'f', '=', '"'
}; // xlink:href="
// some well known streams inside ODF packages
private String mMediaType;
private String mBaseURI;
private ZipHelper mZipFile;
private Resolver mResolver;
private Map<String, ZipArchiveEntry> mZipEntries;
private HashMap<String, ZipArchiveEntry> mOriginalZipEntries;
private Map<String, OdfFileEntry> mManifestEntries;
// All opened documents from the same package are cached (including the root document)
private Map<String, OdfPackageDocument> mPkgDocuments;
// counter for ids that are not allowed to be saved (otherwise it is not guaranteed that this id
// is unique)
private int mTransientMarkupId = 0;
// Three different incarnations of a package file/data
// save() will check 1) mPkgDoms, 2) if not check mMemoryFileCache
private HashMap<String, Document> mPkgDoms;
private HashMap<String, byte[]> mMemoryFileCache;
private Map<String, Object> mConfiguration = new HashMap<String, Object>();
private ErrorHandler mErrorHandler;
private String mManifestVersion;
private OdfManifestDom mManifestDom;
private String mOldPwd;
private String mNewPwd;
/* Commonly used files within the ODF Package */
public enum OdfFile {
/**
* The image directory is not defined by the OpenDocument standard, nevertheless the most spread
* ODF application OpenOffice.org is using the directory named "Pictures".
*/
IMAGE_DIRECTORY("Pictures"),
/**
* The "META-INF/manifest.xml" file is defined by the ODF 1.2 part 3 Package specification. This
* manifest is the 'content table' of the ODF package and describes the file entries of the ZIP
* including directories, but should not contain empty directories.
*/
MANIFEST("META-INF/manifest.xml"),
/**
* The "mime type" file is defined by the ODF 1.2 part 3 Package specification. It contains the
* media type string of the root document and must be the first file in the ZIP and must not be
* compressed.
*/
MEDIA_TYPE("mimetype");
private final String internalPath;
OdfFile(String internalPath) {
this.internalPath = internalPath;
}
public String getPath() {
return internalPath;
}
}
static {
HashSet<String> compressedFileTypes = new HashSet<String>();
String[] typelist =
new String[] {
"jpg", "gif", "png", "zip", "rar", "jpeg", "mpe", "mpg", "mpeg", "mpeg4", "mp4", "7z",
"ari", "arj", "jar", "gz", "tar", "war", "mov", "avi"
};
compressedFileTypes.addAll(Arrays.asList(typelist));
COMPRESSED_FILETYPES = Collections.unmodifiableSet(compressedFileTypes);
}
/** Creates the ODFPackage as an empty Package. */
private OdfPackage() {
mMediaType = null;
mResolver = null;
mPkgDocuments = new HashMap<String, OdfPackageDocument>();
mPkgDoms = new HashMap<String, Document>();
mTransientMarkupId = 0;
mMemoryFileCache = new HashMap<String, byte[]>();
mManifestEntries = new HashMap<String, OdfFileEntry>();
// specify whether validation should be enabled and what SAX
// ErrorHandler should be used.
if (mErrorHandler == null) {
String errorHandlerProperty = System.getProperty("org.odftoolkit.odfdom.validation");
if (errorHandlerProperty != null) {
if (errorHandlerProperty.equalsIgnoreCase("true")) {
mErrorHandler = new DefaultErrorHandler();
Logger.getLogger(OdfPackage.class.getName())
.config("Activated validation with default ErrorHandler!");
} else if (!errorHandlerProperty.equalsIgnoreCase("false")) {
try {
Class<?> cl = Class.forName(errorHandlerProperty);
Constructor<?> ctor = cl.getDeclaredConstructor(new Class[] {});
mErrorHandler = (ErrorHandler) ctor.newInstance();
Logger.getLogger(OdfPackage.class.getName())
.log(
Level.CONFIG,
"Activated validation with ErrorHandler:''{0}''!",
errorHandlerProperty);
} catch (Exception ex) {
Logger.getLogger(OdfPackage.class.getName())
.log(
Level.SEVERE,
"Could not initiate validation with the given ErrorHandler: '"
+ errorHandlerProperty
+ "'",
ex);
}
}
}
}
}
// is called if a low memory notification was received... then its tried to free as much memory as
// possible
public void freeMemory() {
mZipFile = null;
mResolver = null;
mZipEntries = null;
mOriginalZipEntries = null;
mManifestEntries = null;
mPkgDocuments = null;
mPkgDoms = null;
mMemoryFileCache = null;
mConfiguration = null;
mErrorHandler = null;
}
/**
* Creates an OdfPackage from the OpenDocument provided by a File.
*
* <p>OdfPackage relies on the file being available for read access over the whole life-cycle of
* OdfPackage.
*
* @param pkgFile - a file representing the ODF document
* @throws SAXException if there's an XML- or validation-related error while loading the package
* @throws IOException if there's an I/O error while loading the package
*/
private OdfPackage(File pkgFile) throws SAXException, IOException {
this(pkgFile, getBaseURLFromFile(pkgFile), null, null);
}
/**
* Creates an OdfPackage from the OpenDocument provided by a File.
*
* <p>OdfPackage relies on the file being available for read access over the whole life-cycle of
* OdfPackage.
*
* @param pkgFile - a file representing the ODF document
* @param baseURI defining the base URI of ODF package.
* @param password defining the password of ODF package.
* @param errorHandler - SAX ErrorHandler used for ODF validation
* @see #getErrorHandler
* @throws SAXException if there's an XML- or validation-related error while loading the package
* @throws IOException if there's an I/O error while loading the package
* @see #getErrorHandler*
*/
private OdfPackage(File pkgFile, String baseURI, String password, ErrorHandler errorHandler)
throws SAXException, IOException {
this();
mBaseURI = getBaseURLFromFile(pkgFile);
mErrorHandler = errorHandler;
mOldPwd = password;
mNewPwd = mOldPwd;
mBaseURI = baseURI;
InputStream packageStream = new FileInputStream(pkgFile);
try {
initializeZip(packageStream);
} finally {
close(packageStream);
}
}
/**
* Creates an OdfPackage from the OpenDocument provided by a InputStream.
*
* <p>Since an InputStream does not provide the arbitrary (non sequential) read access needed by
* OdfPackage, the InputStream is cached. This usually takes more time compared to the other
* constructors.
*
* @param packageStream - an inputStream representing the ODF package
* @param baseURI defining the base URI of ODF package.
* @param password defining the password of ODF package.
* @param errorHandler - SAX ErrorHandler used for ODF validation
* @see #getErrorHandler
* @throws IOException if there's an I/O error while loading the package
* @throws SAXException if there's an XML- or validation-related error while loading the package
* @see #getErrorHandler*
*/
private OdfPackage(
InputStream packageStream,
String baseURI,
String password,
ErrorHandler errorHandler,
Map<String, Object> configuration)
throws SAXException, IOException {
this(); // calling private constructor
mErrorHandler = errorHandler;
mBaseURI = baseURI;
mOldPwd = password;
mNewPwd = mOldPwd;
mConfiguration = configuration;
initializeZip(packageStream);
}
/**
* Creates an OdfPackage from the OpenDocument provided by a InputStream.
*
* <p>Since an InputStream does not provide the arbitrary (non sequential) read access needed by
* OdfPackage, the InputStream is cached. This usually takes more time compared to the other
* constructors.
*
* @param packageStream - an inputStream representing the ODF package
* @param baseURI defining the base URI of ODF package.
* @param errorHandler - SAX ErrorHandler used for ODF validation
* @throws SAXException if there's an XML- or validation-related error while loading the package
* @throws IOException if there's an I/O error while loading the package
* @see #getErrorHandler
*/
private OdfPackage(InputStream packageStream, String baseURI, ErrorHandler errorHandler)
throws SAXException, IOException {
this(); // calling private constructor
if (errorHandler != null) {
mErrorHandler = errorHandler;
}
mBaseURI = baseURI;
initializeZip(packageStream);
}
/**
* Loads an OdfPackage from the given documentURL.
*
* <p>OdfPackage relies on the file being available for read access over the whole life-cycle of
* OdfPackage.
*
* @param path - the documentURL to the ODF package
* @return the OpenDocument document represented as an OdfPackage
* @throws SAXException if there's an XML- or validation-related error while loading the package
* @throws IOException if there's an I/O error while loading the package
*/
public static OdfPackage loadPackage(String path) throws SAXException, IOException {
File pkgFile = new File(path);
return new OdfPackage(pkgFile, getBaseURLFromFile(pkgFile), null, null);
}
/**
* Loads an OdfPackage from the OpenDocument provided by a File.
*
* <p>OdfPackage relies on the file being available for read access over the whole life-cycle of
* OdfPackage.
*
* @param pkgFile - the ODF Package
* @return the OpenDocument document represented as an OdfPackage
* @throws SAXException if there's an XML- or validation-related error while loading the package
* @throws IOException if there's an I/O error while loading the package
*/
public static OdfPackage loadPackage(File pkgFile) throws SAXException, IOException {
return new OdfPackage(pkgFile, getBaseURLFromFile(pkgFile), null, null);
}
/**
* Creates an OdfPackage from the given InputStream.
*
* <p>Since an InputStream does not provide the arbitrary (non sequential) read access needed by
* OdfPackage, the InputStream is cached. This usually takes more time compared to the other
* loadPackage methods.
*
* @param packageStream - an inputStream representing the ODF package
* @return the OpenDocument document represented as an OdfPackage
* @throws SAXException if there's an XML- or validation-related error while loading the package
* @throws IOException if there's an I/O error while loading the package
*/
public static OdfPackage loadPackage(InputStream packageStream) throws SAXException, IOException {
return new OdfPackage(packageStream, null, null, null, null);
}
/**
* Creates an OdfPackage from the given InputStream.
*
* <p>Since an InputStream does not provide the arbitrary (non sequential) read access needed by
* OdfPackage, the InputStream is cached. This usually takes more time compared to the other
* loadPackage methods.
*
* @param packageStream - an inputStream representing the ODF package
* @param configuration - key/value pairs of user given run-time settings (configuration) For
* instance, the maximum size of tables.
* @return the OpenDocument document represented as an OdfPackage
* @throws java.lang.Exception - if the package could not be loaded
*/
public static OdfPackage loadPackage(InputStream packageStream, Map<String, Object> configuration)
throws Exception {
return new OdfPackage(packageStream, null, null, null, configuration);
}
/**
* Creates an OdfPackage from the given InputStream.
*
* <p>OdfPackage relies on the file being available for read access over the whole life-cycle of
* OdfPackage.
*
* @param packageStream - an inputStream representing the ODF package
* @param baseURI allows to explicitly set the base URI from the document, As the URL can not be
* derived from a stream. In addition it is possible to set the baseURI to any arbitrary URI,
* e.g. an URN. One usage of the baseURI to describe the source of validation exception thrown
* by the ErrorHandler.
* @param errorHandler - SAX ErrorHandler used for ODF validation
* @throws SAXException if there's an XML- or validation-related error while loading the package
* @throws IOException if there's an I/O error while loading the package
* @see #getErrorHandler
*/
public static OdfPackage loadPackage(
InputStream packageStream, String baseURI, ErrorHandler errorHandler)
throws SAXException, IOException {
return new OdfPackage(packageStream, baseURI, null, errorHandler, null);
}
/**
* Loads an OdfPackage from the given File.
*
* <p>OdfPackage relies on the file being available for read access over the whole life-cycle of
* OdfPackage.
*
* @param pkgFile - the ODF Package. A baseURL is being generated based on its location.
* @param errorHandler - SAX ErrorHandler used for ODF validation.
* @throws SAXException if there's an XML- or validation-related error while loading the package
* @throws IOException if there's an I/O error while loading the package
* @see #getErrorHandler
*/
public static OdfPackage loadPackage(File pkgFile, ErrorHandler errorHandler)
throws SAXException, IOException {
return new OdfPackage(pkgFile, getBaseURLFromFile(pkgFile), null, errorHandler);
}
/**
* Run-time configuration such as special logging or maximum table size to create operations from
* are stored in this map.
*
* @return key/value pairs of user given run-time settings (configuration)
*/
public Map<String, Object> getRunTimeConfiguration() {
return mConfiguration;
}
/**
* Loads an OdfPackage from the given File.
*
* <p>OdfPackage relies on the file being available for read access over the whole life-cycle of
* OdfPackage.
*
* @param pkgFile - the ODF Package. A baseURL is being generated based on its location.
* @param password - the ODF Package password.
* @throws SAXException if there's an XML- or validation-related error while loading the package
* @throws IOException if there's an I/O error while loading the package
* @see #getErrorHandler
*/
public static OdfPackage loadPackage(File pkgFile, String password)
throws SAXException, IOException {
return OdfPackage.loadPackage(pkgFile, password, null);
}
/**
* Loads an OdfPackage from the given File.
*
* <p>OdfPackage relies on the file being available for read access over the whole life-cycle of
* OdfPackage.
*
* @param pkgFile - the ODF Package. A baseURL is being generated based on its location.
* @param password - the ODF Package password.
* @param errorHandler - SAX ErrorHandler used for ODF validation.
* @throws SAXException if there's an XML- or validation-related error while loading the package
* @throws IOException if there's an I/O error while loading the package
* @see #getErrorHandler
*/
public static OdfPackage loadPackage(File pkgFile, String password, ErrorHandler errorHandler)
throws SAXException, IOException {
return new OdfPackage(pkgFile, getBaseURLFromFile(pkgFile), password, errorHandler);
}
// Initialize using memory
private void initializeZip(InputStream odfStream) throws SAXException, IOException {
ByteArrayOutputStream tempBuf = new ByteArrayOutputStream();
StreamHelper.transformStream(odfStream, tempBuf);
byte[] mTempByteBuf = tempBuf.toByteArray();
tempBuf.close();
if (mTempByteBuf.length < 3) {
OdfValidationException ve =
new OdfValidationException(OdfPackageConstraint.PACKAGE_IS_NO_ZIP, getBaseURI());
if (mErrorHandler != null) {
mErrorHandler.fatalError(ve);
}
throw new IllegalArgumentException(ve);
}
mZipFile = new ZipHelper(this, mTempByteBuf);
readZip();
}
// // Initialize using ZipFile
// private void initializeZip(File pkgFile) throws Exception {
// try {
// mZipFile = new ZipHelper(this, new ZipFile(pkgFile));
// } catch (ZipException ze) {
// OdfValidationException ve = new
// OdfValidationException(OdfPackageConstraint.PACKAGE_IS_NO_ZIP,
// getBaseURI());
// if (mErrorHandler != null) {
// mErrorHandler.fatalError(ve);
// }
// throw new IllegalArgumentException(ve);
// }
// readZip();
// }
private void readZip() throws SAXException, IOException {
mZipEntries = new HashMap<String, ZipArchiveEntry>();
String firstEntryName = mZipFile.entriesToMap(mZipEntries);
if (mZipEntries.isEmpty()) {
OdfValidationException ve =
new OdfValidationException(OdfPackageConstraint.PACKAGE_IS_NO_ZIP, getBaseURI());
if (mErrorHandler != null) {
mErrorHandler.fatalError(ve);
}
throw new IllegalArgumentException(ve);
} else {
// initialize the files of the package (fileEnties of Manifest)
parseManifest();
// initialize the package media type
initializeMediaType(firstEntryName);
// ToDo: Remove all META-INF/* files from the fileEntries of
// Manifest
mOriginalZipEntries = new HashMap<String, ZipArchiveEntry>();
mOriginalZipEntries.putAll(mZipEntries);
mZipEntries.remove(OdfPackage.OdfFile.MEDIA_TYPE.getPath());
mZipEntries.remove(OdfPackage.OdfFile.MANIFEST.getPath());
mZipEntries.remove("META-INF/");
if (mErrorHandler != null) {
validateManifest();
}
Iterator<String> zipPaths = mZipEntries.keySet().iterator();
while (zipPaths.hasNext()) {
String internalPath = zipPaths.next();
// every resource aside the /META-INF/manifest.xml (and
// META-INF/ directory)
// and "mimetype" will be added as fileEntry
if (!internalPath.equals(OdfPackage.OdfFile.MANIFEST.getPath())
&& !internalPath.equals("META-INF/")
&& !internalPath.equals(OdfPackage.OdfFile.MEDIA_TYPE.getPath())) {
// aside "mediatype" and "META-INF/manifest"
// add manifest entry as to be described by a
// <manifest:file-entry>
ensureFileEntryExistence(internalPath);
}
}
}
}
/**
* Validates if all file entries exist in the ZIP and vice versa
*
* @throws SAXException
*/
private void validateManifest() throws SAXException {
Set<String> zipPaths = mZipEntries.keySet();
Set<String> manifestPaths = mManifestEntries.keySet();
Set<String> sharedPaths = new HashSet<String>(zipPaths);
sharedPaths.retainAll(manifestPaths);
if (sharedPaths.size() < zipPaths.size()) {
Set<String> zipPathSuperset = new HashSet<String>(mZipEntries.keySet());
zipPathSuperset.removeAll(sharedPaths);
Set<String> sortedSet = new TreeSet<String>(zipPathSuperset);
Iterator<String> iter = sortedSet.iterator();
String documentURL = getBaseURI();
String internalPath;
while (iter.hasNext()) {
internalPath = (String) iter.next();
if (!internalPath.endsWith(SLASH)
&& // not for directories!
// The “META-INF/manifest.xml” file need not contain <manifest:file-entry> elements 4.3
// whose manifest:full-path attribute 4.8.4 references files whose relative path start
// with "META-INF/".
!internalPath.startsWith("META-INF/")) {
logValidationError(
OdfPackageConstraint.MANIFEST_DOES_NOT_LIST_FILE, documentURL, internalPath);
}
}
}
if (sharedPaths.size() < manifestPaths.size()) {
Set<String> zipPathSubset = new HashSet<String>(mManifestEntries.keySet());
zipPathSubset.removeAll(sharedPaths);
// removing root directory
zipPathSubset.remove(SLASH);
// No directory are listed in a ZIP removing all directory with
// content
Iterator<String> manifestOnlyPaths = zipPathSubset.iterator();
while (manifestOnlyPaths.hasNext()) {
String manifestOnlyPath = manifestOnlyPaths.next();
// assumption: all directories end with slash
if (manifestOnlyPath.endsWith(SLASH)) {
removeDirectory(manifestOnlyPath);
} else {
// if it is a nonexistent file
logValidationError(
OdfPackageConstraint.MANIFEST_LISTS_NONEXISTENT_FILE, getBaseURI(), manifestOnlyPath);
// remove from the manifest Map
OdfFileEntry manifestEntry = mManifestEntries.remove(manifestOnlyPath);
// remove from the manifest DOM
FileEntryElement manifestEle = manifestEntry.getOdfElement();
manifestEle.getParentNode().removeChild(manifestEle);
}
}
}
// remove none document directories
Iterator<String> sharedPathsIter = sharedPaths.iterator();
while (sharedPathsIter.hasNext()) {
String sharedPath = sharedPathsIter.next();
// assumption: all directories end with slash
if (sharedPath.endsWith(SLASH)) {
removeDirectory(sharedPath);
}
}
}
/**
* Removes directories without a mimetype (all none documents)
*
* @throws SAXException
*/
private void removeDirectory(String path) throws SAXException {
if (path.endsWith(SLASH)) {
// Check if it is a sub-document?
// Our assumption: it is a document if it has a mimetype...
String dirMimeType = mManifestEntries.get(path).getMediaTypeString();
if (dirMimeType == null || EMPTY_STRING.equals(dirMimeType)) {
logValidationWarning(OdfPackageConstraint.MANIFEST_LISTS_DIRECTORY, getBaseURI(), path);
// remove from the manifest Map
OdfFileEntry manifestEntry = mManifestEntries.remove(path);
// remove from the manifest DOM
FileEntryElement manifestEle = manifestEntry.getOdfElement();
manifestEle.getParentNode().removeChild(manifestEle);
}
}
}
/**
* Reads the uncompressed "mimetype" file, which contains the package media / mime type
*
* @throws SAXException
*/
private void initializeMediaType(String firstEntryName) throws SAXException, IOException {
ZipArchiveEntry mimetypeEntry = mZipEntries.get(OdfPackage.OdfFile.MEDIA_TYPE.getPath());
if (mimetypeEntry != null) {
if (mErrorHandler != null) {
validateMimeTypeEntry(mimetypeEntry, firstEntryName);
}
// get mediatype value of the root document/package from the
// mediatype file stream
String entryMediaType = getMediaTypeFromEntry(mimetypeEntry);
// get mediatype value of the root document/package from the
// manifest.xml
String manifestMediaType = getMediaTypeFromManifest();
// if a valid mediatype was set by the "mimetype" file
if (entryMediaType != null && !entryMediaType.equals(EMPTY_STRING)) {
// the root document's mediatype is taken from the "mimetype"
// file
mMediaType = entryMediaType;
if (mErrorHandler != null) {
// if the "mediatype" does exist, the
// "/META-INF/manifest.xml" have to contain a MIMETYPE for
// the root document);
if (manifestMediaType != null && !manifestMediaType.equals(EMPTY_STRING)) {
// if the two media-types are inconsistent
if (!entryMediaType.equals(manifestMediaType)) {
logValidationError(
OdfPackageConstraint.MIMETYPE_DIFFERS_FROM_PACKAGE,
getBaseURI(),
CONTROL_CHAR_PATTERN.matcher(mMediaType).replaceAll(EMPTY_STRING),
manifestMediaType);
}
} else { // if "mimetype" file exists, there have to be a
// mimetype in the manifest.xml for the root
// document (see ODF 1.2 part 3)
logValidationError(
OdfPackageConstraint.MIMETYPE_WITHOUT_MANIFEST_MEDIATYPE,
getBaseURI(),
CONTROL_CHAR_PATTERN.matcher(mMediaType).replaceAll(EMPTY_STRING),
manifestMediaType);
}
}
} else // if there is no media-type was set by the "mimetype" file
// try as fall-back the mediatype of the root document from the
// manifest.xml
{
if (manifestMediaType != null && !manifestMediaType.equals(EMPTY_STRING)) {
// and used as fall-back for the mediatype of the package
mMediaType = manifestMediaType;
}
}
} else {
String manifestMediaType = getMediaTypeFromManifest();
if (manifestMediaType != null && !manifestMediaType.equals(EMPTY_STRING)) {
// if not mimetype file exists, the root document mediaType from
// the manifest.xml is taken
mMediaType = manifestMediaType;
}
if (mErrorHandler != null) {
logValidationWarning(OdfPackageConstraint.MIMETYPE_NOT_IN_PACKAGE, getBaseURI());
}
}
}
private void validateMimeTypeEntry(ZipArchiveEntry mimetypeEntry, String firstEntryName)
throws SAXException {
if (mimetypeEntry.getMethod() != ZipArchiveEntry.STORED) {
logValidationError(OdfPackageConstraint.MIMETYPE_IS_COMPRESSED, getBaseURI());
}
if (mimetypeEntry.getExtra() != null && mimetypeEntry.getExtra().length > 0) {
logValidationError(OdfPackageConstraint.MIMETYPE_HAS_EXTRA_FIELD, getBaseURI());
}
if (!OdfFile.MEDIA_TYPE.getPath().equals(firstEntryName)) {
logValidationError(OdfPackageConstraint.MIMETYPE_NOT_FIRST_IN_PACKAGE, getBaseURI());
}
}
/** @returns the media type of the root document from the manifest.xml */
private String getMediaTypeFromManifest() {
OdfFileEntry rootDocumentEntry = mManifestEntries.get(SLASH);
if (rootDocumentEntry != null) {
return rootDocumentEntry.getMediaTypeString();
} else {
return null;
}
}
/** @returns the media type of the root document from the manifest.xml */
private String getMediaTypeFromEntry(ZipArchiveEntry mimetypeEntry)
throws SAXException, IOException {
String entryMediaType = null;
ByteArrayOutputStream out = new ByteArrayOutputStream();
try {
StreamHelper.transformStream(mZipFile.getInputStream(mimetypeEntry), out);
entryMediaType = new String(out.toByteArray(), 0, out.size(), "UTF-8");
} catch (IOException ex) {
Logger.getLogger(OdfPackage.class.getName()).log(Level.SEVERE, null, ex);
handleIOException(ex, false);
} finally {
if (out != null) {
try {
closeStream(out);
} catch (IOException ex) {
Logger.getLogger(OdfPackage.class.getName()).log(Level.SEVERE, null, ex);
}
out = null;
}
}
return entryMediaType;
}
private void closeStream(Closeable closeable) throws SAXException, IOException {
if (closeable != null) {
try {
closeable.close();
} catch (IOException ioe) {
// Warning only. This is usually just logged.
// Allow user to throw an exception all the same
handleIOException(ioe, true);
}
}
}
private void handleIOException(IOException ex, boolean warningOnly)
throws SAXException, IOException {
if (mErrorHandler != null) {
SAXParseException se = new SAXParseException(ex.getMessage(), null, ex);
try {
if (warningOnly) {
mErrorHandler.warning(se);
} else {
mErrorHandler.error(se);
}
} catch (SAXException e1) {
if (e1 == se) {
throw ex;
// We re-throw the original exception if the error handler
// just threw the SAXException we gave it.
} else {
throw e1; // Throw what the error handler threw.
}
}
}
throw ex; // No error handler? Just throw the original IOException
}
/**
* Insert an ODF document into the package at the given path. The path has to be a directory and
* will receive the MIME type of the OdfPackageDocument.
*
* @param doc the OdfPackageDocument to be inserted.
* @param internalPath path relative to the package root, where the document should be inserted.
*/
void cacheDocument(OdfPackageDocument doc, String internalPath) {
if (!internalPath.isEmpty()) {
internalPath = normalizeDirectoryPath(internalPath);
updateFileEntry(ensureFileEntryExistence(internalPath), doc.getMediaTypeString());
mPkgDocuments.put(internalPath, doc);
}
}
/**
* Set the baseURI for this ODF package. NOTE: Should only be set during saving the package.
*
* @param baseURI defining the location of the package
*/
void setBaseURI(String baseURI) {
mBaseURI = baseURI;
}
/**
* @return The URI to the ODF package, usually the URL, where this ODF package is located. If the
* package has not URI NULL is returned. This is the case if the package was new created
* without an URI and not saved before.
*/
public String getBaseURI() {
return mBaseURI;
}
/**
* Returns on ODF documents based a given mediatype.
*
* @param internalPath path relative to the package root, where the document should be loaded.
* @return The ODF document, which mediatype depends on the parameter or NULL if media type were
* not supported.
*/
public OdfPackageDocument loadDocument(String internalPath) {
OdfPackageDocument doc = getCachedDocument(internalPath);
if (doc == null) {
String mediaTypeString = getMediaTypeString();
// ToDo: Issue 265 - Remove dependency to higher layer by factory
OdfMediaType odfMediaType = OdfMediaType.getOdfMediaType(mediaTypeString);
if (odfMediaType == null) {
doc = new OdfPackageDocument(this, internalPath, mediaTypeString);
} else {
try {
String documentMediaType = getMediaTypeString(internalPath);
odfMediaType = OdfMediaType.getOdfMediaType(documentMediaType);
if (odfMediaType == null) {
return null;
}
// ToDo: Issue 265 - Remove dependency to higher layer by factory
doc = OdfDocument.loadDocument(this, internalPath);
} catch (Exception ex) {
// ToDo: catching Exception, logging it and continuing is bad style.
// Refactor exception handling in higher layer, too. - ??
Logger.getLogger(OdfPackageDocument.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
return doc;
}
/**
* @deprecated This method is only added temporary as workaround for the IBM fork using different
* DOC classes. Until the registering of DOC documents to the PKG layer has been finished.
* @param internalPath path relative to the package root, where the document should be inserted.
* @return an already open OdfPackageDocument via its path, otherwise NULL.
*/
@Deprecated
public OdfPackageDocument getCachedDocument(String internalPath) {
internalPath = normalizeDirectoryPath(internalPath);
return mPkgDocuments.get(internalPath);
}
/**
* @param dom the DOM tree that has been parsed and should be added to the cache.
* @param internalPath path relative to the package root, where the XML of the DOM is located.
* @return an already open OdfPackageDocument via its path, otherwise NULL.
*/
void cacheDom(Document dom, String internalPath) {
internalPath = normalizeFilePath(internalPath);
this.insert(dom, internalPath, null);
}
/**
* @param internalPath path relative to the package root, where the document should be inserted.
* @return an already open W3C XML Document via its path, otherwise NULL.
*/
Document getCachedDom(String internalPath) {
internalPath = normalizeFilePath(internalPath);
return this.mPkgDoms.get(internalPath);
}
/** @return a map with all open W3C XML documents with their internal package path as key. */
Map<String, Document> getCachedDoms() {
return this.mPkgDoms;
}
/**
* Removes a document from the package via its path. Independent if it was already opened or not.
*
* @param internalPath path relative to the package root, where the document should be removed.
*/
public void removeDocument(String internalPath) {
// Note: the EMPTY String for root path will be exchanged to a SLASH
internalPath = normalizeDirectoryPath(internalPath);
// get all files of the package
Set<String> allPackageFileNames = getFilePaths();
// If the document is the root document
// the "/" representing the root document is outside the
// manifest.xml in the API an empty path
// still normalizeDirectoryPath() already exchanged the EMPTY_STRING
// to SLASH
if (internalPath.equals(SLASH)) {
for (String entryName : allPackageFileNames) {
remove(entryName);
}
remove(SLASH);
} else {
// remove all the stream of the directory, such as pictures
List<String> directoryEntryNames = new ArrayList<String>();
for (String entryName : allPackageFileNames) {
if (entryName.startsWith(internalPath)) {
directoryEntryNames.add(entryName);
}
}
for (String entryName : directoryEntryNames) {
remove(entryName);
}
remove(internalPath);
}
}
/** @return all currently opened OdfPackageDocument of this OdfPackage */
Set<String> getCachedPackageDocuments() {
return mPkgDocuments.keySet();
}
public OdfPackageDocument getRootDocument() {
OdfPackageDocument odfPackageDocument = null;
odfPackageDocument = mPkgDocuments.get(OdfPackageDocument.ROOT_DOCUMENT_PATH);
if (odfPackageDocument == null) {
odfPackageDocument = this.loadDocument(OdfPackageDocument.ROOT_DOCUMENT_PATH);
}
return odfPackageDocument;
}
public OdfManifestDom getManifestDom() {
return mManifestDom;
}
/**
* Get the media type of the ODF file or document (ie. a directory). A directory with a mediatype
* can be loaded as <code>OdfPackageDocument</code>. Note: A directoy is represented by in the
* package as directory with media type
*
* @param internalPath within the package of the file or document.
* @return the mediaType for the resource of the given path
*/
public String getMediaTypeString(String internalPath) {
String mediaType = null;
if (internalPath != null) {
if (internalPath.equals(EMPTY_STRING) || internalPath.equals(SLASH)) {
return mMediaType;
} else {
mediaType = getMediaTypeFromEntry(normalizePath(internalPath));
// if no file was found, look for a normalized directory name
if (mediaType == null) {
mediaType = getMediaTypeFromEntry(normalizeDirectoryPath(internalPath));
}
}
}
return mediaType;
}
private String getMediaTypeFromEntry(String internalPath) {
OdfFileEntry entry = getFileEntry(internalPath);
// if the document is not in the package, the return is NULL
if (entry != null) {
return entry.getMediaTypeString();
} else {
return null;
}
}
/**
* Get the media type of the ODF package (equal to media type of ODF root document)
*
* @return the mediaType string of this ODF package
*/
public String getMediaTypeString() {
return mMediaType;
}
/**
* Set the media type of the ODF package (equal to media type of ODF root document)
*
* @param mediaType string of this ODF package
*/
void setMediaTypeString(String mediaType) {
mMediaType = mediaType;
}
/**
* Get an OdfFileEntry for the internalPath NOTE: This method should be better moved to a DOM
* inherited Manifest class
*
* @param internalPath The relative package path within the ODF package
* @return The manifest file entry will be returned.
*/
public OdfFileEntry getFileEntry(String internalPath) {
internalPath = normalizeFilePath(internalPath);
return mManifestEntries.get(internalPath);
}
/**
* Get a OdfFileEntries from the manifest file (i.e. /META/manifest.xml")
*
* @return The paths of the manifest file entries will be returned.
*/
public Set<String> getFilePaths() {
return mManifestEntries.keySet();
}
/**
* Check existence of a file in the package.
*
* @param internalPath The relative package documentURL within the ODF package
* @return True if there is an entry and a file for the given documentURL
*/
public boolean contains(String internalPath) {
internalPath = normalizeFilePath(internalPath);
return mManifestEntries.containsKey(internalPath);
}
/**
* Save the package to given documentURL.
*
* @param odfPath - the path to the ODF package destination
* @throws java.io.IOException - if the package could not be saved
*/
public void save(String odfPath) throws SAXException, IOException {
File f = new File(odfPath);
save(f);
}
/**
* Save package to a given File. After saving it is still necessary to close the package to have
* again full access about the file.
*
* @param pkgFile - the File to save the ODF package to
* @throws java.io.IOException - if the package could not be saved
*/
public void save(File pkgFile) throws SAXException, IOException {
String baseURL = getBaseURLFromFile(pkgFile);
// if (baseURL.equals(mBaseURI)) {
// // save to the same file: cache everything first
// // ToDo: (Issue 219 - PackageRefactoring) --maybe it's better to
// write to a new file and copy that
// // to the original one - would be less memory footprint
// cacheContent();
// }
FileOutputStream fos = new FileOutputStream(pkgFile);
try {
save(fos, baseURL);
} finally {
fos.close();
}
}
/**
* Saves the package to a given {@link OutputStream}. The given stream is not closed by this
* method.
*
* @param odfStream the output stream
* @throws IOException if an I/O error occurs while saving the package
* @throws SAXException
*/
public void save(OutputStream odfStream) throws SAXException, IOException {
save(odfStream, null);
}
/**
* Sets the password of this package. if password is not null, package will be encrypted when
* save.
*
* @param password password
* @since 0.8.9
*/
public void setPassword(String password) {
mNewPwd = password;
}
/**
* Save an ODF document to the OutputStream.
*
* @param odfStream - the OutputStream to insert content to
* @param baseURL defining the location of the package
* @throws java.io.IOException if an I/O error occurs while saving the package
*/
private void save(OutputStream odfStream, String baseURL) throws IOException, SAXException {
mBaseURI = baseURL;
OdfFileEntry rootEntry = mManifestEntries.get(SLASH);
if (rootEntry == null) {
rootEntry =
new OdfFileEntry(
getManifestDom().getRootElement().newFileEntryElement(SLASH, mMediaType));
mManifestEntries.put(SLASH, rootEntry);
} else {
rootEntry.setMediaTypeString(mMediaType);
}
ZipArchiveOutputStream zos = new ZipArchiveOutputStream(odfStream);
// remove mediatype path and use it as first
this.mManifestEntries.remove(OdfFile.MEDIA_TYPE.getPath());
Set<String> keys = mManifestEntries.keySet();
boolean isFirstFile = true;
CRC32 crc = new CRC32();
long modTime = (new java.util.Date()).getTime();
byte[] data = null;
for (String path : keys) {
// ODF requires the "mimetype" file to be at first in the package
if (isFirstFile) {
isFirstFile = false;
// create "mimetype" from current attribute value
data = mMediaType.getBytes("UTF-8");
createZipEntry(OdfFile.MEDIA_TYPE.getPath(), data, zos, modTime, crc);
}
// create an entry, but NOT for "ODF document directory", "MANIFEST" or "mimetype"
if (!path.endsWith(SLASH)
&& !path.equals(OdfPackage.OdfFile.MANIFEST.getPath())
&& !path.equals(OdfPackage.OdfFile.MEDIA_TYPE.getPath())) {
data = getBytes(path);
createZipEntry(path, data, zos, modTime, crc);
}
data = null;
}
// Create "META-INF/" directory
createZipEntry("META-INF/", null, zos, modTime, crc);
// Create "META-INF/manifest.xml" file after all entries with potential encryption have been
// added
data = getBytes(OdfFile.MANIFEST.getPath());
createZipEntry(OdfFile.MANIFEST.getPath(), data, zos, modTime, crc);
zos.flush();
zos.close();
odfStream.flush();
}
private void createZipEntry(
String path, byte[] data, ZipArchiveOutputStream zos, long modTime, CRC32 crc)
throws IOException {
ZipArchiveEntry ze = null;
ze = mZipEntries.get(path);
if (ze == null) {
ze = new ZipArchiveEntry(path);
}
ze.setTime(modTime);
if (fileNeedsCompression(path)) {
ze.setMethod(ZipArchiveEntry.DEFLATED);
} else {
ze.setMethod(ZipArchiveEntry.STORED);
}
crc.reset();
if (data != null) {
OdfFileEntry fileEntry = mManifestEntries.get(path);
// encrypt file
if (data.length > 0 && fileNeedsEncryption(path)) {
data = encryptData(data, fileEntry);
// encrypted file entries shall be flagged as 'STORED'.
ze.setMethod(ZipArchiveEntry.STORED);
// the size of the encrypted file should replace the real
// size value.
ze.setCompressedSize(data.length);
} else {
if (fileEntry != null) {
fileEntry.setSize(null);
FileEntryElement fileEntryEle = fileEntry.getOdfElement();
EncryptionDataElement encryptionDataElement =
OdfElement.findFirstChildNode(EncryptionDataElement.class, fileEntryEle);
while (encryptionDataElement != null) {
fileEntryEle.removeChild(encryptionDataElement);
encryptionDataElement =
OdfElement.findFirstChildNode(EncryptionDataElement.class, fileEntryEle);
}
}
ze.setCompressedSize(-1);
}
ze.setSize(data.length);
crc.update(data);
ze.setCrc(crc.getValue());
} else {
ze.setSize(0);
ze.setCrc(0);
ze.setCompressedSize(-1);
}
zos.putArchiveEntry(ze);
if (data != null) {
zos.write(data, 0, data.length);
}
zos.closeArchiveEntry();
mZipEntries.put(path, ze);
}
/**
* Determines if a file have to be compressed.
*
* @param internalPath the file location
* @return true if the file needs compression, false, otherwise
*/
private boolean fileNeedsCompression(String internalPath) {
boolean result = true;
// ODF spec does not allow compression of "./mimetype" file
if (internalPath.equals(OdfPackage.OdfFile.MEDIA_TYPE.getPath())) {
return false;
}
// see if the file was already compressed
if (internalPath.lastIndexOf(".") > 0) {
String suffix =
internalPath.substring(internalPath.lastIndexOf(".") + 1, internalPath.length());
if (COMPRESSED_FILETYPES.contains(suffix.toLowerCase())) {
result = false;
}
}
return result;
}
/**
* Determines if a file have to be encrypted.
*
* @param internalPath the file location
* @return true if the file needs encrypted, false, otherwise
*/
private boolean fileNeedsEncryption(String internalPath) {
if (mNewPwd != null) {
// ODF spec does not allow encrytion of "./mimetype" file
if (internalPath.endsWith(SLASH)
|| OdfFile.MANIFEST.getPath().equals(internalPath)
|| OdfPackage.OdfFile.MEDIA_TYPE.getPath().equals(internalPath)) {
return false;
}
return fileNeedsCompression(internalPath);
} else {
return false;
}
}
private void close(Closeable closeable) throws SAXException, IOException {
if (closeable != null) {
try {
closeable.close();
} catch (IOException ioe) {
// Warning only. This is usually just logged.
// Allow user to throw an exception all the same
handleIOException(ioe, true);
}
}
}
/**
* Close the OdfPackage after it is no longer needed. Even after saving it is still necessary to
* close the package to have again full access about the file. Closing the OdfPackage will release
* all temporary created data. Do this as the last action to free resources. Closing an already
* closed document has no effect.
*/
public void close() {
if (mZipFile != null) {
try {
mZipFile.close();
} catch (IOException ex) {
// log exception and continue
Logger.getLogger(OdfPackage.class.getName()).log(Level.INFO, null, ex);
}
}
// release all stuff - this class is impossible to use afterwards
mZipFile = null;
mMediaType = null;
mZipEntries = null;
mPkgDoms = null;
mMemoryFileCache = null;
mManifestEntries = null;
mBaseURI = null;
mResolver = null;
}
/** Parse the Manifest file */
private void parseManifest() throws SAXException, IOException {
mManifestDom = (OdfManifestDom) OdfFileDom.newFileDom(this, OdfFile.MANIFEST.getPath());
ManifestElement manifestEle = mManifestDom.getRootElement();
if (manifestEle != null) {
setManifestVersion(manifestEle.getVersionAttribute());
} else {
logValidationError(OdfPackageConstraint.MANIFEST_NOT_IN_PACKAGE, getBaseURI());
}
Map<String, OdfFileEntry> entries = getManifestEntries();
FileEntryElement fileEntryEle =
OdfElement.findFirstChildNode(FileEntryElement.class, manifestEle);
while (fileEntryEle != null) {
String path = fileEntryEle.getFullPathAttribute();
if (path.equals(EMPTY_STRING)) {
if (getErrorHandler() != null) {
logValidationError(OdfPackageConstraint.MANIFEST_WITH_EMPTY_PATH, getBaseURI());
}
} else {
path = normalizePath(path);
OdfFileEntry currentFileEntry = entries.get(path);
if (currentFileEntry == null) {
currentFileEntry = new OdfFileEntry(fileEntryEle);
}
if (path != null) {
entries.put(path, currentFileEntry);
}
}
fileEntryEle = OdfElement.findNextChildNode(FileEntryElement.class, fileEntryEle);
}
mMemoryFileCache.remove(OdfFile.MANIFEST.getPath());
mPkgDoms.put(OdfFile.MANIFEST.getPath(), mManifestDom);
}
XMLReader getXMLReader() throws ParserConfigurationException, SAXException {
// create sax parser
SAXParserFactory saxFactory = new org.apache.xerces.jaxp.SAXParserFactoryImpl();
saxFactory.setNamespaceAware(true);
saxFactory.setValidating(false);
try {
saxFactory.setXIncludeAware(false);
saxFactory.setFeature(
"http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
// removing potential vulnerability: see
// https://www.owasp.org/index.php/XML_External_Entity_%28XXE%29_Processing
saxFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
saxFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
saxFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
} catch (Exception ex) {
Logger.getLogger(OdfPackage.class.getName()).log(Level.SEVERE, null, ex);
throw new RuntimeException();
}
SAXParser parser;
try {
parser = saxFactory.newSAXParser();
} catch (ParserConfigurationException pce) {
// Re-throw as SAXException in order not to introduce too many checked exceptions
throw new SAXException(pce);
}
XMLReader xmlReader = parser.getXMLReader();
// More details at http://xerces.apache.org/xerces2-j/features.html#namespaces
xmlReader.setFeature("http://xml.org/sax/features/namespaces", true);
// More details at http://xerces.apache.org/xerces2-j/features.html#namespace-prefixes
xmlReader.setFeature("http://xml.org/sax/features/namespace-prefixes", true);
// More details at http://xerces.apache.org/xerces2-j/features.html#xmlns-uris
xmlReader.setFeature("http://xml.org/sax/features/xmlns-uris", true);
// removing potential vulnerability: see
// https://www.owasp.org/index.php/XML_External_Entity_%28XXE%29_Processing
xmlReader.setFeature("http://xml.org/sax/features/external-general-entities", false);
xmlReader.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
xmlReader.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
return xmlReader;
}
// Add the given path and all its subdirectories to the internalPath list
// to be written later to the manifest
private void createSubEntries(String internalPath) {
ManifestElement manifestEle = getManifestDom().getRootElement();
StringTokenizer tok = new StringTokenizer(internalPath, SLASH);
if (tok.countTokens() > 1) {
String path = EMPTY_STRING;
while (tok.hasMoreTokens()) {
String directory = tok.nextToken();
// it is a directory, if there are more token
if (tok.hasMoreTokens()) {
path = path + directory + SLASH;
OdfFileEntry fileEntry = mManifestEntries.get(path);
// ??? no subdirectory without mimetype in the specification allowed
if (fileEntry == null) {
mManifestEntries.put(
path, new OdfFileEntry(manifestEle.newFileEntryElement(path, null)));
}
}
}
}
}
/**
* Insert DOM tree into OdfPackage. An existing file will be replaced.
*
* @param fileDOM - XML DOM tree to be inserted as file.
* @param internalPath - relative documentURL where the DOM tree should be inserted as XML file
* @param mediaType - media type of stream. Set to null if unknown
* @throws java.lang.Exception when the DOM tree could not be inserted
*/
public void insert(Document fileDOM, String internalPath, String mediaType) {
internalPath = normalizeFilePath(internalPath);
if (mediaType == null) {
mediaType = XML_MEDIA_TYPE;
}
if (fileDOM == null) {
mPkgDoms.remove(internalPath);
} else {
mPkgDoms.put(internalPath, fileDOM);
}
if (!internalPath.endsWith(OdfFile.MANIFEST.internalPath)) {
updateFileEntry(ensureFileEntryExistence(internalPath), mediaType);
}
// remove byte array version of new DOM
mMemoryFileCache.remove(internalPath);
}
/**
* Embed an OdfPackageDocument to the current OdfPackage. All the file entries of child document
* will be inserted.
*
* @param sourceDocument the OdfPackageDocument to be embedded.
* @param destinationPath path to the directory the ODF document should be inserted (relative to
* ODF package root).
*/
public void insertDocument(OdfPackageDocument sourceDocument, String destinationPath) {
destinationPath = normalizeDirectoryPath(destinationPath);
// opened DOM of descendant Documents will be flashed to the their pkg
flushDoms(sourceDocument);
// Gets the OdfDocument's manifest entry info, no matter it is a
// independent document or an embeddedDocument.
Map<String, OdfFileEntry> manifestEntriesToCopy;
String sourceSubPath = null;
if (sourceDocument.isRootDocument()) {
manifestEntriesToCopy = sourceDocument.getPackage().getManifestEntries();
sourceSubPath = ROOT_DOCUMENT_PATH;
} else {
manifestEntriesToCopy =
sourceDocument.getPackage().getSubDirectoryEntries(sourceDocument.getDocumentPath());
sourceSubPath = sourceDocument.getDocumentPath();
}
addEntriesToPackageAndManifest(
manifestEntriesToCopy, sourceDocument, sourceSubPath, destinationPath);
if (!mManifestEntries.containsKey(destinationPath)) {
ManifestElement manifestEle = mManifestDom.getRootElement();
// make sure the media type of embedded Document is right set.
OdfFileEntry embedDocumentRootEntry =
new OdfFileEntry(
manifestEle.newFileEntryElement(
destinationPath, sourceDocument.getMediaTypeString()));
mManifestEntries.put(destinationPath, embedDocumentRootEntry);
}
// the new document will be attached to its new package (it has been
// inserted to)
sourceDocument.setPackage(this);
cacheDocument(sourceDocument, destinationPath);
}
private void addEntriesToPackageAndManifest(
Map<String, OdfFileEntry> entryMapToCopy,
OdfPackageDocument sourceDocument,
String subDocumentPath,
String destinationPath) {
// insert to package and add it to the Manifest
destinationPath = sourceDocument.setDocumentPath(destinationPath);
Set<String> entryNameList = entryMapToCopy.keySet();
for (String entryName : entryNameList) {
OdfFileEntry entry = entryMapToCopy.get(entryName);
if (entry != null) {
try {
if (!subDocumentPath.equals(ROOT_DOCUMENT_PATH)) {
entryName = entryName.substring(subDocumentPath.length());
if (entryName.length() == 0) {
entryName = SLASH;
}
}
// if entry is a directory (e.g. an ODF document root)
if (entryName.endsWith(SLASH)) {
// insert directory
if (entryName.equals(SLASH)) {
insert((byte[]) null, destinationPath, sourceDocument.getMediaTypeString());
} else {
String mediaType = sourceDocument.getMediaTypeString();
if (mediaType != null && mediaType.length() != 0) {
if (!destinationPath.equals(SLASH)) {
entryName = destinationPath + entryName;
}
insert((byte[]) null, entryName, entry.getMediaTypeString());
}
}
} else {
String documentDirectory = null;
if (destinationPath.equals(SLASH)) {
documentDirectory = EMPTY_STRING;
} else {
documentDirectory = destinationPath;
}
String packagePath = documentDirectory + entryName;
insert(
sourceDocument.getPackage().getInputStream(entry.getPath()),
packagePath,
entry.getMediaTypeString());
}
} catch (Exception ex) {
Logger.getLogger(OdfPackage.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
/**
* Insert all open DOMs of XML files beyond parent document to the package. The XML files will be
* updated in the package after calling save.
*
* @param parentDocument the document, which XML files shall be serialized
*/
void flushDoms(OdfPackageDocument parentDocument) {
OdfPackage pkg = parentDocument.getPackage();
if (parentDocument.isRootDocument()) {
// for every parsed XML file (DOM)
for (String xmlFilePath : pkg.getCachedDoms().keySet()) {
// insert it to the package (serializing and caching it till
// final save)
pkg.insert(pkg.getCachedDom(xmlFilePath), xmlFilePath, "text/xml");
}
} else {
// if not root document, check ..
String parentDocumentPath = parentDocument.getDocumentPath();
// for every parsed XML file (DOM)
for (String xmlFilePath : pkg.getCachedDoms().keySet()) {
// if the file is within the given document
if (xmlFilePath.startsWith(parentDocumentPath)) {
// insert it to the package (serializing and caching it till
// final save)
pkg.insert(pkg.getCachedDom(xmlFilePath), xmlFilePath, "text/xml");
}
}
}
}
/** Get all the file entries from a sub directory */
private Map<String, OdfFileEntry> getSubDirectoryEntries(String directory) {
directory = normalizeDirectoryPath(directory);
Map<String, OdfFileEntry> subEntries = new HashMap<String, OdfFileEntry>();
Map<String, OdfFileEntry> allEntries = getManifestEntries();
Set<String> rootEntryNameSet = getFilePaths();
for (String entryName : rootEntryNameSet) {
if (entryName.startsWith(directory)) {
subEntries.put(entryName, allEntries.get(entryName));
}
}
return subEntries;
}
/**
* Method returns the paths of all document within the package.
*
* @return A set of paths of all documents of the package, including the root document.
*/
public Set<String> getDocumentPaths() {
return getDocumentPaths(null, null);
}
/**
* Method returns the paths of all document within the package matching the given criteria.
*
* @param mediaTypeString limits the desired set of document paths to documents of the given
* mediaType
* @return A set of paths of all documents of the package, including the root document, that match
* the given parameter.
*/
public Set<String> getDocumentPaths(String mediaTypeString) {
return getDocumentPaths(mediaTypeString, null);
}
/**
* Method returns the paths of all document within the package matching the given criteria.
*
* @param mediaTypeString limits the desired set of document paths to documents of the given
* mediaType
* @param subDirectory limits the desired set document paths to those documents below of this
* subdirectory
* @return A set of paths of all documents of the package, including the root document, that match
* the given parameter.
*/
Set<String> getDocumentPaths(String mediaTypeString, String subDirectory) {
Set<String> innerDocuments = new HashSet<String>();
Set<String> packageFilePaths = getFilePaths();
// check manifest for current embedded OdfPackageDocuments
for (String filePath : packageFilePaths) {
// check if a subdirectory was the criteria and if the files are
// beyond the given subdirectory
if (subDirectory == null
|| filePath.startsWith(subDirectory) && !filePath.equals(subDirectory)) {
// with documentURL is not empty and is a directory (ie. a
// potential document)
if (filePath.length() > 1 && filePath.endsWith(SLASH)) {
String fileMediaType = getFileEntry(filePath).getMediaTypeString();
if (fileMediaType != null && !fileMediaType.equals(EMPTY_STRING)) {
// check if a certain mediaType was the critera and was
// matched
if (mediaTypeString == null || mediaTypeString.equals(fileMediaType)) {
// only relative path is allowed as path
innerDocuments.add(filePath);
}
}
}
}
}
return innerDocuments;
}
/**
* Adding a manifest:file-entry to be saved in manifest.xml. In addition, sub directories will be
* added as well to the manifest.
*/
private OdfFileEntry ensureFileEntryExistence(String internalPath) {
// if it is NOT the resource "/META-INF/manifest.xml"
OdfFileEntry fileEntry = null;
if (!OdfFile.MANIFEST.internalPath.equals(internalPath) && !internalPath.equals(EMPTY_STRING)) {
if (mManifestEntries == null) {
mManifestEntries = new HashMap<String, OdfFileEntry>();
}
fileEntry = mManifestEntries.get(internalPath);
// for every new file entry
if (fileEntry == null) {
ManifestElement manifestEle = getManifestDom().getRootElement();
if (manifestEle == null) {
return null;
}
fileEntry = new OdfFileEntry(manifestEle.newFileEntryElement(internalPath, ""));
mManifestEntries.put(internalPath, fileEntry);
// creates recursive file entries for all sub directories
// BUT ONLY SUBDIRECTORYS WITH MIMETYPE (documents) ARE ALLOWED IN THE MANIFEST
// createSubEntries(internalPath);
}
}
return fileEntry;
}
/** update file entry setting. */
private void updateFileEntry(OdfFileEntry fileEntry, String mediaType) {
// overwrite previous settings
fileEntry.setMediaTypeString(mediaType);
// reset encryption data (ODFDOM does not support encryption yet)
// fileEntry.setEncryptionData(null);
// reset size to be unset
fileEntry.setSize(null);
}
/**
* Gets org.w3c.dom.Document for XML file contained in package.
*
* @param internalPath to a file within the Odf Package (eg. content.xml)
* @return an org.w3c.dom.Document
* @throws SAXException
* @throws ParserConfigurationException
* @throws IOException
* @throws IllegalArgumentException
* @throws TransformerConfigurationException
* @throws TransformerException
*/
public Document getDom(String internalPath)
throws SAXException, ParserConfigurationException, IllegalArgumentException,
TransformerConfigurationException, TransformerException, IOException {
Document dom = mPkgDoms.get(internalPath);
if (dom != null) {
return dom;
}
InputStream is = getInputStream(internalPath);
// We depend on Xerces. So we just go ahead and create a Xerces DBF,
// without
// forcing everything else to do so.
DocumentBuilderFactory factory = new org.apache.xerces.jaxp.DocumentBuilderFactoryImpl();
factory.setNamespaceAware(true);
factory.setValidating(false);
try {
factory.setXIncludeAware(false);
factory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
// removing potential vulnerability: see
// https://www.owasp.org/index.php/XML_External_Entity_%28XXE%29_Processing
factory.setFeature("http://xml.org/sax/features/external-general-entities", false);
factory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
factory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
} catch (Exception ex) {
Logger.getLogger(OdfPackage.class.getName()).log(Level.SEVERE, null, ex);
throw new RuntimeException();
}
DocumentBuilder builder = factory.newDocumentBuilder();
builder.setEntityResolver(getEntityResolver());
String uri = getBaseURI() + internalPath;
if (mErrorHandler != null) {
builder.setErrorHandler(mErrorHandler);
}
InputSource ins = new InputSource(is);
ins.setSystemId(uri);
dom = builder.parse(ins);
if (dom != null) {
mPkgDoms.put(internalPath, dom);
mMemoryFileCache.remove(internalPath);
}
return dom;
}
/**
* Inserts an external file into an OdfPackage. An existing file will be replaced.
*
* @param sourceURI - the source URI to the file to be inserted into the package.
* @param internalPath - relative documentURL where the tree should be inserted as XML file
* @param mediaType - media type of stream. Set to null if unknown
* @throws java.lang.Exception In case the file could not be saved
*/
public void insert(URI sourceURI, String internalPath, String mediaType) throws Exception {
InputStream is = null;
if (sourceURI.isAbsolute()) {
// if the URI is absolute it can be converted to URL
is = sourceURI.toURL().openStream();
} else {
// otherwise create a file class to open the stream
is = new FileInputStream(sourceURI.toString());
}
insert(is, internalPath, mediaType);
}
/**
* Inserts InputStream into an OdfPackage. An existing file will be replaced.
*
* @param fileStream - the stream of the file to be inserted into the ODF package.
* @param internalPath - relative documentURL where the tree should be inserted as XML file
* @param mediaType - media type of stream. Set to null if unknown
*/
public void insert(InputStream fileStream, String internalPath, String mediaType)
throws IOException {
internalPath = normalizeFilePath(internalPath);
if (fileStream == null) {
// adding a simple directory without MIMETYPE
insert((byte[]) null, internalPath, mediaType);
} else {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
BufferedInputStream bis = null;
if (fileStream instanceof BufferedInputStream) {
bis = (BufferedInputStream) fileStream;
} else {
bis = new BufferedInputStream(fileStream);
}
StreamHelper.transformStream(bis, baos);
byte[] data = baos.toByteArray();
insert(data, internalPath, mediaType);
}
}
/**
* Inserts a byte array into OdfPackage. An existing file will be replaced. If the byte array is
* NULL a directory with the given mimetype will be created.
*
* @param fileBytes - data of the file stream to be stored in package. If NULL a directory with
* the given mimetype will be created.
* @param internalPath - path of the file or directory relative to the package root.
* @param mediaTypeString - media type of stream. If unknown null can be used.
*/
public void insert(byte[] fileBytes, String internalPath, String mediaTypeString) {
internalPath = normalizeFilePath(internalPath);
// if path is from the mimetype, which should be first in document
if (OdfPackage.OdfFile.MEDIA_TYPE.getPath().equals(internalPath)) {
try {
setMediaTypeString(new String(fileBytes, "UTF-8"));
} catch (UnsupportedEncodingException useEx) {
Logger.getLogger(OdfPackage.class.getName())
.log(Level.SEVERE, "ODF file could not be created as string!", useEx);
}
return;
}
if (fileBytes != null) {
mMemoryFileCache.put(internalPath, fileBytes);
// as DOM would overwrite data cache, any existing DOM cache will be
// deleted
if (mPkgDoms.containsKey(internalPath)) {
mPkgDoms.remove(internalPath);
}
}
updateFileEntry(ensureFileEntryExistence(internalPath), mediaTypeString);
}
// changed to package access as the manifest interiors are an implementation
// detail
Map<String, OdfFileEntry> getManifestEntries() {
return mManifestEntries;
}
/**
* Get package (sub-) content as byte array
*
* @param internalPath relative documentURL to the package content
* @return the unzipped package content as byte array
* @throws java.lang.Exception
*/
public byte[] getBytes(String internalPath) {
// if path is null or empty return null
if (internalPath == null || internalPath.equals(EMPTY_STRING)) {
return null;
}
internalPath = normalizeFilePath(internalPath);
byte[] data = null;
// if the file is "mimetype"
if (internalPath.equals(OdfPackage.OdfFile.MEDIA_TYPE.getPath())) {
if (mMediaType == null) {
return null;
} else {
try {
data = mMediaType.getBytes("UTF-8");
} catch (UnsupportedEncodingException use) {
Logger.getLogger(OdfPackage.class.getName()).log(Level.SEVERE, null, use);
return null;
}
}
} else if (mPkgDoms.get(internalPath) != null) {
data = flushDom(mPkgDoms.get(internalPath));
mMemoryFileCache.put(internalPath, data);
// if the path's file was cached to memory (second high priority)
} else if (mManifestEntries.containsKey(internalPath)
&& mMemoryFileCache.get(internalPath) != null) {
data = mMemoryFileCache.get(internalPath);
// if the path's file was cached to disc (lowest priority)
}
// if not available, check if file exists in ZIP
if (data == null) {
ZipArchiveEntry entry = null;
if ((entry = mZipEntries.get(internalPath)) != null) {
InputStream inputStream = null;
try {
inputStream = mZipFile.getInputStream(entry);
if (inputStream != null) {
ByteArrayOutputStream out = new ByteArrayOutputStream();
StreamHelper.transformStream(inputStream, out);
data = out.toByteArray();
// decrypt data as needed
if (!(internalPath.equals(OdfFile.MEDIA_TYPE.getPath())
|| internalPath.equals(OdfFile.MANIFEST.getPath()))) {
OdfFileEntry manifestEntry = getManifestEntries().get(internalPath);
EncryptionDataElement encryptionDataElement = manifestEntry.getEncryptionData();
if (encryptionDataElement != null) {
byte[] newData = decryptData(data, manifestEntry, encryptionDataElement);
if (newData != null) {
data = newData;
} else {
Logger.getLogger(OdfPackage.class.getName())
.log(Level.SEVERE, null, "Wrong password being used for decryption!");
}
}
}
// store for further usage; do not care about manifest:
// that is handled exclusively
mMemoryFileCache.put(internalPath, data);
}
} catch (IOException ex) {
// Catching IOException here should be fine: in-memory operations only
Logger.getLogger(OdfPackage.class.getName()).log(Level.SEVERE, null, ex);
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
} catch (IOException ex) {
Logger.getLogger(OdfPackage.class.getName()).log(Level.SEVERE, null, ex);
}
}
}
}
return data;
}
// encrypt data and update manifest.
private byte[] encryptData(byte[] data, OdfFileEntry fileEntry) {
byte[] encryptedData = null;
try {
// 1.The original uncompressed, unencrypted size is
// contained in the manifest:size.
fileEntry.setSize(data.length);
// 2.Compress with the "deflate" algorithm
Deflater compresser = new Deflater(Deflater.DEFLATED, true);
compresser.setInput(data);
compresser.finish();
byte[] compressedData = new byte[data.length];
int compressedDataLength = compresser.deflate(compressedData);
// 3. The start key is generated: the byte sequence
// representing the password in UTF-8 is used to
// generate a 20-byte SHA1 digest.
byte[] passBytes = mNewPwd.getBytes("UTF-8");
MessageDigest md = MessageDigest.getInstance("SHA1");
passBytes = md.digest(passBytes);
// 4. Checksum specifies a digest in BASE64 encoding
// that can be used to detect password correctness. The
// digest is build from the compressed unencrypted file.
md.reset();
md.update(compressedData, 0, (compressedDataLength > 1024 ? 1024 : compressedDataLength));
byte[] checksumBytes = new byte[20];
md.digest(checksumBytes, 0, 20);
// 5. For each file, a 16-byte salt is generated by a random
// generator.
// The salt is a BASE64 encoded binary sequence.
SecureRandom secureRandom = SecureRandom.getInstance("SHA1PRNG");
byte[] salt = new byte[16];
secureRandom.nextBytes(salt);
// char passChars[] = new String(passBytes, "UTF-8").toCharArray();
/*
* char passChars[] = new char[20]; for (int i = 0; i <
* passBytes.length; i++) { passChars[i] = (char)
* ((passBytes[i]+256)%256);
* //System.out.println("passChars[i]:"+passChars
* [i]+", passBytes[i]"+passBytes[i]); } //char passChars[] =
* getChars(passBytes); // 6. The PBKDF2 algorithm based on the
* HMAC-SHA-1 function is used for the key derivation.
* SecretKeyFactory factory =
* SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); // 7. The
* salt is used together with the start key to derive a unique
* 128-bit key for each file. // The default iteration count for the
* algorithm is 1024. KeySpec spec = new PBEKeySpec(passChars, salt,
* 1024, 128); SecretKey skey = factory.generateSecret(spec); byte[]
* raw = skey.getEncoded(); // algorithm-name="Blowfish CFB"
* SecretKeySpec skeySpec = new SecretKeySpec(raw, "Blowfish");
*/
byte[] dk = derivePBKDF2Key(passBytes, salt, 1024, 16);
SecretKeySpec key = new SecretKeySpec(dk, "Blowfish");
// 8.The files are encrypted: The random number
// generator is used to generate the 8-byte initialization vector
// for the
// algorithm. The derived key is used together with the
// initialization
// vector to encrypt the file using the Blowfish algorithm in cipher
// feedback
// CFB mode.
Cipher cipher = Cipher.getInstance("Blowfish/CFB/NoPadding");
// initialisation-vector specifies the byte-sequence used
// as an initialization vector to a encryption algorithm. The
// initialization vector is a BASE64 encoded binary sequence.
byte[] iv = new byte[8];
secureRandom.nextBytes(iv);
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.ENCRYPT_MODE, key, ivParameterSpec);
encryptedData = cipher.doFinal(compressedData, 0, compressedDataLength);
// 9.update file entry encryption data.
String checksum = new Base64Binary(checksumBytes).toString();
FileEntryElement fileEntryElement = fileEntry.getOdfElement();
EncryptionDataElement encryptionDataElement =
OdfElement.findFirstChildNode(EncryptionDataElement.class, fileEntryElement);
if (encryptionDataElement != null) {
fileEntryElement.removeChild(encryptionDataElement);
}
encryptionDataElement = fileEntryElement.newEncryptionDataElement(checksum, "SHA1/1K");
String initialisationVector = new Base64Binary(iv).toString();
AlgorithmElement algorithmElement =
OdfElement.findFirstChildNode(AlgorithmElement.class, encryptionDataElement);
if (algorithmElement != null) {
encryptionDataElement.removeChild(algorithmElement);
}
algorithmElement =
encryptionDataElement.newAlgorithmElement("Blowfish CFB", initialisationVector);
String saltStr = new Base64Binary(salt).toString();
KeyDerivationElement keyDerivationElement =
OdfElement.findFirstChildNode(KeyDerivationElement.class, encryptionDataElement);
if (keyDerivationElement != null) {
encryptionDataElement.removeChild(keyDerivationElement);
}
keyDerivationElement = encryptionDataElement.newKeyDerivationElement(1024, "PBKDF2", saltStr);
StartKeyGenerationElement startKeyGenerationElement =
OdfElement.findFirstChildNode(StartKeyGenerationElement.class, encryptionDataElement);
if (startKeyGenerationElement != null) {
encryptionDataElement.removeChild(startKeyGenerationElement);
}
encryptionDataElement.newStartKeyGenerationElement("SHA1").setKeySizeAttribute(20);
// System.out.println("full-path=\""+ path +"\"");
// System.out.println("size=\""+ data.length +"\"");
// System.out.println("checksum=\""+ checksum +"\"");
// System.out.println("compressedData ="+compressedDataLength);
// System.out.println("MANIFEST: " + fileEntryElement.getParentNode().toString());
} catch (Exception e) {
// throws NoSuchAlgorithmException,
// InvalidKeySpecException, NoSuchPaddingException,
// InvalidKeyException,
// InvalidAlgorithmParameterException,
// IllegalBlockSizeException, BadPaddingException
Logger.getLogger(OdfPackage.class.getName()).log(Level.SEVERE, null, e);
}
return encryptedData;
}
private byte[] decryptData(
byte[] data, OdfFileEntry manifestEntry, EncryptionDataElement encryptionDataElement) {
byte[] decompressData = null;
try {
KeyDerivationElement keyDerivationElement =
OdfElement.findFirstChildNode(KeyDerivationElement.class, encryptionDataElement);
AlgorithmElement algorithmElement =
OdfElement.findFirstChildNode(AlgorithmElement.class, encryptionDataElement);
String saltStr = keyDerivationElement.getSaltAttribute();
String ivStr = algorithmElement.getInitialisationVectorAttribute();
String checksum = encryptionDataElement.getChecksumAttribute();
byte[] salt = Base64Binary.valueOf(saltStr).getBytes();
byte[] iv = Base64Binary.valueOf(ivStr).getBytes();
byte[] passBytes = mOldPwd.getBytes("UTF-8");
MessageDigest md = MessageDigest.getInstance("SHA-1");
passBytes = md.digest(passBytes);
/*
* char passChars[] = new char[passBytes.length]; for(int i = 0;
* i<passBytes.length; i++){ passChars[i] =
* (char)(passBytes[i]|0xFF); } KeySpec spec = new
* PBEKeySpec(passChars, salt, 1024, 128); SecretKeyFactory factory
* = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); SecretKey
* skey = factory.generateSecret(spec); byte[] raw =
* skey.getEncoded(); SecretKeySpec skeySpec = new
* SecretKeySpec(raw, "Blowfish");
*/
byte[] dk = derivePBKDF2Key(passBytes, salt, 1024, 16);
SecretKeySpec key = new SecretKeySpec(dk, "Blowfish");
Cipher cipher = Cipher.getInstance("Blowfish/CFB/NoPadding");
IvParameterSpec ivParameterSpec = new IvParameterSpec(iv);
cipher.init(Cipher.DECRYPT_MODE, key, ivParameterSpec);
byte[] decryptedData = cipher.doFinal(data);
// valid checksum
md.reset();
md.update(decryptedData, 0, (decryptedData.length > 1024 ? 1024 : decryptedData.length));
byte[] checksumBytes = new byte[20];
md.digest(checksumBytes, 0, 20);
String newChecksum = new Base64Binary(checksumBytes).toString();
if (newChecksum.equals(checksum)) {
// decompress the bytes
Inflater decompresser = new Inflater(true);
decompresser.setInput(decryptedData);
decompressData = new byte[manifestEntry.getSize()];
decompresser.inflate(decompressData);
decompresser.end();
} else {
throw new OdfDecryptedException("The given password is wrong, please check it.");
}
} catch (Exception e) {
Logger.getLogger(OdfPackage.class.getName()).log(Level.SEVERE, null, e);
}
return decompressData;
}
// derive PBKDF2Key (reference http://www.ietf.org/rfc/rfc2898.txt)
byte[] derivePBKDF2Key(byte[] password, byte[] salt, int iterationCount, int keyLength)
throws NoSuchAlgorithmException, InvalidKeyException {
SecretKeySpec keyspec = new SecretKeySpec(password, "HmacSHA1");
Mac hmac = Mac.getInstance("HmacSHA1");
hmac.init(keyspec);
// length in octets of HmacSHA1 function output, a positive integer
int hmacLen = hmac.getMacLength();
// let l be the number of hLen-octet blocks in the derived key, rounding
// up,
// l = CEIL (dkLen / hLen) Here, CEIL (x) is the smallest integer
// greater than, or equal to, x.
int l = (keyLength % hmacLen > 0) ? (keyLength / hmacLen + 1) : (keyLength / hmacLen);
// let r be the number of octets in the last block: r = dkLen - (l - 1)
// * hLen .
int r = keyLength - (l - 1) * hmacLen;
byte T[] = new byte[l * hmacLen];
int offset = 0;
// For each block of the derived key apply the function F defined below
// to the password P, the salt S, the iteration count c, and
// the block index to compute the block:
for (int i = 1; i <= l; i++) {
byte Ur[] = new byte[hmacLen];
byte Ui[] = new byte[salt.length + 4];
System.arraycopy(salt, 0, Ui, 0, salt.length);
// Here, INT (i) is a four-octet encoding of the integer i, most
// significant octet first.
Ui[salt.length + 0] = (byte) (i >>> 24);
Ui[salt.length + 1] = (byte) (i >>> 16);
Ui[salt.length + 2] = (byte) (i >>> 8);
Ui[salt.length + 3] = (byte) (i);
// U_1 \xor U_2 \xor ... \xor U_c
for (int j = 0; j < iterationCount; j++) {
Ui = hmac.doFinal(Ui);
// XOR
for (int k = 0; k < T.length; k++) {
Ur[k] ^= Ui[k];
}
}
System.arraycopy(Ur, 0, T, offset, hmacLen);
offset += hmacLen;
}
if (r < hmacLen) {
byte DK[] = new byte[keyLength];
System.arraycopy(T, 0, DK, 0, keyLength);
return DK;
}
return T;
}
// Serializes a DOM tree into a byte array.
// Providing the counterpart of the generic Namespace handling of
// OdfFileDom.
private byte[] flushDom(Document dom) {
// if it is one of our DOM files we may flush all collected namespaces
// to the root element
if (dom instanceof OdfFileDom) {
OdfFileDom odfDom = (OdfFileDom) dom;
Map<String, String> nsByUri = odfDom.getMapNamespacePrefixByUri();
OdfElement root = odfDom.getRootElement();
if (root != null) {
for (Entry<String, String> entry : nsByUri.entrySet()) {
root.setAttributeNS(
"http://www.w3.org/2000/xmlns/", "xmlns:" + entry.getValue(), entry.getKey());
}
}
}
ByteArrayOutputStream baos = new ByteArrayOutputStream();
DOMXSImplementationSourceImpl dis = new org.apache.xerces.dom.DOMXSImplementationSourceImpl();
DOMImplementationLS impl = (DOMImplementationLS) dis.getDOMImplementation("LS");
LSSerializer writer = impl.createLSSerializer();
LSOutput output = impl.createLSOutput();
output.setByteStream(baos);
writer.write(dom, output);
return baos.toByteArray();
}
/**
* Get the latest version of package content as InputStream, as it would be saved. This might not
* be the original version once loaded from the package.
*
* @param internalPath of the desired stream.
* @return Inputstream of the ODF file within the package for the given path.
*/
public InputStream getInputStream(String internalPath) {
internalPath = normalizeFilePath(internalPath);
// else we always cache here and return a ByteArrayInputStream because
// if
// we would return ZipFile getInputStream(entry) we would not be
// able to read 2 Entries at the same time. This is a limitation of the
// ZipFile class.
// As it would be quite a common thing to read the content.xml and the
// styles.xml
// simultanously when using XSLT on OdfPackages we want to circumvent
// this limitation
byte[] data = getBytes(internalPath);
if (data != null && data.length != 0) {
ByteArrayInputStream bais = new ByteArrayInputStream(data);
return bais;
}
return null;
}
/**
* Get the latest version of package content as InputStream, as it would be saved. This might not
* be the original version once loaded from the package.
*
* @param internalPath of the desired stream.
* @param useOriginal true uses the stream as loaded from the ZIP. False will return even modified
* file content as a stream.
* @return Inputstream of the ODF file within the package for the given path.
*/
public InputStream getInputStream(String internalPath, boolean useOriginal) {
InputStream stream = null;
if (useOriginal) {
ZipArchiveEntry entry = mOriginalZipEntries.get(internalPath);
if (entry != null) {
try {
stream = mZipFile.getInputStream(entry);
} catch (IOException ex) {
// Catching IOException here should be fine: in-memory operations only
Logger.getLogger(OdfPackage.class.getName()).log(Level.SEVERE, null, ex);
}
}
} else {
stream = getInputStream(internalPath);
}
return stream;
}
/**
* Gets the InputStream containing whole OdfPackage.
*
* @return the ODF package as input stream
* @throws java.io.IOException - if the package could not be read
*/
public InputStream getInputStream() throws IOException, SAXException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
save(out, mBaseURI);
return new ByteArrayInputStream(out.toByteArray());
}
/**
* Insert the OutputStream for into OdfPackage. An existing file will be replaced.
*
* @param internalPath - relative documentURL where the DOM tree should be inserted as XML file
* @return outputstream for the data of the file to be stored in package
* @throws java.lang.Exception when the DOM tree could not be inserted
*/
public OutputStream insertOutputStream(String internalPath) throws Exception {
return insertOutputStream(internalPath, null);
}
/**
* Insert the OutputStream - to be filled after method - when stream is closed into OdfPackage. An
* existing file will be replaced.
*
* @param internalPath - relative documentURL where the DOM tree should be inserted as XML file
* @param mediaType - media type of stream
* @return outputstream for the data of the file to be stored in package
* @throws java.io.IOException when the DOM tree could not be inserted
*/
public OutputStream insertOutputStream(String internalPath, String mediaType) throws IOException {
internalPath = normalizeFilePath(internalPath);
final String fPath = internalPath;
final OdfFileEntry fFileEntry = getFileEntry(internalPath);
final String fMediaType = mediaType;
ByteArrayOutputStream baos =
new ByteArrayOutputStream() {
@Override
public void close() throws IOException {
byte[] data = this.toByteArray();
if (fMediaType == null || fMediaType.length() == 0) {
insert(data, fPath, fFileEntry == null ? null : fFileEntry.getMediaTypeString());
} else {
insert(data, fPath, fMediaType);
}
super.close();
}
};
return baos;
}
/**
* Removes a single file from the package.
*
* @param internalPath of the file relative to the package root
*/
public void remove(String internalPath) {
internalPath = normalizePath(internalPath);
if (mZipEntries != null && mZipEntries.containsKey(internalPath)) {
mZipEntries.remove(internalPath);
}
if (mManifestEntries != null && mManifestEntries.containsKey(internalPath)) {
// remove from the manifest Map
OdfFileEntry manifestEntry = mManifestEntries.remove(internalPath);
// remove from the manifest DOM
FileEntryElement manifestEle = manifestEntry.getOdfElement();
manifestEle.getParentNode().removeChild(manifestEle);
}
}
/**
* Get the size of an internal file from the package.
*
* @param internalPath of the file relative to the package root
* @return the size of the file in bytes or -1 if the size could not be received.
*/
public long getSize(String internalPath) {
long size = -1;
internalPath = normalizePath(internalPath);
if (mZipEntries != null && mZipEntries.containsKey(internalPath)) {
ZipArchiveEntry zipEntry = mZipEntries.get(internalPath);
size = zipEntry.getSize();
}
return size;
}
/** Encoded XML Attributes */
private String encodeXMLAttributes(String attributeValue) {
String encodedValue = QUOTATION_PATTERN.matcher(attributeValue).replaceAll(ENCODED_QUOTATION);
encodedValue = APOSTROPHE_PATTERN.matcher(encodedValue).replaceAll(ENCODED_APOSTROPHE);
return encodedValue;
}
/**
* Get EntityResolver to be used in XML Parsers which can resolve content inside the OdfPackage
*
* @return a SAX EntityResolver
*/
public EntityResolver getEntityResolver() {
if (mResolver == null) {
mResolver = new Resolver(this);
}
return mResolver;
}
/**
* Get URIResolver to be used in XSL Transformations which can resolve content inside the
* OdfPackage
*
* @return a TraX Resolver
*/
public URIResolver getURIResolver() {
if (mResolver == null) {
mResolver = new Resolver(this);
}
return mResolver;
}
private static String getBaseURLFromFile(File file) throws IOException {
String baseURL = Util.toExternalForm(file.getCanonicalFile().toURI());
baseURL = BACK_SLASH_PATTERN.matcher(baseURL).replaceAll(SLASH);
return baseURL;
}
/**
* Ensures that the given file path is not null nor empty and not an external reference
*
* <ol>
* <li>All backslashes "\" are exchanged by slashes "/"
* <li>Any substring "/../", "/./" or "//" will be removed
* <li>A prefix "./" and "../" will be removed
* </ol>
*
* @throws IllegalArgumentException If the path is NULL, empty or an external path (e.g. starting
* with "../" is given). None relative URLs will NOT throw an exception.
* @return the normalized path or the URL
*/
static String normalizeFilePath(String internalPath) {
if (internalPath.equals(EMPTY_STRING)) {
return SLASH;
} else {
return normalizePath(internalPath);
}
}
/**
* Ensures the given directory path is not null nor an external reference to resources outside the
* package. An empty path and slash "/" are both mapped to the root directory/document. NOTE:
* Although ODF only refer the "/" as root, the empty path aligns more adequate with the file
* system concept. To ensure the given directory path within the package can be used as a key (is
* unique for the Package) the path will be normalized.
*
* @see #normalizeFilePath(String) In addition to the file path normalization a trailing slash
* will be used for directories.
*/
static String normalizeDirectoryPath(String directoryPath) {
directoryPath = normalizePath(directoryPath);
// if not the root document - which is from ODF view a '/' and no
// trailing '/'
if (!directoryPath.equals(OdfPackageDocument.ROOT_DOCUMENT_PATH)) {
if (!directoryPath.endsWith(SLASH)) {
// add a trailing slash
directoryPath = directoryPath + SLASH;
}
if (directoryPath.startsWith(SLASH) && !directoryPath.equals(SLASH)) {
directoryPath = directoryPath.substring(1);
}
}
return directoryPath;
}
/** 1 Normalizes both directory and file path */
static String normalizePath(String path) {
if (path == null) {
String errMsg = "The internalPath given by parameter is NULL!";
Logger.getLogger(OdfPackage.class.getName()).severe(errMsg);
throw new IllegalArgumentException(errMsg);
} else if (!mightBeExternalReference(path)) {
if (path.equals(EMPTY_STRING)) {
path = SLASH;
} else {
// exchange all backslash "\" with a slash "/"
if (path.indexOf('\\') != -1) {
path = BACK_SLASH_PATTERN.matcher(path).replaceAll(SLASH);
}
// exchange all double slash "//" with a slash "/"
while (path.indexOf("//") != -1) {
path = DOUBLE_SLASH_PATTERN.matcher(path).replaceAll(SLASH);
}
// if directory replacements (e.g. ..) exist, resolve and remove
// them
if (path.indexOf("/.") != -1 || path.indexOf("./") != -1) {
path = removeChangeDirectories(path);
}
if (path.startsWith(SLASH) && !path.equals(SLASH)) {
path = path.substring(1);
}
}
}
return path;
}
/** Normalizes both directory and file path */
private static boolean mightBeExternalReference(String internalPath) {
boolean isExternalReference = false;
// if the fileReference is a external relative documentURL..
if (internalPath.startsWith(DOUBLE_DOT)
|| // or absolute documentURL
// AND not root document
internalPath.startsWith(SLASH) && !internalPath.equals(SLASH)
|| // or
// absolute
// IRI
internalPath.contains(COLON)) {
isExternalReference = true;
}
return isExternalReference;
}
/** Resolving the directory replacements (ie. "/../" and "/./") with a slash "/" */
private static String removeChangeDirectories(String path) {
boolean isDirectory = path.endsWith(SLASH);
StringTokenizer tokenizer = new StringTokenizer(path, SLASH);
int tokenCount = tokenizer.countTokens();
List<String> tokenList = new ArrayList<String>(tokenCount);
// add all paths to a list
while (tokenizer.hasMoreTokens()) {
String token = tokenizer.nextToken();
tokenList.add(token);
}
if (!isDirectory) {
String lastPath = tokenList.get(tokenCount - 1);
if (lastPath.equals(DOT) || lastPath.equals(DOUBLE_DOT)) {
isDirectory = true;
}
}
String currentToken;
int removeDirLevel = 0;
StringBuilder out = new StringBuilder();
// work on the list from back to front
for (int i = tokenCount - 1; i >= 0; i--) {
currentToken = tokenList.get(i);
// every ".." will remove an upcoming path
if (currentToken.equals(DOUBLE_DOT)) {
removeDirLevel++;
} else if (currentToken.equals(DOT)) {
} else // if a path have to be remove, neglect current path
{
if (removeDirLevel > 0) {
removeDirLevel--;
} else {
// add the path segment
out.insert(0, SLASH);
out.insert(0, currentToken);
}
}
}
if (removeDirLevel > 0) {
return EMPTY_STRING;
} else {
if (!isDirectory) {
// remove trailing slash /
out.deleteCharAt(out.length() - 1);
}
return out.toString();
}
}
/**
* Checks if the given reference is a reference, which points outside the ODF package
*
* @param internalPath the file reference to be checked
* @return true if the reference is an package external reference
*/
public static boolean isExternalReference(String internalPath) {
if (mightBeExternalReference(internalPath)) {
return true;
} else {
return mightBeExternalReference(normalizePath(internalPath));
}
}
/**
* Allow an application to register an error event handler.
*
* <p>If the application does not register an error handler, all error events reported by the
* ODFDOM (e.g. the SAX Parser) will be silently ignored; however, normal processing may not
* continue. It is highly recommended that all ODF applications implement an error handler to
* avoid unexpected bugs.
*
* <p>Applications may register a new or different handler in the middle of a parse, and the
* ODFDOM will begin using the new handler immediately.
*
* @param handler The error handler.
* @see #getErrorHandler
*/
public void setErrorHandler(ErrorHandler handler) {
mErrorHandler = handler;
}
/**
* Return the current error handler used for ODF validation.
*
* @return The current error handler, or null if none has been registered and validation is
* disabled.
* @see #setErrorHandler
*/
public ErrorHandler getErrorHandler() {
return mErrorHandler;
}
void logValidationWarning(ValidationConstraint constraint, String baseURI, Object... o)
throws SAXException {
if (mErrorHandler == null) {
return;
}
int varCount = 0;
if (o != null) {
varCount = o.length;
}
switch (varCount) {
case 0:
mErrorHandler.warning(new OdfValidationException(constraint, baseURI, o));
break;
case 1:
mErrorHandler.warning(new OdfValidationException(constraint, baseURI, o[0]));
break;
case 2:
mErrorHandler.warning(new OdfValidationException(constraint, baseURI, o[0], o[1]));
break;
}
}
void logValidationError(ValidationConstraint constraint, String baseURI, Object... o)
throws SAXException {
if (mErrorHandler == null) {
return;
}
int varCount = 0;
if (o != null) {
varCount = o.length;
}
switch (varCount) {
case 0:
mErrorHandler.error(new OdfValidationException(constraint, baseURI, o));
break;
case 1:
mErrorHandler.error(new OdfValidationException(constraint, baseURI, o[0]));
break;
case 2:
mErrorHandler.error(new OdfValidationException(constraint, baseURI, o[0], o[1]));
break;
}
}
/** @param odfVersion parsed from the manifest */
void setManifestVersion(String odfVersion) {
mManifestVersion = odfVersion;
}
/**
* @return the ODF version found in the manifest. Meant to be used to reuse when the manifest is
* recreated
*/
String getManifestVersion() {
return mManifestVersion;
}
/**
* @return counter for ids that are not allowed to be saved (otherwise it is not guaranteed that
* this id is unique)
*/
public String getNextMarkupId() {
return Integer.toString(mTransientMarkupId++);
}
}