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