1 /**
  2  * Copyright (C) 2012-2013 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 Node, NodeFilter, runtime, core, xmldom, odf, DOMParser, document, webodf */
 26 
 27 (function () {
 28     "use strict";
 29     var styleInfo = new odf.StyleInfo(),
 30         domUtils = core.DomUtils,
 31         /**@const
 32            @type{!string}*/
 33         officens = "urn:oasis:names:tc:opendocument:xmlns:office:1.0",
 34         /**@const
 35            @type{!string}*/
 36         manifestns = "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",
 37         /**@const
 38            @type{!string}*/
 39         webodfns = "urn:webodf:names:scope",
 40         /**@const
 41            @type{!string}*/
 42         stylens = odf.Namespaces.stylens,
 43         /**@const
 44            @type{!Array.<!string>}*/
 45         nodeorder = ['meta', 'settings', 'scripts', 'font-face-decls', 'styles',
 46             'automatic-styles', 'master-styles', 'body'],
 47         /**@const
 48            @type{!string}*/
 49         automaticStylePrefix = Date.now() + "_webodf_",
 50         base64 = new core.Base64(),
 51         /**@const
 52            @type{!string}*/
 53         documentStylesScope = "document-styles",
 54         /**@const
 55            @type{!string}*/
 56         documentContentScope = "document-content";
 57 
 58     /**
 59      * Return the position the node should get according to the ODF flat format.
 60      * @param {!Node} child
 61      * @return {!number}
 62      */
 63     function getNodePosition(child) {
 64         var i, l = nodeorder.length;
 65         for (i = 0; i < l; i += 1) {
 66             if (child.namespaceURI === officens &&
 67                     child.localName === nodeorder[i]) {
 68                 return i;
 69             }
 70         }
 71         return -1;
 72     }
 73     /**
 74      * Class that filters runtime specific nodes from the DOM.
 75      * Additionally all unused automatic styles are skipped, if a tree
 76      * of elements was passed to check the style usage in it.
 77      * @constructor
 78      * @implements {xmldom.LSSerializerFilter}
 79      * @param {!Element} styleUsingElementsRoot root element of tree of elements using styles
 80      * @param {?Element=} automaticStyles root element of the automatic style definition tree
 81      */
 82     function OdfStylesFilter(styleUsingElementsRoot, automaticStyles) {
 83         var usedStyleList = new styleInfo.UsedStyleList(styleUsingElementsRoot, automaticStyles),
 84             odfNodeFilter = new odf.OdfNodeFilter();
 85 
 86         /**
 87          * @param {!Node} node
 88          * @return {!number}
 89          */
 90         this.acceptNode = function (node) {
 91             var result = odfNodeFilter.acceptNode(node);
 92             if (result === NodeFilter.FILTER_ACCEPT
 93                     && node.parentNode === automaticStyles
 94                     && node.nodeType === Node.ELEMENT_NODE) {
 95                 // skip all automatic styles which are not used
 96                 if (usedStyleList.uses(/**@type{!Element}*/(node))) {
 97                     result = NodeFilter.FILTER_ACCEPT;
 98                 } else {
 99                     result = NodeFilter.FILTER_REJECT;
100                 }
101             }
102             return result;
103         };
104     }
105     /**
106      * Class that extends OdfStylesFilter
107      * Additionally, filter out ' ' within the <text:s> element and '\t' within the <text:tab> element
108      * @constructor
109      * @implements {xmldom.LSSerializerFilter}
110      * @param {!Element} styleUsingElementsRoot root element of tree of elements using styles
111      * @param {?Element=} automaticStyles root element of the automatic style definition tree
112      */
113     function OdfContentFilter(styleUsingElementsRoot, automaticStyles) {
114         var odfStylesFilter = new OdfStylesFilter(styleUsingElementsRoot, automaticStyles);
115 
116         /**
117          * @param {!Node} node
118          * @return {!number}
119          */
120         this.acceptNode = function (node) {
121             var result = odfStylesFilter.acceptNode(node);
122             if (result === NodeFilter.FILTER_ACCEPT
123                     && node.parentNode
124                     && node.parentNode.namespaceURI === odf.Namespaces.textns
125                     && (node.parentNode.localName === 's' || node.parentNode.localName === 'tab')) {
126                 result = NodeFilter.FILTER_REJECT;
127             }
128             return result;
129         };
130     }
131     /**
132      * Put the element at the right position in the parent.
133      * The right order is given by the value returned from getNodePosition.
134      * @param {!Node} node
135      * @param {?Node} child
136      * @return {undefined}
137      */
138     function setChild(node, child) {
139         if (!child) {
140             return;
141         }
142         var childpos = getNodePosition(child),
143             pos,
144             c = node.firstChild;
145         if (childpos === -1) {
146             return;
147         }
148         while (c) {
149             pos = getNodePosition(c);
150             if (pos !== -1 && pos > childpos) {
151                 break;
152             }
153             c = c.nextSibling;
154         }
155         node.insertBefore(child, c);
156     }
157     /*jslint emptyblock: true*/
158     /**
159      * A DOM element that is part of and ODF part of a DOM.
160      * @constructor
161      * @extends {Element}
162      */
163     odf.ODFElement = function ODFElement() {
164     };
165     /**
166      * The root element of an ODF document.
167      * @constructor
168      * @extends {odf.ODFElement}
169      */
170     odf.ODFDocumentElement = function ODFDocumentElement() {
171     };
172     /*jslint emptyblock: false*/
173     odf.ODFDocumentElement.prototype = new odf.ODFElement();
174     odf.ODFDocumentElement.prototype.constructor = odf.ODFDocumentElement;
175     /**
176      * Optional tag <office:automatic-styles/>
177      * If it is missing, it is created.
178      * @type {!Element}
179      */
180     odf.ODFDocumentElement.prototype.automaticStyles;
181     /**
182      * Required tag <office:body/>
183      * @type {!Element}
184      */
185     odf.ODFDocumentElement.prototype.body;
186     /**
187      * Optional tag <office:font-face-decls/>
188      * @type {Element}
189      */
190     odf.ODFDocumentElement.prototype.fontFaceDecls = null;
191     /**
192      * @type {Element}
193      */
194     odf.ODFDocumentElement.prototype.manifest = null;
195     /**
196      * Optional tag <office:master-styles/>
197      * If it is missing, it is created.
198      * @type {!Element}
199      */
200     odf.ODFDocumentElement.prototype.masterStyles;
201     /**
202      * Optional tag <office:meta/>
203      * @type {?Element}
204      */
205     odf.ODFDocumentElement.prototype.meta;
206     /**
207      * Optional tag <office:settings/>
208      * @type {Element}
209      */
210     odf.ODFDocumentElement.prototype.settings = null;
211     /**
212      * Optional tag <office:styles/>
213      * If it is missing, it is created.
214      * @type {!Element}
215      */
216     odf.ODFDocumentElement.prototype.styles;
217     odf.ODFDocumentElement.namespaceURI = officens;
218     odf.ODFDocumentElement.localName = 'document';
219 
220     /*jslint emptyblock: true*/
221     /**
222      * An element that also has a pointer to the optional annotation end
223      * @constructor
224      * @extends {odf.ODFElement}
225      */
226     odf.AnnotationElement = function AnnotationElement() {
227     };
228     /*jslint emptyblock: false*/
229 
230     /**
231     * @type {?Element}
232     */
233     odf.AnnotationElement.prototype.annotationEndElement;
234 
235     // private constructor
236     /**
237      * @constructor
238      * @param {string} name
239      * @param {string} mimetype
240      * @param {!odf.OdfContainer} container
241      * @param {core.Zip} zip
242      */
243     odf.OdfPart = function OdfPart(name, mimetype,  container, zip) {
244         var self = this;
245 
246         // declare public variables
247         this.size = 0;
248         this.type = null;
249         this.name = name;
250         this.container = container;
251         /**@type{?string}*/
252         this.url = null;
253         /**@type{string}*/
254         this.mimetype = mimetype;
255         this.document = null;
256         this.onstatereadychange = null;
257         /**@type{?function(!odf.OdfPart)}*/
258         this.onchange;
259         this.EMPTY = 0;
260         this.LOADING = 1;
261         this.DONE = 2;
262         this.state = this.EMPTY;
263         this.data = "";
264 
265         // private functions
266         // public functions
267         /**
268          * @return {undefined}
269          */
270         this.load = function () {
271             if (zip === null) {
272                 return;
273             }
274             this.mimetype = mimetype;
275             zip.loadAsDataURL(name, mimetype, function (err, url) {
276                 if (err) {
277                     runtime.log(err);
278                 }
279                 self.url = url;
280                 if (self.onchange) {
281                     self.onchange(self);
282                 }
283                 if (self.onstatereadychange) {
284                     self.onstatereadychange(self);
285                 }
286             });
287         };
288     };
289     /*jslint emptyblock: true*/
290     odf.OdfPart.prototype.load = function () {
291     };
292     /*jslint emptyblock: false*/
293     odf.OdfPart.prototype.getUrl = function () {
294         if (this.data) {
295             return 'data:;base64,' + base64.toBase64(this.data);
296         }
297         return null;
298     };
299     /**
300      * The OdfContainer class manages the various parts that constitues an ODF
301      * document.
302      * The constructor takes a url or a type. If urlOrType is a type, an empty
303      * document of that type is created. Otherwise, urlOrType is interpreted as
304      * a url and loaded from that url.
305      *
306      * @constructor
307      * @param {!string|!odf.OdfContainer.DocumentType} urlOrType
308      * @param {?function(!odf.OdfContainer)=} onstatereadychange
309      * @return {?}
310      */
311     odf.OdfContainer = function OdfContainer(urlOrType, onstatereadychange) {
312         var self = this,
313             /**@type {!core.Zip}*/
314             zip,
315             /**@type {!Object.<!string,!string>}*/
316             partMimetypes = {},
317             /**@type {?Element}*/
318             contentElement,
319             /**@type{!string}*/
320             url = "";
321 
322         // NOTE each instance of OdfContainer has a copy of the private functions
323         // it would be better to have a class OdfContainerPrivate where the
324         // private functions can be defined via OdfContainerPrivate.prototype
325         // without exposing them
326 
327         // declare public variables
328         this.onstatereadychange = onstatereadychange;
329         this.onchange = null;
330         this.state = null;
331         /**
332          * @type {!odf.ODFDocumentElement}
333          */
334         this.rootElement;
335 
336         /**
337          * @param {!Element} element
338          * @return {undefined}
339          */
340         function removeProcessingInstructions(element) {
341             var n = element.firstChild, next, e;
342             while (n) {
343                 next = n.nextSibling;
344                 if (n.nodeType === Node.ELEMENT_NODE) {
345                     e = /**@type{!Element}*/(n);
346                     removeProcessingInstructions(e);
347                 } else if (n.nodeType === Node.PROCESSING_INSTRUCTION_NODE) {
348                     element.removeChild(n);
349                 }
350                 n = next;
351             }
352         }
353 
354         // private functions
355         /**
356          * Iterates through the subtree of rootElement and adds annotation-end
357          * elements as direct properties of the corresponding annotation elements.
358          * Expects properly used annotation elements, does not try
359          * to do heuristic fixes or drop broken elements.
360          * @param {!Element} rootElement
361          * @return {undefined}
362          */
363         function linkAnnotationStartAndEndElements(rootElement) {
364             var document = rootElement.ownerDocument,
365                 /** @type {!Object.<!string,!Element>} */
366                 annotationStarts = {},
367                 n, name, annotationStart,
368                 // TODO: optimize by using a filter rejecting subtrees without annotations possible
369                 nodeIterator = document.createNodeIterator(rootElement, NodeFilter.SHOW_ELEMENT, null, false);
370 
371             n = /**@type{?Element}*/(nodeIterator.nextNode());
372             while (n) {
373                 if (n.namespaceURI === officens) {
374                     if (n.localName === "annotation") {
375                         name = n.getAttributeNS(officens, 'name');
376                         if (name) {
377                             if (annotationStarts.hasOwnProperty(name)) {
378                                 runtime.log("Warning: annotation name used more than once with <office:annotation/>: '" + name + "'");
379                             } else {
380                                 annotationStarts[name] = n;
381                             }
382                         }
383                     } else if (n.localName === "annotation-end") {
384                         name = n.getAttributeNS(officens, 'name');
385                         if (name) {
386                             if (annotationStarts.hasOwnProperty(name)) {
387                                 annotationStart = /** @type {!odf.AnnotationElement}*/(annotationStarts[name]);
388                                 if (!annotationStart.annotationEndElement) {
389                                     // Linking annotation start & end
390                                     annotationStart.annotationEndElement = n;
391                                 } else {
392                                     runtime.log("Warning: annotation name used more than once with <office:annotation-end/>: '" + name + "'");
393                                 }
394                             } else {
395                                 runtime.log("Warning: annotation end without an annotation start, name: '" + name + "'");
396                             }
397                         } else {
398                             runtime.log("Warning: annotation end without a name found");
399                         }
400                     }
401                 }
402                 n = /**@type{?Element}*/(nodeIterator.nextNode());
403             }
404         }
405 
406         /**
407          * Tags all styles with an attribute noting their scope.
408          * Helper function for the primitive complete backwriting of
409          * the automatic styles.
410          * @param {?Element} stylesRootElement
411          * @param {!string} scope
412          * @return {undefined}
413          */
414         function setAutomaticStylesScope(stylesRootElement, scope) {
415             var n = stylesRootElement && stylesRootElement.firstChild;
416             while (n) {
417                 if (n.nodeType === Node.ELEMENT_NODE) {
418                     /**@type{!Element}*/(n).setAttributeNS(webodfns, "scope", scope);
419                 }
420                 n = n.nextSibling;
421             }
422         }
423 
424         /**
425          * Returns the meta element. If it did not exist before, it will be created.
426          * @return {!Element}
427          */
428         function getEnsuredMetaElement() {
429             var root = self.rootElement,
430                 meta = root.meta;
431 
432             if (!meta) {
433                 root.meta = meta = document.createElementNS(officens, "meta");
434                 setChild(root, meta);
435             }
436 
437             return meta;
438         }
439 
440         /**
441          * @param {!string} metadataNs
442          * @param {!string} metadataLocalName
443          * @return {?string}
444          */
445         function getMetadata(metadataNs, metadataLocalName) {
446             var node = self.rootElement.meta, textNode;
447 
448             node = node && node.firstChild;
449             while (node && (node.namespaceURI !== metadataNs || node.localName !== metadataLocalName)) {
450                 node = node.nextSibling;
451             }
452             node = node && node.firstChild;
453             while (node && node.nodeType !== Node.TEXT_NODE) {
454                 node = node.nextSibling;
455             }
456             if (node) {
457                 textNode = /**@type{!Text}*/(node);
458                 return textNode.data;
459             }
460             return null;
461         }
462         this.getMetadata = getMetadata;
463 
464         /**
465          * Returns key with a number postfix or none, as key unused both in map1 and map2.
466          * @param {!string} key
467          * @param {!Object} map1
468          * @param {!Object} map2
469          * @return {!string}
470          */
471         function unusedKey(key, map1, map2) {
472             var i = 0, postFixedKey;
473 
474             // cut any current postfix number
475             key = key.replace(/\d+$/, '');
476             // start with no postfix, continue with i = 1, aiming for the simpelst unused number or key
477             postFixedKey = key;
478             while (map1.hasOwnProperty(postFixedKey) || map2.hasOwnProperty(postFixedKey)) {
479                 i += 1;
480                 postFixedKey = key + i;
481             }
482 
483             return postFixedKey;
484         }
485 
486         /**
487          * Returns a map with the fontface declaration elements, with font-face name as key.
488          * @param {!Element} fontFaceDecls
489          * @return {!Object.<!string,!Element>}
490           */
491         function mapByFontFaceName(fontFaceDecls) {
492             var fn, result = {}, fontname;
493             // create map of current target decls
494             fn = fontFaceDecls.firstChild;
495             while (fn) {
496                 if (fn.nodeType === Node.ELEMENT_NODE
497                         && fn.namespaceURI === stylens
498                         && fn.localName === "font-face") {
499                     fontname = /**@type{!Element}*/(fn).getAttributeNS(stylens, "name");
500                     // assuming existance and uniqueness of style:name here
501                     result[fontname] = fn;
502                 }
503                 fn = fn.nextSibling;
504             }
505             return result;
506         }
507 
508         /**
509          * Merges all style:font-face elements from the source into the target.
510          * Skips elements equal to one already in the target.
511          * Elements with the same style:name but different properties get a new
512          * value for style:name. Any name changes are logged and returned as a map
513          * with the old names as keys.
514          * @param {!Element} targetFontFaceDeclsRootElement
515          * @param {!Element} sourceFontFaceDeclsRootElement
516          * @return {!Object.<!string,!string>}  mapping of old font-face name to new
517          */
518         function mergeFontFaceDecls(targetFontFaceDeclsRootElement, sourceFontFaceDeclsRootElement) {
519             var e, s, fontFaceName, newFontFaceName,
520                 targetFontFaceDeclsMap, sourceFontFaceDeclsMap,
521                 fontFaceNameChangeMap = {};
522 
523             targetFontFaceDeclsMap = mapByFontFaceName(targetFontFaceDeclsRootElement);
524             sourceFontFaceDeclsMap = mapByFontFaceName(sourceFontFaceDeclsRootElement);
525 
526             // merge source decls into target
527             e = sourceFontFaceDeclsRootElement.firstElementChild;
528             while (e) {
529                 s = e.nextElementSibling;
530                 if (e.namespaceURI === stylens && e.localName === "font-face") {
531                     fontFaceName = e.getAttributeNS(stylens, "name");
532                     // already such a name used in target?
533                     if (targetFontFaceDeclsMap.hasOwnProperty(fontFaceName)) {
534                         // skip it if the declarations are equal, otherwise insert with a new, unused name
535                         if (!e.isEqualNode(targetFontFaceDeclsMap[fontFaceName])) {
536                             newFontFaceName = unusedKey(fontFaceName, targetFontFaceDeclsMap, sourceFontFaceDeclsMap);
537                             e.setAttributeNS(stylens, "style:name", newFontFaceName);
538                             // copy with a new name
539                             targetFontFaceDeclsRootElement.appendChild(e);
540                             targetFontFaceDeclsMap[newFontFaceName] = e;
541                             delete sourceFontFaceDeclsMap[fontFaceName];
542                             // note name change
543                             fontFaceNameChangeMap[fontFaceName] = newFontFaceName;
544                         }
545                     } else {
546                         // move over
547                         // perhaps one day it could also be checked if there is an equal declaration
548                         // with a different name, but that has yet to be seen in real life
549                         targetFontFaceDeclsRootElement.appendChild(e);
550                         targetFontFaceDeclsMap[fontFaceName] = e;
551                         delete sourceFontFaceDeclsMap[fontFaceName];
552                     }
553                 }
554                 e = s;
555             }
556             return fontFaceNameChangeMap;
557         }
558 
559         /**
560          * Creates a clone of the styles tree containing only styles tagged
561          * with the given scope, or with no specified scope.
562          * Helper function for the primitive complete backwriting of
563          * the automatic styles.
564          * @param {?Element} stylesRootElement
565          * @param {!string} scope
566          * @return {?Element}
567          */
568         function cloneStylesInScope(stylesRootElement, scope) {
569             var copy = null, e, s, scopeAttrValue;
570             if (stylesRootElement) {
571                 copy = stylesRootElement.cloneNode(true);
572                 e = copy.firstElementChild;
573                 while (e) {
574                     s = e.nextElementSibling;
575                     scopeAttrValue = e.getAttributeNS(webodfns, "scope");
576                     if (scopeAttrValue && scopeAttrValue !== scope) {
577                         copy.removeChild(e);
578                     }
579                     e = s;
580                 }
581             }
582             return copy;
583         }
584         /**
585          * Creates a clone of the font face declaration tree containing only
586          * those declarations which are referenced in the passed styles.
587          * @param {?Element} fontFaceDeclsRootElement
588          * @param {!Array.<!Element>} stylesRootElementList
589          * @return {?Element}
590          */
591         function cloneFontFaceDeclsUsedInStyles(fontFaceDeclsRootElement, stylesRootElementList) {
592             var e, nextSibling, fontFaceName,
593                 copy = null,
594                 usedFontFaceDeclMap = {};
595 
596             if (fontFaceDeclsRootElement) {
597                 // first collect used font faces
598                 stylesRootElementList.forEach(function (stylesRootElement) {
599                     styleInfo.collectUsedFontFaces(usedFontFaceDeclMap, stylesRootElement);
600                 });
601 
602                 // then clone all font face declarations and drop those which are not in the list of used
603                 copy = fontFaceDeclsRootElement.cloneNode(true);
604                 e = copy.firstElementChild;
605                 while (e) {
606                     nextSibling = e.nextElementSibling;
607                     fontFaceName = e.getAttributeNS(stylens, "name");
608                     if (!usedFontFaceDeclMap[fontFaceName]) {
609                         copy.removeChild(e);
610                     }
611                     e = nextSibling;
612                 }
613             }
614             return copy;
615         }
616 
617         /**
618          * Import the document elementnode into the DOM of OdfContainer.
619          * Any processing instructions are removed, since importing them
620          * gives an exception.
621          * @param {Document|undefined} xmldoc
622          * @return {!Element|undefined}
623          */
624         function importRootNode(xmldoc) {
625             var doc = self.rootElement.ownerDocument,
626                 node;
627             // remove all processing instructions
628             // TODO: replace cursor processing instruction with an element
629             if (xmldoc) {
630                 removeProcessingInstructions(xmldoc.documentElement);
631                 try {
632                     node = /**@type{!Element}*/(doc.importNode(xmldoc.documentElement, true));
633                 } catch (ignore) {
634                 }
635             }
636             return node;
637         }
638         /**
639          * @param {!number} state
640          * @return {undefined}
641          */
642         function setState(state) {
643             self.state = state;
644             if (self.onchange) {
645                 self.onchange(self);
646             }
647             if (self.onstatereadychange) {
648                 self.onstatereadychange(self);
649             }
650         }
651         /**
652          * @param {!Element} root
653          * @return {undefined}
654          */
655         function setRootElement(root) {
656             contentElement = null;
657             self.rootElement = /**@type{!odf.ODFDocumentElement}*/(root);
658             root.fontFaceDecls = domUtils.getDirectChild(root, officens, 'font-face-decls');
659             root.styles = domUtils.getDirectChild(root, officens, 'styles');
660             root.automaticStyles = domUtils.getDirectChild(root, officens, 'automatic-styles');
661             root.masterStyles = domUtils.getDirectChild(root, officens, 'master-styles');
662             root.body = domUtils.getDirectChild(root, officens, 'body');
663             root.meta = domUtils.getDirectChild(root, officens, 'meta');
664             root.settings = domUtils.getDirectChild(root, officens, 'settings');
665             root.scripts = domUtils.getDirectChild(root, officens, 'scripts');
666             linkAnnotationStartAndEndElements(root);
667         }
668         /**
669          * @param {Document|undefined} xmldoc
670          * @return {undefined}
671          */
672         function handleFlatXml(xmldoc) {
673             var root = importRootNode(xmldoc);
674             if (!root || root.localName !== 'document' ||
675                     root.namespaceURI !== officens) {
676                 setState(OdfContainer.INVALID);
677                 return;
678             }
679             setRootElement(/**@type{!Element}*/(root));
680             setState(OdfContainer.DONE);
681         }
682         /**
683          * @param {Document} xmldoc
684          * @return {undefined}
685          */
686         function handleStylesXml(xmldoc) {
687             var node = importRootNode(xmldoc),
688                 root = self.rootElement,
689                 n;
690             if (!node || node.localName !== 'document-styles' ||
691                     node.namespaceURI !== officens) {
692                 setState(OdfContainer.INVALID);
693                 return;
694             }
695             root.fontFaceDecls = domUtils.getDirectChild(node, officens, 'font-face-decls');
696             setChild(root, root.fontFaceDecls);
697             n = domUtils.getDirectChild(node, officens, 'styles');
698             root.styles = n || xmldoc.createElementNS(officens, 'styles');
699             setChild(root, root.styles);
700             n = domUtils.getDirectChild(node, officens, 'automatic-styles');
701             root.automaticStyles = n || xmldoc.createElementNS(officens, 'automatic-styles');
702             setAutomaticStylesScope(root.automaticStyles, documentStylesScope);
703             setChild(root, root.automaticStyles);
704             node = domUtils.getDirectChild(node, officens, 'master-styles');
705             root.masterStyles = node || xmldoc.createElementNS(officens,
706                     'master-styles');
707             setChild(root, root.masterStyles);
708             // automatic styles from styles.xml could shadow automatic styles
709             // from content.xml, because they could have the same name
710             // so prefix them and their uses with some almost unique string
711             styleInfo.prefixStyleNames(root.automaticStyles, automaticStylePrefix, root.masterStyles);
712         }
713         /**
714          * @param {Document} xmldoc
715          * @return {undefined}
716          */
717         function handleContentXml(xmldoc) {
718             var node = importRootNode(xmldoc),
719                 root,
720                 automaticStyles,
721                 fontFaceDecls,
722                 fontFaceNameChangeMap,
723                 c;
724             if (!node || node.localName !== 'document-content' ||
725                     node.namespaceURI !== officens) {
726                 setState(OdfContainer.INVALID);
727                 return;
728             }
729             root = self.rootElement;
730             fontFaceDecls = domUtils.getDirectChild(node, officens, 'font-face-decls');
731             if (root.fontFaceDecls && fontFaceDecls) {
732                 fontFaceNameChangeMap = mergeFontFaceDecls(root.fontFaceDecls, fontFaceDecls);
733             } else if (fontFaceDecls) {
734                 root.fontFaceDecls = fontFaceDecls;
735                 setChild(root, fontFaceDecls);
736             }
737             automaticStyles = domUtils.getDirectChild(node, officens, 'automatic-styles');
738             setAutomaticStylesScope(automaticStyles, documentContentScope);
739             if (fontFaceNameChangeMap) {
740                 styleInfo.changeFontFaceNames(automaticStyles, fontFaceNameChangeMap);
741             }
742             if (root.automaticStyles && automaticStyles) {
743                 c = automaticStyles.firstChild;
744                 while (c) {
745                     root.automaticStyles.appendChild(c);
746                     c = automaticStyles.firstChild; // works because node c moved
747                 }
748             } else if (automaticStyles) {
749                 root.automaticStyles = automaticStyles;
750                 setChild(root, automaticStyles);
751             }
752             node = domUtils.getDirectChild(node, officens, 'body');
753             if (node === null) {
754                 throw "<office:body/> tag is mising.";
755             }
756             root.body = node;
757             setChild(root, root.body);
758         }
759         /**
760          * @param {Document} xmldoc
761          * @return {undefined}
762          */
763         function handleMetaXml(xmldoc) {
764             var node = importRootNode(xmldoc),
765                 root;
766             if (!node || node.localName !== 'document-meta' ||
767                     node.namespaceURI !== officens) {
768                 return;
769             }
770             root = self.rootElement;
771             root.meta = domUtils.getDirectChild(node, officens, 'meta');
772             setChild(root, root.meta);
773         }
774         /**
775          * @param {Document} xmldoc
776          * @return {undefined}
777          */
778         function handleSettingsXml(xmldoc) {
779             var node = importRootNode(xmldoc),
780                 root;
781             if (!node || node.localName !== 'document-settings' ||
782                     node.namespaceURI !== officens) {
783                 return;
784             }
785             root = self.rootElement;
786             root.settings = domUtils.getDirectChild(node, officens, 'settings');
787             setChild(root, root.settings);
788         }
789         /**
790          * @param {Document} xmldoc
791          * @return {undefined}
792          */
793         function handleManifestXml(xmldoc) {
794             var node = importRootNode(xmldoc),
795                 root,
796                 e;
797             if (!node || node.localName !== 'manifest' ||
798                     node.namespaceURI !== manifestns) {
799                 return;
800             }
801             root = self.rootElement;
802             root.manifest = /**@type{!Element}*/(node);
803             e = root.manifest.firstElementChild;
804             while (e) {
805                 if (e.localName === "file-entry" &&
806                         e.namespaceURI === manifestns) {
807                     partMimetypes[e.getAttributeNS(manifestns, "full-path")] =
808                         e.getAttributeNS(manifestns, "media-type");
809                 }
810                 e = e.nextElementSibling;
811             }
812         }
813         /**
814          * @param {!Document} xmldoc
815          * @param {!string} localName
816          * @param {!Object.<!string,!boolean>} allowedNamespaces
817          * @return {undefined}
818          */
819         function removeElements(xmldoc, localName, allowedNamespaces) {
820             var elements = domUtils.getElementsByTagName(xmldoc, localName),
821                 element,
822                 i;
823             for (i = 0; i < elements.length; i += 1) {
824                 element = elements[i];
825                 if (!allowedNamespaces.hasOwnProperty(element.namespaceURI)) {
826                     element.parentNode.removeChild(element);
827                 }
828             }
829         }
830         /**
831          * Remove any HTML <script/> tags from the DOM.
832          * The tags need to be removed, because otherwise they would be executed
833          * when the dom is inserted into the document.
834          * To be safe, all elements with localName "script" are removed, unless
835          * they are in a known, allowed namespace.
836          * @param {!Document} xmldoc
837          * @return {undefined}
838          */
839         function removeDangerousElements(xmldoc) {
840             removeElements(xmldoc, "script", {
841                 "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0": true,
842                 "urn:oasis:names:tc:opendocument:xmlns:office:1.0": true,
843                 "urn:oasis:names:tc:opendocument:xmlns:table:1.0": true,
844                 "urn:oasis:names:tc:opendocument:xmlns:text:1.0": true,
845                 "urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0": true
846             });
847             removeElements(xmldoc, "style", {
848                 "urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0": true,
849                 "urn:oasis:names:tc:opendocument:xmlns:drawing:1.0": true,
850                 "urn:oasis:names:tc:opendocument:xmlns:style:1.0": true
851             });
852         }
853 
854         /**
855          * Remove all attributes that have no namespace and that have
856          * localname like 'on....', the event handler attributes.
857          * @param {!Element} element
858          * @return {undefined}
859          */
860         function removeDangerousAttributes(element) {
861             var e = element.firstElementChild, as = [], i, n, a,
862                 atts = element.attributes,
863                 l = atts.length;
864             // collect all dangerous attributes
865             for (i = 0; i < l; i += 1) {
866                 a = atts.item(i);
867                 n = a.localName.substr(0, 2).toLowerCase();
868                 if (a.namespaceURI === null && n === "on") {
869                     as.push(a);
870                 }
871             }
872             // remove the dangerous attributes
873             l = as.length;
874             for (i = 0; i < l; i += 1) {
875                 element.removeAttributeNode(as[i]);
876             }
877             // recurse into the child elements
878             while (e) {
879                 removeDangerousAttributes(e);
880                 e = e.nextElementSibling;
881             }
882         }
883 
884         /**
885          * @param {!Array.<!{path:string,handler:function(?Document)}>} remainingComponents
886          * @return {undefined}
887          */
888         function loadNextComponent(remainingComponents) {
889             var component = remainingComponents.shift();
890 
891             if (component) {
892                 zip.loadAsDOM(component.path, function (err, xmldoc) {
893                     if (xmldoc) {
894                         removeDangerousElements(xmldoc);
895                         removeDangerousAttributes(xmldoc.documentElement);
896                     }
897                     component.handler(xmldoc);
898                     if (self.state === OdfContainer.INVALID) {
899                         if (err) {
900                             runtime.log("ERROR: Unable to load " + component.path + " - " + err);
901                         } else {
902                             runtime.log("ERROR: Unable to load " + component.path);
903                         }
904                         return;
905                     }
906                     if (err) {
907                         runtime.log("DEBUG: Unable to load " + component.path + " - " + err);
908                     }
909                     loadNextComponent(remainingComponents);
910                 });
911             } else {
912                 linkAnnotationStartAndEndElements(self.rootElement);
913                 setState(OdfContainer.DONE);
914             }
915         }
916         /**
917          * @return {undefined}
918          */
919         function loadComponents() {
920             var componentOrder = [
921                 {path: 'styles.xml', handler: handleStylesXml},
922                 {path: 'content.xml', handler: handleContentXml},
923                 {path: 'meta.xml', handler: handleMetaXml},
924                 {path: 'settings.xml', handler: handleSettingsXml},
925                 {path: 'META-INF/manifest.xml', handler: handleManifestXml}
926             ];
927             loadNextComponent(componentOrder);
928         }
929         /**
930          * @param {!string} name
931          * @return {!string}
932          */
933         function createDocumentElement(name) {
934             var /**@type{string}*/
935                 s = "";
936 
937             /**
938              * @param {string} prefix
939              * @param {string} ns
940              */
941             function defineNamespace(prefix, ns) {
942                 s += " xmlns:" + prefix + "=\"" + ns + "\"";
943             }
944             odf.Namespaces.forEachPrefix(defineNamespace);
945             return "<?xml version=\"1.0\" encoding=\"UTF-8\"?><office:" + name +
946                     " " + s + " office:version=\"1.2\">";
947         }
948         /**
949          * @return {!string}
950          */
951         function serializeMetaXml() {
952             var serializer = new xmldom.LSSerializer(),
953                 /**@type{!string}*/
954                 s = createDocumentElement("document-meta");
955             serializer.filter = new odf.OdfNodeFilter();
956             s += serializer.writeToString(self.rootElement.meta, odf.Namespaces.namespaceMap);
957             s += "</office:document-meta>";
958             return s;
959         }
960         /**
961          * Creates a manifest:file-entry node
962          * @param {!string} fullPath Full-path attribute value for the file-entry
963          * @param {!string} mediaType Media-type attribute value for the file-entry
964          * @return {!Node}
965          */
966         function createManifestEntry(fullPath, mediaType) {
967             var element = document.createElementNS(manifestns, 'manifest:file-entry');
968             element.setAttributeNS(manifestns, 'manifest:full-path', fullPath);
969             element.setAttributeNS(manifestns, 'manifest:media-type', mediaType);
970             return element;
971         }
972         /**
973          * @return {string}
974          */
975         function serializeManifestXml() {
976             var header = '<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n',
977                 xml = '<manifest:manifest xmlns:manifest="' + manifestns + '" manifest:version="1.2"></manifest:manifest>',
978                 manifest = /**@type{!Document}*/(runtime.parseXML(xml)),
979                 manifestRoot = manifest.documentElement,
980                 serializer = new xmldom.LSSerializer(),
981                 /**@type{string}*/
982                 fullPath;
983 
984             for (fullPath in partMimetypes) {
985                 if (partMimetypes.hasOwnProperty(fullPath)) {
986                     manifestRoot.appendChild(createManifestEntry(fullPath, partMimetypes[fullPath]));
987                 }
988             }
989             serializer.filter = new odf.OdfNodeFilter();
990             return header + serializer.writeToString(manifest, odf.Namespaces.namespaceMap);
991         }
992         /**
993          * @return {!string}
994          */
995         function serializeSettingsXml() {
996             var serializer,
997                 /**@type{!string}*/
998                 s = "";
999             // <office:settings/> is optional, but if present must have at least one child element
1000             if (self.rootElement.settings && self.rootElement.settings.firstElementChild) {
1001                 serializer = new xmldom.LSSerializer();
1002                 s = createDocumentElement("document-settings");
1003                 serializer.filter = new odf.OdfNodeFilter();
1004                 s += serializer.writeToString(self.rootElement.settings, odf.Namespaces.namespaceMap);
1005                 s += "</office:document-settings>";
1006             }
1007             return s;
1008         }
1009         /**
1010          * @return {!string}
1011          */
1012         function serializeStylesXml() {
1013             var fontFaceDecls, automaticStyles, masterStyles,
1014                 nsmap = odf.Namespaces.namespaceMap,
1015                 serializer = new xmldom.LSSerializer(),
1016                 /**@type{!string}*/
1017                 s = createDocumentElement("document-styles");
1018 
1019             // special handling for merged toplevel nodes
1020             automaticStyles = cloneStylesInScope(
1021                 self.rootElement.automaticStyles,
1022                 documentStylesScope
1023             );
1024             masterStyles = /**@type{!Element}*/(self.rootElement.masterStyles.cloneNode(true));
1025             fontFaceDecls = cloneFontFaceDeclsUsedInStyles(self.rootElement.fontFaceDecls, [masterStyles, self.rootElement.styles, automaticStyles]);
1026 
1027             // automatic styles from styles.xml could shadow automatic styles from content.xml,
1028             // because they could have the same name
1029             // thus they were prefixed on loading with some almost unique string, which cam be removed
1030             // again before saving
1031             styleInfo.removePrefixFromStyleNames(automaticStyles,
1032                     automaticStylePrefix, masterStyles);
1033             serializer.filter = new OdfStylesFilter(masterStyles, automaticStyles);
1034 
1035             s += serializer.writeToString(fontFaceDecls, nsmap);
1036             s += serializer.writeToString(self.rootElement.styles, nsmap);
1037             s += serializer.writeToString(automaticStyles, nsmap);
1038             s += serializer.writeToString(masterStyles, nsmap);
1039             s += "</office:document-styles>";
1040             return s;
1041         }
1042         /**
1043          * @return {!string}
1044          */
1045         function serializeContentXml() {
1046             var fontFaceDecls, automaticStyles,
1047                 nsmap = odf.Namespaces.namespaceMap,
1048                 serializer = new xmldom.LSSerializer(),
1049                 /**@type{!string}*/
1050                 s = createDocumentElement("document-content");
1051 
1052             // special handling for merged toplevel nodes
1053             automaticStyles = cloneStylesInScope(self.rootElement.automaticStyles, documentContentScope);
1054             fontFaceDecls = cloneFontFaceDeclsUsedInStyles(self.rootElement.fontFaceDecls, [automaticStyles]);
1055 
1056             serializer.filter = new OdfContentFilter(self.rootElement.body, automaticStyles);
1057 
1058             s += serializer.writeToString(fontFaceDecls, nsmap);
1059             s += serializer.writeToString(automaticStyles, nsmap);
1060             s += serializer.writeToString(self.rootElement.body, nsmap);
1061             s += "</office:document-content>";
1062             return s;
1063         }
1064         /**
1065          * @param {!{Type:function(new:Object),namespaceURI:string,localName:string}} type
1066          * @return {!Element}
1067          */
1068         function createElement(type) {
1069             var original = document.createElementNS(
1070                     type.namespaceURI,
1071                     type.localName
1072                 ),
1073                 /**@type{string}*/
1074                 method,
1075                 iface = new type.Type();
1076             for (method in iface) {
1077                 if (iface.hasOwnProperty(method)) {
1078                     original[method] = iface[method];
1079                 }
1080             }
1081             return original;
1082         }
1083         /**
1084          * @param {!string} url
1085          * @param {!function((string)):undefined} callback
1086          * @return {undefined}
1087          */
1088         function loadFromXML(url, callback) {
1089             /**
1090              * @param {?string} err
1091              * @param {?Document} dom
1092              */
1093             function handler(err, dom) {
1094                 if (err) {
1095                     callback(err);
1096                 } else if (!dom) {
1097                     callback("No DOM was loaded.");
1098                 } else {
1099                     removeDangerousElements(dom);
1100                     removeDangerousAttributes(dom.documentElement);
1101                     handleFlatXml(dom);
1102                 }
1103             }
1104             runtime.loadXML(url, handler);
1105         }
1106         // public functions
1107         this.setRootElement = setRootElement;
1108 
1109         /**
1110          * @return {!Element}
1111          */
1112         this.getContentElement = function () {
1113             var /**@type{!Element}*/
1114                 body;
1115             if (!contentElement) {
1116                 body = self.rootElement.body;
1117                 contentElement = domUtils.getDirectChild(body, officens, "text")
1118                     || domUtils.getDirectChild(body, officens, "presentation")
1119                     || domUtils.getDirectChild(body, officens, "spreadsheet");
1120             }
1121             if (!contentElement) {
1122                 throw "Could not find content element in <office:body/>.";
1123             }
1124             return contentElement;
1125         };
1126 
1127         /**
1128          * Gets the document type as 'text', 'presentation', or 'spreadsheet'.
1129          * @return {!string}
1130          */
1131         this.getDocumentType = function () {
1132             var content = self.getContentElement();
1133             return content && content.localName;
1134         };
1135 
1136         /**
1137          * Returns whether the document is a template.
1138          * @return {!boolean}
1139          */
1140         this.isTemplate = function () {
1141             var docMimetype = partMimetypes["/"];
1142             return (docMimetype.substr(-9) === "-template");
1143         };
1144 
1145          /**
1146          * Sets whether the document is a template or not.
1147          * @param {!boolean} isTemplate
1148          * @return {undefined}
1149          */
1150        this.setIsTemplate = function (isTemplate) {
1151             var docMimetype = partMimetypes["/"],
1152                 oldIsTemplate = (docMimetype.substr(-9) === "-template"),
1153                 data;
1154 
1155             if (isTemplate === oldIsTemplate) {
1156                 return;
1157             }
1158 
1159             if (isTemplate) {
1160                 docMimetype = docMimetype + "-template";
1161             } else {
1162                 docMimetype = docMimetype.substr(0, docMimetype.length-9);
1163             }
1164 
1165             partMimetypes["/"] = docMimetype;
1166             data = runtime.byteArrayFromString(docMimetype, "utf8");
1167             zip.save("mimetype", data, false, new Date());
1168         };
1169 
1170         /**
1171          * Open file and parse it. Return the XML Node. Return the root node of
1172          * the file or null if this is not possible.
1173          * For 'content.xml', 'styles.xml', 'meta.xml', and 'settings.xml', the
1174          * elements 'document-content', 'document-styles', 'document-meta', or
1175          * 'document-settings' will be returned respectively.
1176          * @param {string} partname
1177          * @return {!odf.OdfPart}
1178          **/
1179         this.getPart = function (partname) {
1180             return new odf.OdfPart(partname, partMimetypes[partname], self, zip);
1181         };
1182         /**
1183          * @param {string} url
1184          * @param {function(?string, ?Uint8Array)} callback receiving err and data
1185          * @return {undefined}
1186          */
1187         this.getPartData = function (url, callback) {
1188             zip.load(url, callback);
1189         };
1190 
1191         /**
1192          * Sets the metadata fields from the given properties map.
1193          * @param {?Object.<!string, !string>} setProperties A flat object that is a string->string map of field name -> value.
1194          * @param {?Array.<!string>} removedPropertyNames An array of metadata field names (prefixed).
1195          * @return {undefined}
1196          */
1197         function setMetadata(setProperties, removedPropertyNames) {
1198             var metaElement = getEnsuredMetaElement();
1199 
1200             if (setProperties) {
1201                 domUtils.mapKeyValObjOntoNode(metaElement, setProperties, odf.Namespaces.lookupNamespaceURI);
1202             }
1203             if (removedPropertyNames) {
1204                 domUtils.removeKeyElementsFromNode(metaElement, removedPropertyNames, odf.Namespaces.lookupNamespaceURI);
1205             }
1206         }
1207         this.setMetadata = setMetadata;
1208 
1209         /**
1210          * Increment the number of times the document has been edited.
1211          * @return {!number} new number of editing cycles
1212          */
1213         this.incrementEditingCycles = function () {
1214             var currentValueString = getMetadata(odf.Namespaces.metans, "editing-cycles"),
1215                 currentCycles = currentValueString ? parseInt(currentValueString, 10) : 0;
1216 
1217             if (isNaN(currentCycles)) {
1218                 currentCycles = 0;
1219             }
1220 
1221             setMetadata({"meta:editing-cycles": currentCycles + 1}, null);
1222             return currentCycles + 1;
1223         };
1224 
1225         /**
1226          * Write pre-saving metadata to the DOM
1227          * @return {undefined}
1228          */
1229         function updateMetadataForSaving() {
1230             // set the opendocument provider used to create/
1231             // last modify the document.
1232             // this string should match the definition for
1233             // user-agents in the http protocol as specified
1234             // in section 14.43 of [RFC2616].
1235             var generatorString,
1236                 window = runtime.getWindow();
1237 
1238             generatorString = "WebODF/" + webodf.Version;
1239 
1240             if (window) {
1241                 generatorString = generatorString + " " + window.navigator.userAgent;
1242             }
1243 
1244             setMetadata({"meta:generator": generatorString}, null);
1245         }
1246 
1247         /**
1248          * @param {!string} type
1249          * @param {!boolean=} isTemplate  Default value is false.
1250          * @return {!core.Zip}
1251          */
1252         function createEmptyDocument(type, isTemplate) {
1253             var emptyzip = new core.Zip("", null),
1254                 mimetype = "application/vnd.oasis.opendocument." + type + (isTemplate === true ? "-template" : ""),
1255                 data = runtime.byteArrayFromString(
1256                     mimetype,
1257                     "utf8"
1258                 ),
1259                 root = self.rootElement,
1260                 content = document.createElementNS(officens, type);
1261             emptyzip.save("mimetype", data, false, new Date());
1262             /**
1263              * @param {!string} memberName  variant of the real local name which allows dot notation
1264              * @param {!string=} realLocalName
1265              * @return {undefined}
1266              */
1267             function addToplevelElement(memberName, realLocalName) {
1268                 var element;
1269                 if (!realLocalName) {
1270                     realLocalName = memberName;
1271                 }
1272                 element = document.createElementNS(officens, realLocalName);
1273                 root[memberName] = element;
1274                 root.appendChild(element);
1275             }
1276             // add toplevel elements in correct order to the root node
1277             addToplevelElement("meta");
1278             addToplevelElement("settings");
1279             addToplevelElement("scripts");
1280             addToplevelElement("fontFaceDecls",   "font-face-decls");
1281             addToplevelElement("styles");
1282             addToplevelElement("automaticStyles", "automatic-styles");
1283             addToplevelElement("masterStyles",    "master-styles");
1284             addToplevelElement("body");
1285             root.body.appendChild(content);
1286             partMimetypes["/"] = mimetype;
1287             partMimetypes["settings.xml"] = "text/xml";
1288             partMimetypes["meta.xml"] = "text/xml";
1289             partMimetypes["styles.xml"] = "text/xml";
1290             partMimetypes["content.xml"] = "text/xml";
1291 
1292             setState(OdfContainer.DONE);
1293             return emptyzip;
1294         }
1295 
1296         /**
1297          * Fill the zip with current data.
1298          * @return {undefined}
1299          */
1300         function fillZip() {
1301             // the assumption so far is that all ODF parts are serialized
1302             // already, but meta, settings, styles and content should be
1303             // refreshed
1304             // update the zip entries with the data from the live ODF DOM
1305             var data,
1306                 date = new Date(),
1307                 settings;
1308 
1309             settings = serializeSettingsXml();
1310             if (settings) {
1311                 // Optional according to package spec
1312                 // See http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part1.html#__RefHeading__440346_826425813
1313                 data = runtime.byteArrayFromString(settings, "utf8");
1314                 zip.save("settings.xml", data, true, date);
1315             } else {
1316                 zip.remove("settings.xml");
1317             }
1318             updateMetadataForSaving();
1319             // Even thought meta-data is optional, it is always created by the previous statement
1320             data = runtime.byteArrayFromString(serializeMetaXml(), "utf8");
1321             zip.save("meta.xml", data, true, date);
1322             data = runtime.byteArrayFromString(serializeStylesXml(), "utf8");
1323             zip.save("styles.xml", data, true, date);
1324             data = runtime.byteArrayFromString(serializeContentXml(), "utf8");
1325             zip.save("content.xml", data, true, date);
1326             data = runtime.byteArrayFromString(serializeManifestXml(), "utf8");
1327             zip.save("META-INF/manifest.xml", data, true, date);
1328         }
1329         /**
1330          * Create a bytearray from the zipfile.
1331          * @param {!function(!Uint8Array):undefined} successCallback receiving zip as bytearray
1332          * @param {!function(?string):undefined} errorCallback receiving possible err
1333          * @return {undefined}
1334          */
1335         function createByteArray(successCallback, errorCallback) {
1336             fillZip();
1337             zip.createByteArray(successCallback, errorCallback);
1338         }
1339         this.createByteArray = createByteArray;
1340         /**
1341          * @param {!string} newurl
1342          * @param {function(?string):undefined} callback
1343          * @return {undefined}
1344          */
1345         function saveAs(newurl, callback) {
1346             fillZip();
1347             zip.writeAs(newurl, function (err) {
1348                 callback(err);
1349             });
1350         }
1351         this.saveAs = saveAs;
1352         /**
1353          * @param {function(?string):undefined} callback
1354          * @return {undefined}
1355          */
1356         this.save = function (callback) {
1357             saveAs(url, callback);
1358         };
1359 
1360         /**
1361          * @return {!string}
1362          */
1363         this.getUrl = function () {
1364             // TODO: saveAs seems to not update the url, is that wanted?
1365             return url;
1366         };
1367         /**
1368          * Add a new blob or overwrite any existing blob which has the same filename.
1369          * @param {!string} filename
1370          * @param {!string} mimetype
1371          * @param {!string} content base64 encoded string
1372          */
1373         this.setBlob = function (filename, mimetype, content) {
1374             var data = base64.convertBase64ToByteArray(content),
1375                 date = new Date();
1376             zip.save(filename, data, false, date);
1377             if (partMimetypes.hasOwnProperty(filename)) {
1378                 runtime.log(filename + " has been overwritten.");
1379             }
1380             partMimetypes[filename] = mimetype;
1381         };
1382         /**
1383          * @param {!string} filename
1384          */
1385         this.removeBlob = function (filename) {
1386             var foundAndRemoved = zip.remove(filename);
1387             runtime.assert(foundAndRemoved, "file is not found: " + filename);
1388             delete partMimetypes[filename];
1389         };
1390         // initialize public variables
1391         this.state = OdfContainer.LOADING;
1392         this.rootElement = /**@type{!odf.ODFDocumentElement}*/(
1393             createElement({
1394                 Type: odf.ODFDocumentElement,
1395                 namespaceURI: odf.ODFDocumentElement.namespaceURI,
1396                 localName: odf.ODFDocumentElement.localName
1397             })
1398         );
1399 
1400         // initialize private variables
1401         if (urlOrType === odf.OdfContainer.DocumentType.TEXT) {
1402             zip = createEmptyDocument("text");
1403         } else if (urlOrType === odf.OdfContainer.DocumentType.TEXT_TEMPLATE) {
1404             zip = createEmptyDocument("text", true);
1405         } else if (urlOrType === odf.OdfContainer.DocumentType.PRESENTATION) {
1406             zip = createEmptyDocument("presentation");
1407         } else if (urlOrType === odf.OdfContainer.DocumentType.PRESENTATION_TEMPLATE) {
1408             zip = createEmptyDocument("presentation", true);
1409         } else if (urlOrType === odf.OdfContainer.DocumentType.SPREADSHEET) {
1410             zip = createEmptyDocument("spreadsheet");
1411         } else if (urlOrType === odf.OdfContainer.DocumentType.SPREADSHEET_TEMPLATE) {
1412             zip = createEmptyDocument("spreadsheet", true);
1413         } else {
1414             url = /**@type{!string}*/(urlOrType);
1415             zip = new core.Zip(url, function (err, zipobject) {
1416                 zip = zipobject;
1417                 if (err) {
1418                     loadFromXML(url, function (xmlerr) {
1419                         if (err) {
1420                             zip.error = err + "\n" + xmlerr;
1421                             setState(OdfContainer.INVALID);
1422                         }
1423                     });
1424                 } else {
1425                     loadComponents();
1426                 }
1427             });
1428         }
1429     };
1430     odf.OdfContainer.EMPTY = 0;
1431     odf.OdfContainer.LOADING = 1;
1432     odf.OdfContainer.DONE = 2;
1433     odf.OdfContainer.INVALID = 3;
1434     odf.OdfContainer.SAVING = 4;
1435     odf.OdfContainer.MODIFIED = 5;
1436     /**
1437      * @param {!string} url
1438      * @return {!odf.OdfContainer}
1439      */
1440     odf.OdfContainer.getContainer = function (url) {
1441         return new odf.OdfContainer(url, null);
1442     };
1443 }());
1444 /**
1445  * @enum {number}
1446  */
1447 odf.OdfContainer.DocumentType = {
1448     TEXT:                  1,
1449     TEXT_TEMPLATE:         2,
1450     PRESENTATION:          3,
1451     PRESENTATION_TEMPLATE: 4,
1452     SPREADSHEET:           5,
1453     SPREADSHEET_TEMPLATE:  6
1454 };
1455