1 /**
  2  * Copyright (C) 2012 KO GmbH <copyright@kogmbh.com>
  3  *
  4  * @licstart
  5  * This file is part of WebODF.
  6  *
  7  * WebODF is free software: you can redistribute it and/or modify it
  8  * under the terms of the GNU Affero General Public License (GNU AGPL)
  9  * as published by the Free Software Foundation, either version 3 of
 10  * the License, or (at your option) any later version.
 11  *
 12  * WebODF is distributed in the hope that it will be useful, but
 13  * WITHOUT ANY WARRANTY; without even the implied warranty of
 14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15  * GNU Affero General Public License for more details.
 16  *
 17  * You should have received a copy of the GNU Affero General Public License
 18  * along with WebODF.  If not, see <http://www.gnu.org/licenses/>.
 19  * @licend
 20  *
 21  * @source: http://www.webodf.org/
 22  * @source: https://github.com/kogmbh/WebODF/
 23  */
 24 
 25 /*global runtime, core, DOMParser, externs*/
 26 /*jslint bitwise: true*/
 27 
 28 /**
 29  * @constructor
 30  * @param {!string} url path to zip file, should be readable by the runtime
 31  * @param {?function(?string, !core.Zip):undefined} entriesReadCallback callback
 32  *        indicating the zip
 33  *        has loaded this list of entries, the arguments are a string that
 34  *        indicates error if present and the created object
 35  */
 36 core.Zip = function Zip(url, entriesReadCallback) {
 37     "use strict";
 38     var /**@type{!core.Zip}*/
 39         self = this,
 40         /**@type{!JSZip}*/
 41         zip,
 42         base64 = new core.Base64();
 43 
 44     /**
 45      * @param {!string} filename
 46      * @param {!function(?string, ?Uint8Array)} callback receiving err and data
 47      * @return {undefined}
 48      */
 49     function load(filename, callback) {
 50         var entry = zip.file(filename);
 51         if (entry) {
 52             callback(null, entry.asUint8Array());
 53         } else {
 54             callback(filename + " not found.", null);
 55         }
 56     }
 57     /**
 58      * @param {!string} filename
 59      * @param {!function(?string, ?string):undefined} callback receiving err and data
 60      * @return {undefined}
 61      */
 62     function loadAsString(filename, callback) {
 63         // the javascript implementation simply reads the file and converts to
 64         // string
 65         load(filename, function (err, data) {
 66             if (err || data === null) {
 67                 return callback(err, null);
 68             }
 69             var d = runtime.byteArrayToString(data, "utf8");
 70             callback(null, d);
 71         });
 72     }
 73     /**
 74      * @param {!string} filename
 75      * @param {!{rootElementReady: function(?string, ?string=, boolean=):undefined}} handler
 76      * @return {undefined}
 77      */
 78     function loadContentXmlAsFragments(filename, handler) {
 79         // the javascript implementation simply reads the file
 80         loadAsString(filename, function (err, data) {
 81             if (err) {
 82                 return handler.rootElementReady(err);
 83             }
 84             handler.rootElementReady(null, data, true);
 85         });
 86     }
 87     /**
 88      * @param {!string} filename
 89      * @param {!string} mimetype
 90      * @param {!function(?string,?string):undefined} callback
 91      */
 92     function loadAsDataURL(filename, mimetype, callback) {
 93         load(filename, function (err, data) {
 94             if (err || !data) {
 95                 return callback(err, null);
 96             }
 97             var /**@const@type{!Uint8Array}*/p = data,
 98                 chunksize = 45000, // must be multiple of 3 and less than 50000
 99                 i = 0,
100                 dataurl;
101             if (!mimetype) {
102                 if (p[1] === 0x50 && p[2] === 0x4E && p[3] === 0x47) {
103                     mimetype = "image/png";
104                 } else if (p[0] === 0xFF && p[1] === 0xD8 && p[2] === 0xFF) {
105                     mimetype = "image/jpeg";
106                 } else if (p[0] === 0x47 && p[1] === 0x49 && p[2] === 0x46) {
107                     mimetype = "image/gif";
108                 } else {
109                     mimetype = "";
110                 }
111             }
112             dataurl = 'data:' + mimetype + ';base64,';
113             // to avoid exceptions, base64 encoding is done in chunks
114             // it would make sense to move this to base64.toBase64
115             while (i < data.length) {
116                 dataurl += base64.convertUTF8ArrayToBase64(
117                     p.subarray(i, Math.min(i + chunksize, p.length))
118                 );
119                 i += chunksize;
120             }
121             callback(null, dataurl);
122         });
123     }
124     /**
125      * @param {!string} filename
126      * @param {function(?string,?Document):undefined} callback
127      * @return {undefined}
128      */
129     function loadAsDOM(filename, callback) {
130         loadAsString(filename, function (err, xmldata) {
131             if (err || xmldata === null) {
132                 callback(err, null);
133                 return;
134             }
135             var parser = new DOMParser(),
136                 dom = parser.parseFromString(xmldata, "text/xml");
137             callback(null, dom);
138         });
139     }
140     /**
141      * Add or replace an entry to the zip file.
142      * This data is not stored to disk yet, and therefore, no callback is
143      * necessary.
144      * @param {!string} filename
145      * @param {!Uint8Array} data
146      * @param {!boolean} compressed
147      * @param {!Date} date
148      * @return {undefined}
149      */
150     function save(filename, data, compressed, date) {
151         zip.file(filename, data, {date: date, compression: compressed ? null : "STORE"});
152     }
153     /**
154      * Removes entry from the zip.
155      * @param {!string} filename
156      * @return {!boolean} return false if entry is not found; otherwise true.
157      */
158     function remove(filename) {
159         var exists = zip.file(filename) !== null;
160         zip.remove(filename);
161         return exists;
162     }
163     /**
164      * Create a bytearray from the zipfile.
165      * @param {!function(!Uint8Array):undefined} successCallback receiving zip as bytearray
166      * @param {!function(?string):undefined} errorCallback receiving possible err
167      * @return {undefined}
168      */
169     function createByteArray(successCallback, errorCallback) {
170         try {
171             successCallback(/**@type{!Uint8Array}*/(zip.generate({type: "uint8array", compression: "DEFLATE"})));
172         } catch(/**@type{!Error}*/e) {
173             errorCallback(e.message);
174         }
175     }
176     /**
177      * Write the zipfile to the given path.
178      * @param {!string} newurl
179      * @param {!function(?string):undefined} callback receiving possible err
180      * @return {undefined}
181      */
182     function writeAs(newurl, callback) {
183         createByteArray(function (data) {
184             runtime.writeFile(newurl, data, callback);
185         }, callback);
186     }
187     /**
188      * Write the zipfile to the given path.
189      * @param {!function(?string):undefined} callback receiving possible err
190      * @return {undefined}
191      */
192     function write(callback) {
193         writeAs(url, callback);
194     }
195     this.load = load;
196     this.save = save;
197     this.remove = remove;
198     this.write = write;
199     this.writeAs = writeAs;
200     this.createByteArray = createByteArray;
201     // a special function that makes faster odf loading possible
202     this.loadContentXmlAsFragments = loadContentXmlAsFragments;
203     this.loadAsString = loadAsString;
204     this.loadAsDOM = loadAsDOM;
205     this.loadAsDataURL = loadAsDataURL;
206 
207     /**
208      * @return {!Array.<!{filename: !string}>}
209      */
210     this.getEntries = function () {
211         return Object.keys(zip.files).map(function(filename) { return { filename: filename }; });
212     };
213 
214     zip = new externs.JSZip();
215     // if no callback is defined, this is a new file
216     if (entriesReadCallback === null) {
217         return;
218     }
219     runtime.readFile(url, "binary", function (err, result) {
220         if (typeof result === "string") {
221             err = "file was read as a string. Should be Uint8Array.";
222         }
223         if (err || !result || result.length === 0) {
224             entriesReadCallback("File '" + url + "' cannot be read. Err: " + (err || "[none]"), self);
225         } else {
226             try {
227                 // CRC32 check disabled to improve performance
228                 zip.load(/**@type{!Uint8Array}*/(result), { checkCRC32: false });
229                 entriesReadCallback(null, self);
230             } catch (/**@type{!Error}*/e) {
231                 entriesReadCallback(e.message, self);
232             }
233         }
234     });
235 };
236