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