You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

EmbeddedExtractor.java 16KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425
  1. /* ====================================================================
  2. Licensed to the Apache Software Foundation (ASF) under one or more
  3. contributor license agreements. See the NOTICE file distributed with
  4. this work for additional information regarding copyright ownership.
  5. The ASF licenses this file to You under the Apache License, Version 2.0
  6. (the "License"); you may not use this file except in compliance with
  7. the License. You may obtain a copy of the License at
  8. http://www.apache.org/licenses/LICENSE-2.0
  9. Unless required by applicable law or agreed to in writing, software
  10. distributed under the License is distributed on an "AS IS" BASIS,
  11. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. See the License for the specific language governing permissions and
  13. limitations under the License.
  14. ==================================================================== */
  15. package org.apache.poi.ss.extractor;
  16. import static org.apache.poi.util.StringUtil.endsWithIgnoreCase;
  17. import java.io.ByteArrayOutputStream;
  18. import java.io.IOException;
  19. import java.io.InputStream;
  20. import java.util.ArrayList;
  21. import java.util.Arrays;
  22. import java.util.Collections;
  23. import java.util.Iterator;
  24. import java.util.List;
  25. import org.apache.poi.hpsf.ClassID;
  26. import org.apache.poi.poifs.filesystem.DirectoryNode;
  27. import org.apache.poi.poifs.filesystem.DocumentInputStream;
  28. import org.apache.poi.poifs.filesystem.Entry;
  29. import org.apache.poi.poifs.filesystem.Ole10Native;
  30. import org.apache.poi.poifs.filesystem.Ole10NativeException;
  31. import org.apache.poi.poifs.filesystem.POIFSFileSystem;
  32. import org.apache.poi.ss.usermodel.Drawing;
  33. import org.apache.poi.ss.usermodel.ObjectData;
  34. import org.apache.poi.ss.usermodel.Picture;
  35. import org.apache.poi.ss.usermodel.PictureData;
  36. import org.apache.poi.ss.usermodel.Shape;
  37. import org.apache.poi.ss.usermodel.ShapeContainer;
  38. import org.apache.poi.ss.usermodel.Sheet;
  39. import org.apache.poi.ss.usermodel.Workbook;
  40. import org.apache.poi.util.Beta;
  41. import org.apache.poi.util.IOUtils;
  42. import org.apache.poi.util.LocaleUtil;
  43. import org.apache.poi.util.POILogFactory;
  44. import org.apache.poi.util.POILogger;
  45. import org.apache.poi.xssf.usermodel.XSSFObjectData;
  46. /**
  47. * This extractor class tries to identify various embedded documents within Excel files
  48. * and provide them via a common interface, i.e. the EmbeddedData instances
  49. */
  50. @Beta
  51. public class EmbeddedExtractor implements Iterable<EmbeddedExtractor> {
  52. private static final POILogger LOG = POILogFactory.getLogger(EmbeddedExtractor.class);
  53. // contentType
  54. private static final String CONTENT_TYPE_BYTES = "binary/octet-stream";
  55. private static final String CONTENT_TYPE_PDF = "application/pdf";
  56. private static final String CONTENT_TYPE_DOC = "application/msword";
  57. private static final String CONTENT_TYPE_XLS = "application/vnd.ms-excel";
  58. /**
  59. * @return the list of known extractors, if you provide custom extractors, override this method
  60. */
  61. @Override
  62. public Iterator<EmbeddedExtractor> iterator() {
  63. EmbeddedExtractor[] ee = {
  64. new Ole10Extractor(), new PdfExtractor(), new BiffExtractor(), new OOXMLExtractor(), new FsExtractor()
  65. };
  66. return Arrays.asList(ee).iterator();
  67. }
  68. public EmbeddedData extractOne(DirectoryNode src) throws IOException {
  69. for (EmbeddedExtractor ee : this) {
  70. if (ee.canExtract(src)) {
  71. return ee.extract(src);
  72. }
  73. }
  74. return null;
  75. }
  76. public EmbeddedData extractOne(Picture src) throws IOException {
  77. for (EmbeddedExtractor ee : this) {
  78. if (ee.canExtract(src)) {
  79. return ee.extract(src);
  80. }
  81. }
  82. return null;
  83. }
  84. public List<EmbeddedData> extractAll(Sheet sheet) throws IOException {
  85. Drawing<?> patriarch = sheet.getDrawingPatriarch();
  86. if (null == patriarch){
  87. return Collections.emptyList();
  88. }
  89. List<EmbeddedData> embeddings = new ArrayList<EmbeddedData>();
  90. extractAll(patriarch, embeddings);
  91. return embeddings;
  92. }
  93. protected void extractAll(ShapeContainer<?> parent, List<EmbeddedData> embeddings) throws IOException {
  94. for (Shape shape : parent) {
  95. EmbeddedData data = null;
  96. if (shape instanceof ObjectData) {
  97. ObjectData od = (ObjectData)shape;
  98. try {
  99. if (od.hasDirectoryEntry()) {
  100. data = extractOne((DirectoryNode)od.getDirectory());
  101. } else {
  102. String contentType = CONTENT_TYPE_BYTES;
  103. if (od instanceof XSSFObjectData) {
  104. contentType = ((XSSFObjectData)od).getObjectPart().getContentType();
  105. }
  106. data = new EmbeddedData(od.getFileName(), od.getObjectData(), contentType);
  107. }
  108. } catch (Exception e) {
  109. LOG.log(POILogger.WARN, "Entry not found / readable - ignoring OLE embedding", e);
  110. }
  111. } else if (shape instanceof Picture) {
  112. data = extractOne((Picture)shape);
  113. } else if (shape instanceof ShapeContainer) {
  114. extractAll((ShapeContainer<?>)shape, embeddings);
  115. }
  116. if (data == null) {
  117. continue;
  118. }
  119. data.setShape(shape);
  120. String filename = data.getFilename();
  121. String extension = (filename == null || filename.lastIndexOf('.') == -1) ? ".bin" : filename.substring(filename.lastIndexOf('.'));
  122. // try to find an alternative name
  123. if (filename == null || "".equals(filename) || filename.startsWith("MBD") || filename.startsWith("Root Entry")) {
  124. filename = shape.getShapeName();
  125. if (filename != null) {
  126. filename += extension;
  127. }
  128. }
  129. // default to dummy name
  130. if (filename == null || "".equals(filename)) {
  131. filename = "picture_" + embeddings.size() + extension;
  132. }
  133. filename = filename.trim();
  134. data.setFilename(filename);
  135. embeddings.add(data);
  136. }
  137. }
  138. public boolean canExtract(DirectoryNode source) {
  139. return false;
  140. }
  141. public boolean canExtract(Picture source) {
  142. return false;
  143. }
  144. protected EmbeddedData extract(DirectoryNode dn) throws IOException {
  145. assert(canExtract(dn));
  146. POIFSFileSystem dest = new POIFSFileSystem();
  147. copyNodes(dn, dest.getRoot());
  148. // start with a reasonable big size
  149. ByteArrayOutputStream bos = new ByteArrayOutputStream(20000);
  150. dest.writeFilesystem(bos);
  151. dest.close();
  152. return new EmbeddedData(dn.getName(), bos.toByteArray(), CONTENT_TYPE_BYTES);
  153. }
  154. protected EmbeddedData extract(Picture source) throws IOException {
  155. return null;
  156. }
  157. public static class Ole10Extractor extends EmbeddedExtractor {
  158. @Override
  159. public boolean canExtract(DirectoryNode dn) {
  160. ClassID clsId = dn.getStorageClsid();
  161. return ClassID.OLE10_PACKAGE.equals(clsId);
  162. }
  163. @Override
  164. public EmbeddedData extract(DirectoryNode dn) throws IOException {
  165. try {
  166. // TODO: inspect the CompObj record for more details, i.e. the content type
  167. Ole10Native ole10 = Ole10Native.createFromEmbeddedOleObject(dn);
  168. return new EmbeddedData(ole10.getFileName(), ole10.getDataBuffer(), CONTENT_TYPE_BYTES);
  169. } catch (Ole10NativeException e) {
  170. throw new IOException(e);
  171. }
  172. }
  173. }
  174. static class PdfExtractor extends EmbeddedExtractor {
  175. static ClassID PdfClassID = new ClassID("{B801CA65-A1FC-11D0-85AD-444553540000}");
  176. @Override
  177. public boolean canExtract(DirectoryNode dn) {
  178. ClassID clsId = dn.getStorageClsid();
  179. return (PdfClassID.equals(clsId)
  180. || dn.hasEntry("CONTENTS"));
  181. }
  182. @Override
  183. public EmbeddedData extract(DirectoryNode dn) throws IOException {
  184. ByteArrayOutputStream bos = new ByteArrayOutputStream();
  185. InputStream is = dn.createDocumentInputStream("CONTENTS");
  186. IOUtils.copy(is, bos);
  187. is.close();
  188. return new EmbeddedData(dn.getName() + ".pdf", bos.toByteArray(), CONTENT_TYPE_PDF);
  189. }
  190. @Override
  191. public boolean canExtract(Picture source) {
  192. PictureData pd = source.getPictureData();
  193. return (pd != null && pd.getPictureType() == Workbook.PICTURE_TYPE_EMF);
  194. }
  195. /**
  196. * Mac Office encodes embedded objects inside the picture, e.g. PDF is part of an EMF.
  197. * If an embedded stream is inside an EMF picture, this method extracts the payload.
  198. *
  199. * @return the embedded data in an EMF picture or null if none is found
  200. */
  201. @Override
  202. protected EmbeddedData extract(Picture source) throws IOException {
  203. // check for emf+ embedded pdf (poor mans style :( )
  204. // Mac Excel 2011 embeds pdf files with this method.
  205. PictureData pd = source.getPictureData();
  206. if (pd != null && pd.getPictureType() != Workbook.PICTURE_TYPE_EMF) {
  207. return null;
  208. }
  209. // TODO: investigate if this is just an EMF-hack or if other formats are also embedded in EMF
  210. byte pictureBytes[] = pd.getData();
  211. int idxStart = indexOf(pictureBytes, 0, "%PDF-".getBytes(LocaleUtil.CHARSET_1252));
  212. if (idxStart == -1) {
  213. return null;
  214. }
  215. int idxEnd = indexOf(pictureBytes, idxStart, "%%EOF".getBytes(LocaleUtil.CHARSET_1252));
  216. if (idxEnd == -1) {
  217. return null;
  218. }
  219. int pictureBytesLen = idxEnd-idxStart+6;
  220. byte[] pdfBytes = new byte[pictureBytesLen];
  221. System.arraycopy(pictureBytes, idxStart, pdfBytes, 0, pictureBytesLen);
  222. String filename = source.getShapeName().trim();
  223. if (!endsWithIgnoreCase(filename, ".pdf")) {
  224. filename += ".pdf";
  225. }
  226. return new EmbeddedData(filename, pdfBytes, CONTENT_TYPE_PDF);
  227. }
  228. }
  229. static class OOXMLExtractor extends EmbeddedExtractor {
  230. @Override
  231. public boolean canExtract(DirectoryNode dn) {
  232. return dn.hasEntry("package");
  233. }
  234. @Override
  235. public EmbeddedData extract(DirectoryNode dn) throws IOException {
  236. ClassID clsId = dn.getStorageClsid();
  237. String contentType, ext;
  238. if (ClassID.WORD2007.equals(clsId)) {
  239. ext = ".docx";
  240. contentType = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
  241. } else if (ClassID.WORD2007_MACRO.equals(clsId)) {
  242. ext = ".docm";
  243. contentType = "application/vnd.ms-word.document.macroEnabled.12";
  244. } else if (ClassID.EXCEL2007.equals(clsId) || ClassID.EXCEL2003.equals(clsId) || ClassID.EXCEL2010.equals(clsId)) {
  245. ext = ".xlsx";
  246. contentType = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
  247. } else if (ClassID.EXCEL2007_MACRO.equals(clsId)) {
  248. ext = ".xlsm";
  249. contentType = "application/vnd.ms-excel.sheet.macroEnabled.12";
  250. } else if (ClassID.EXCEL2007_XLSB.equals(clsId)) {
  251. ext = ".xlsb";
  252. contentType = "application/vnd.ms-excel.sheet.binary.macroEnabled.12";
  253. } else if (ClassID.POWERPOINT2007.equals(clsId)) {
  254. ext = ".pptx";
  255. contentType = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
  256. } else if (ClassID.POWERPOINT2007_MACRO.equals(clsId)) {
  257. ext = ".ppsm";
  258. contentType = "application/vnd.ms-powerpoint.slideshow.macroEnabled.12";
  259. } else {
  260. ext = ".zip";
  261. contentType = "application/zip";
  262. }
  263. DocumentInputStream dis = dn.createDocumentInputStream("package");
  264. byte data[] = IOUtils.toByteArray(dis);
  265. dis.close();
  266. return new EmbeddedData(dn.getName()+ext, data, contentType);
  267. }
  268. }
  269. static class BiffExtractor extends EmbeddedExtractor {
  270. @Override
  271. public boolean canExtract(DirectoryNode dn) {
  272. return canExtractExcel(dn) || canExtractWord(dn);
  273. }
  274. protected boolean canExtractExcel(DirectoryNode dn) {
  275. ClassID clsId = dn.getStorageClsid();
  276. return (ClassID.EXCEL95.equals(clsId)
  277. || ClassID.EXCEL97.equals(clsId)
  278. || dn.hasEntry("Workbook") /*...*/);
  279. }
  280. protected boolean canExtractWord(DirectoryNode dn) {
  281. ClassID clsId = dn.getStorageClsid();
  282. return (ClassID.WORD95.equals(clsId)
  283. || ClassID.WORD97.equals(clsId)
  284. || dn.hasEntry("WordDocument"));
  285. }
  286. @Override
  287. public EmbeddedData extract(DirectoryNode dn) throws IOException {
  288. EmbeddedData ed = super.extract(dn);
  289. if (canExtractExcel(dn)) {
  290. ed.setFilename(dn.getName() + ".xls");
  291. ed.setContentType(CONTENT_TYPE_XLS);
  292. } else if (canExtractWord(dn)) {
  293. ed.setFilename(dn.getName() + ".doc");
  294. ed.setContentType(CONTENT_TYPE_DOC);
  295. }
  296. return ed;
  297. }
  298. }
  299. static class FsExtractor extends EmbeddedExtractor {
  300. @Override
  301. public boolean canExtract(DirectoryNode dn) {
  302. return true;
  303. }
  304. @Override
  305. public EmbeddedData extract(DirectoryNode dn) throws IOException {
  306. EmbeddedData ed = super.extract(dn);
  307. ed.setFilename(dn.getName() + ".ole");
  308. // TODO: read the content type from CombObj stream
  309. return ed;
  310. }
  311. }
  312. protected static void copyNodes(DirectoryNode src, DirectoryNode dest) throws IOException {
  313. for (Entry e : src) {
  314. if (e instanceof DirectoryNode) {
  315. DirectoryNode srcDir = (DirectoryNode)e;
  316. DirectoryNode destDir = (DirectoryNode)dest.createDirectory(srcDir.getName());
  317. destDir.setStorageClsid(srcDir.getStorageClsid());
  318. copyNodes(srcDir, destDir);
  319. } else {
  320. InputStream is = src.createDocumentInputStream(e);
  321. try {
  322. dest.createDocument(e.getName(), is);
  323. } finally {
  324. is.close();
  325. }
  326. }
  327. }
  328. }
  329. /**
  330. * Knuth-Morris-Pratt Algorithm for Pattern Matching
  331. * Finds the first occurrence of the pattern in the text.
  332. */
  333. private static int indexOf(byte[] data, int offset, byte[] pattern) {
  334. int[] failure = computeFailure(pattern);
  335. int j = 0;
  336. if (data.length == 0) {
  337. return -1;
  338. }
  339. for (int i = offset; i < data.length; i++) {
  340. while (j > 0 && pattern[j] != data[i]) {
  341. j = failure[j - 1];
  342. }
  343. if (pattern[j] == data[i]) { j++; }
  344. if (j == pattern.length) {
  345. return i - pattern.length + 1;
  346. }
  347. }
  348. return -1;
  349. }
  350. /**
  351. * Computes the failure function using a boot-strapping process,
  352. * where the pattern is matched against itself.
  353. */
  354. private static int[] computeFailure(byte[] pattern) {
  355. int[] failure = new int[pattern.length];
  356. int j = 0;
  357. for (int i = 1; i < pattern.length; i++) {
  358. while (j > 0 && pattern[j] != pattern[i]) {
  359. j = failure[j - 1];
  360. }
  361. if (pattern[j] == pattern[i]) {
  362. j++;
  363. }
  364. failure[i] = j;
  365. }
  366. return failure;
  367. }
  368. }