node-webodf
Version:
WebODF - JavaScript Document Engine http://webodf.org/
789 lines (768 loc) • 28.2 kB
JavaScript
/**
* Copyright (C) 2012 KO GmbH <copyright@kogmbh.com>
*
* @licstart
* This file is part of WebODF.
*
* WebODF is free software: you can redistribute it and/or modify it
* under the terms of the GNU Affero General Public License (GNU AGPL)
* as published by the Free Software Foundation, either version 3 of
* the License, or (at your option) any later version.
*
* WebODF is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with WebODF. If not, see <http://www.gnu.org/licenses/>.
* @licend
*
* @source: http://www.webodf.org/
* @source: https://github.com/kogmbh/WebODF/
*/
/*global runtime, Node, window, DOMParser, core, xmldom, NodeFilter, alert,
FileReader*/
runtime.loadClass("core.Zip");
runtime.loadClass("core.Base64");
runtime.loadClass("xmldom.RelaxNG");
runtime.loadClass("xmldom.RelaxNGParser");
/** This code runs a number of tests on an ODF document.
* Ideally, it would use ODFContainer, but for now, it uses a custome container
* for loaded odf files.
*/
function conformsToPattern(object, pattern) {
"use strict";
var i;
if (object === undefined || object === null) {
return pattern === null || (typeof pattern) !== "object";
}
for (i in pattern) {
if (pattern.hasOwnProperty(i)) {
if (!(object.hasOwnProperty(i) ||
(i === "length" && object.length)) ||
!conformsToPattern(object[i], pattern[i])) {
return false;
}
}
}
return true;
}
function getConformingObjects(object, pattern, name) {
"use strict";
var c = [], i;
name = name || "??";
// we do not look inside long arrays and strings atm,
// detection of these types could be better
function accept(object) {
return object !== null && object !== undefined &&
(typeof object) === "object" &&
(object.length === undefined || object.length < 1000) &&
!(object instanceof Node) &&
!(object.constructor && object.constructor === window.Uint8Array);
}
for (i in object) {
if (object.hasOwnProperty(i) && accept(object[i])) {
c = c.concat(getConformingObjects(object[i], pattern, i));
}
}
if (conformsToPattern(object, pattern)) {
c.push(object);
}
return c;
}
function parseXml(data, errorlog, name) {
"use strict";
function getText(e) {
var str = "", c = e.firstChild;
while (c) {
if (c.nodeType === 3) {
str += c.nodeValue;
} else {
str += getText(c);
}
c = c.nextSibling;
}
return str;
}
var str, parser, errorelements;
try {
str = runtime.byteArrayToString(data, "utf8");
parser = new DOMParser();
str = parser.parseFromString(str, "text/xml");
if (str.documentElement.localName === "parsererror"
|| str.documentElement.localName === "html") {
errorelements = str.getElementsByTagName("parsererror");
if (errorelements.length > 0) {
errorlog.push("invalid XML in " + name + ": " +
getText(errorelements[0]));
str = null;
}
}
} catch (err) {
errorlog.push(err);
}
return str;
}
/*** the jobs / tests ***/
function ParseXMLJob() {
"use strict";
this.inputpattern = { file: { entries: [] } };
this.outputpattern = {
file: { entries: [] },
errors: { parseXmlErrors: [] },
content_xml: null,
manifest_xml: null,
settings_xml: null,
meta_xml: null,
styles_xml: null
};
function parseXmlFiles(input, position, callback) {
var e = input.file.entries,
filename,
ext,
dom;
if (position >= e.length) {
return callback();
}
filename = e[position].filename;
ext = filename.substring(filename.length - 4);
if (ext === ".xml" || ext === ".rdf") {
dom = parseXml(e[position].data, input.errors.parseXmlErrors,
filename);
if (filename === "content.xml") {
input.content_xml = dom;
} else if (filename === "META-INF/manifest.xml") {
input.manifest_xml = dom;
} else if (filename === "styles.xml") {
input.styles_xml = dom;
} else if (filename === "meta.xml") {
input.meta_xml = dom;
} else if (filename === "settings.xml") {
input.settings_xml = dom;
}
e[position].dom = dom;
}
window.setTimeout(function () {
parseXmlFiles(input, position + 1, callback);
}, 0);
}
this.run = function (input, callback) {
input.errors = input.errors || {};
input.errors.parseXmlErrors = [];
input.content_xml = null;
input.manifest_xml = null;
input.styles_xml = null;
input.meta_xml = null;
input.settings_xml = null;
parseXmlFiles(input, 0, callback);
};
}
function UnpackJob() {
"use strict";
this.inputpattern = { file: { path: "", data: { length: 0 } } };
this.outputpattern = {
file: { entries: [], dom: null },
errors: { unpackErrors: [] }
};
function loadZipEntries(input, zip, position, callback) {
if (position >= input.file.entries.length) {
return callback();
}
var e = input.file.entries[position];
zip.load(e.filename, function (err, data) {
if (err) {
input.errors.unpackErrors.push(err);
}
e.error = err;
e.data = data;
window.setTimeout(function () {
loadZipEntries(input, zip, position + 1, callback);
}, 0);
});
}
function loadZip(input, callback) {
var zip = new core.Zip(input.file.path, function (err, zip) {
if (err) {
input.errors.unpackErrors.push(err);
callback();
} else {
input.file.entries = zip.getEntries();
loadZipEntries(input, zip, 0, callback);
}
});
// only done to make jslint see the var used
return zip;
}
function loadXml(input, callback) {
input.file.dom = parseXml(input.file.data, input.errors.unpackErrors,
input.file.name);
callback();
}
this.run = function (input, callback) {
input.errors = input.errors || {};
input.errors.unpackErrors = [];
input.file.dom = null;
input.file.entries = [];
if (input.file.data.length < 1) {
input.errors.unpackErrors.push("Input data is empty.");
return;
}
if (input.file.data[0] === 80) { // a ZIP file starts with 'P'
loadZip(input, callback);
} else {
loadXml(input, callback);
}
};
}
function MimetypeTestJob() {
"use strict";
this.inputpattern = {
file: { entries: [], dom: null },
manifest_xml: null
};
this.outputpattern = { mimetype: "", errors: { mimetypeErrors: [] } };
var manifestns = "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0";
function getManifestMimetype(manifest) {
if (!manifest) {
return null;
}
var path, mimetype, node;
node = manifest.documentElement.firstChild;
while (node) {
if (node.nodeType === 1 && node.localName === "file-entry" &&
node.namespaceURI === manifestns) {
path = node.getAttributeNS(manifestns, "full-path");
if (path === "/") {
mimetype = node.getAttributeNS(manifestns, "media-type");
break;
}
}
node = node.nextSibling;
}
return mimetype;
}
this.run = function (input, callback) {
input.mimetype = null;
input.errors.mimetypeErrors = [];
var mime = null,
altmime,
e = input.file.entries,
i;
if (input.file.dom) {
mime = input.file.dom.documentElement.getAttributeNS(
"urn:oasis:names:tc:opendocument:xmlns:office:1.0",
"mimetype"
);
} else {
if (e.length < 1 || e[0].filename !== "mimetype") {
input.errors.mimetypeErrors.push(
"First file in zip is not 'mimetype'"
);
}
for (i = 0; i < e.length; i += 1) {
if (e[i].filename === "mimetype") {
mime = runtime.byteArrayToString(e[i].data, "binary");
break;
}
}
if (mime) {
altmime = input.file.data.subarray(38, 38 + mime.length);
altmime = runtime.byteArrayToString(altmime, "binary");
if (mime !== altmime) {
input.errors.mimetypeErrors.push(
"mimetype should start at byte 38 in the zip file."
);
}
}
// compare with mimetype from manifest_xml
altmime = getManifestMimetype(input.manifest_xml);
if (altmime !== mime) {
input.errors.mimetypeErrors.push(
"manifest.xml has a different mimetype."
);
}
}
if (!mime) {
input.errors.mimetypeErrors.push("No mimetype was found.");
}
input.mimetype = mime;
callback();
};
}
function VersionTestJob() {
"use strict";
this.inputpattern = {
file: { dom: null },
content_xml: null,
styles_xml: null,
meta_xml: null,
settings_xml: null,
manifest_xml: null
};
this.outputpattern = { version: "", errors: { versionErrors: [] } };
var officens = "urn:oasis:names:tc:opendocument:xmlns:office:1.0";
function getVersion(dom, filename, log, vinfo, filerequired) {
var v, ns = officens;
if (!dom) {
if (filerequired) {
log.push(filename + " is missing, so version cannot be found.");
}
return;
}
if (filename === "META-INF/manifest.xml") {
ns = "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0";
}
if (!dom.documentElement.hasAttributeNS(ns, "version")) {
if (vinfo.versionrequired) {
log.push(filename + " has no version number.");
}
return;
}
v = dom.documentElement.getAttributeNS(ns, "version");
if (vinfo.version === undefined) {
vinfo.version = v;
// version number is required since ODF 1.2
vinfo.needversion = vinfo.version === "1.2";
vinfo.versionSource = filename;
} else if (v !== vinfo.version) {
log.push(vinfo.versionSource + " and " + filename + " " +
" have different version number.");
}
}
this.run = function (input, callback) {
input.errors.versionErrors = [];
var log = input.errors.versionErrors,
vinfo = {
version: undefined,
needversion: null,
versionSource: null
},
contentxmlhasnoversionnumber;
if (input.file.dom) {
getVersion(input.file.dom, input.file.name, log, vinfo, true);
} else {
// until we know the version number, we cannot claim that
// content.xml needs a version number
getVersion(input.content_xml, "content.xml", log, vinfo, true);
contentxmlhasnoversionnumber = vinfo.version === undefined;
getVersion(input.manifest_xml, "META-INF/manifest.xml", log,
vinfo, true);
getVersion(input.styles_xml, "styles.xml", log, vinfo);
getVersion(input.meta_xml, "meta.xml", log, vinfo);
getVersion(input.settings_xml, "settings.xml", log, vinfo);
if (vinfo.needversion && contentxmlhasnoversionnumber) {
log.push("content.xml has no version number.");
}
}
input.version = vinfo.version;
callback();
};
}
function GetThumbnailJob() {
"use strict";
var base64 = new core.Base64();
this.inputpattern = { file: { entries: [] }, errors: {}, mimetype: "" };
this.outputpattern = { thumbnail: "", errors: { thumbnailErrors: [] } };
this.run = function (input, callback) {
input.thumbnail = null;
input.errors.thumbnailErrors = [];
var i, e = input.file.entries, mime = input.mimetype, thumb = null;
if (/^application\/vnd\.oasis\.opendocument\.text/.test(mime)) {
thumb = "application-vnd.oasis.opendocument.text.png";
} else if (/^application\/vnd\.oasis\.opendocument\.spreadsheet/.test(mime)) {
thumb = "application-vnd.oasis.opendocument.spreadsheet.png";
} else if (/^application\/vnd\.oasis\.opendocument\.presentation/.test(mime)) {
thumb = "application-vnd.oasis.opendocument.presentation.png";
}
for (i = 0; i < e.length; i += 1) {
if (e[i].filename === "Thumbnails/thumbnail.png") {
thumb = "data:image/png;base64," +
base64.convertUTF8ArrayToBase64(e[i].data);
break;
}
}
input.thumbnail = thumb;
callback();
};
}
function RelaxNGJob() {
"use strict";
var parser = new xmldom.RelaxNGParser(),
validators = {};
this.inputpattern = { file: {dom: null}, version: null };
this.outputpattern = { errors: { relaxngErrors: [] } };
function loadValidator(ns, version, callback) {
var rng;
if (ns === "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0") {
if (version === "1.2") {
rng = "OpenDocument-v1.2-cos01-manifest-schema.rng";
} else if (version === "1.1") {
rng = "OpenDocument-manifest-schema-v1.1.rng";
} else if (version === "1.0") {
rng = "OpenDocument-manifest-schema-v1.0-os.rng";
}
} else if (ns === "urn:oasis:names:tc:opendocument:xmlns:office:1.0") {
if (version === "1.2") {
rng = "OpenDocument-v1.2-cos01-schema.rng";
} else if (version === "1.1") {
rng = "OpenDocument-schema-v1.1.rng";
} else if (version === "1.0") {
rng = "OpenDocument-schema-v1.0-os.rng";
}
}
if (rng) {
runtime.loadXML(rng, function (err, dom) {
var relaxng;
if (err) {
runtime.log(err);
} else {
relaxng = new xmldom.RelaxNG();
err = parser.parseRelaxNGDOM(dom, relaxng.makePattern);
if (err) {
runtime.log(err);
} else {
relaxng.init(parser.rootPattern);
}
}
validators[ns] = validators[ns] || {};
validators[ns][version] = relaxng;
callback(relaxng);
});
} else {
callback(null);
}
}
function getValidator(ns, version, callback) {
if (ns === "urn:oasis:names:tc:opendocument:xmlns:office:1.0" ||
ns === "urn:oasis:names:tc:opendocument:xmlns:manifest:1.0") {
if (!version) {
version = "1.1";
}
}
if (validators[ns] && validators[ns][version]) {
return callback(validators[ns][version]);
}
loadValidator(ns, version, callback);
}
function validate(log, dom, filename, version, callback) {
var ns = dom.documentElement.namespaceURI;
getValidator(ns, version, function (relaxng) {
if (!relaxng) {
return callback();
}
var walker = dom.createTreeWalker(dom.firstChild, 0xFFFFFFFF, {
acceptNode: function () {
return NodeFilter.FILTER_ACCEPT;
}
}, false),
err;
err = relaxng.validate(walker, function (err) {
var i;
if (err) {
for (i = 0; i < err.length; i += 1) {
log.push(filename + ": " + err[i]);
}
}
callback();
});
if (err) {
runtime.log(err);
}
});
}
function validateEntries(log, entries, position, version, callback) {
if (position >= entries.length) {
return callback();
}
var e = entries[position];
if (e.dom) {
validate(log, e.dom, e.filename, version, function () {
window.setTimeout(function () {
validateEntries(log, entries, position + 1, version,
callback);
}, 0);
});
} else {
validateEntries(log, entries, position + 1, version, callback);
}
}
this.run = function (input, callback) {
input.errors = input.errors || {};
input.errors.relaxngErrors = [];
runtime.log(input.version);
if (input.file.dom) {
validate(input.errors.relaxngErrors, input.file.dom,
input.file.path, input.version, callback);
return;
}
validateEntries(input.errors.relaxngErrors, input.file.entries, 0,
input.version, callback);
};
}
function DataRenderer(parentelement) {
"use strict";
var doc = parentelement.ownerDocument,
element = doc.createElement("div"),
lastrendertime,
delayedRenderComing,
renderinterval = 300; // minimal milliseconds between renders
function clear(element) {
while (element.firstChild) {
element.removeChild(element.firstChild);
}
}
function addParagraph(div, text) {
var p = doc.createElement("p");
p.appendChild(doc.createTextNode(text));
div.appendChild(p);
}
function addErrors(div, e, active) {
var i, o;
for (i in e) {
if (e.hasOwnProperty(i)) {
o = e[i];
if (active && ((typeof o) === "string"
|| o instanceof String)) {
addParagraph(div, o);
} else if (o && (typeof o) === "object" &&
!(o instanceof Node) &&
!(o.constructor &&
o.constructor === window.Uint8Array)) {
addErrors(div, o, active || i === "errors");
}
}
}
}
function renderFile(data) {
var div = doc.createElement("div"),
h1 = doc.createElement("h1"),
icon = doc.createElement("img");
div.style.clear = "both";
div.appendChild(h1);
div.appendChild(icon);
h1.appendChild(doc.createTextNode(data.file.path));
element.appendChild(div);
if (data.thumbnail) {
icon.src = data.thumbnail;
}
icon.style.width = "128px";
icon.style.float = "left";
icon.style.mozBoxShadow = icon.style.webkitBoxShadow =
icon.style.boxShadow = "3px 3px 4px #000";
icon.style.marginRight = icon.style.marginBottom = "10px";
addParagraph(div, "mimetype: " + data.mimetype);
addParagraph(div, "version: " + data.version);
addParagraph(div, "document representation: " +
((data.file.dom) ? "single XML document" : "package"));
addErrors(div, data, false);
}
function dorender(data) {
clear(element);
var i;
for (i = 0; i < data.length; i += 1) {
renderFile(data[i]);
}
}
this.render = function render(data) {
var now = Date.now();
if (!lastrendertime || now - lastrendertime > renderinterval) {
lastrendertime = now;
dorender(data);
} else if (!delayedRenderComing) {
delayedRenderComing = true;
window.setTimeout(function () {
delayedRenderComing = false;
lastrendertime = now + renderinterval;
dorender(data);
}, renderinterval);
}
};
parentelement.appendChild(element);
}
function JobRunner(datarenderer) {
"use strict";
var jobrunner = this,
jobtypes = [],
data,
busy = false,
todo = [];
jobtypes.push(new UnpackJob());
jobtypes.push(new MimetypeTestJob());
jobtypes.push(new GetThumbnailJob());
jobtypes.push(new VersionTestJob());
jobtypes.push(new ParseXMLJob());
jobtypes.push(new RelaxNGJob());
function run() {
if (busy) {
return;
}
var job = todo.shift();
if (job) {
busy = true;
job.job.run(job.object, function () {
busy = false;
if (!conformsToPattern(job.object, job.job.outputpattern)) {
throw "Job does not give correct output.";
}
datarenderer.render(data);
window.setTimeout(run, 0);
});
}
}
/*jslint unparam: true*/
function update(ignore, callback) {
var i, jobtype, j, inobjects, outobjects;
todo = [];
for (i = 0; i < jobtypes.length; i += 1) {
jobtype = jobtypes[i];
inobjects = getConformingObjects(data, jobtype.inputpattern);
outobjects = getConformingObjects(data, jobtype.outputpattern);
for (j = 0; j < inobjects.length; j += 1) {
if (outobjects.indexOf(inobjects[j]) === -1) {
todo.push({job: jobtype, object: inobjects[j]});
}
}
}
if (todo.length > 0) {
// run update again after all todos are done
todo.push({job: jobrunner, object: null});
}
if (callback) {
callback();
} else {
run();
}
}
/*jslint unparam: false*/
this.run = update;
this.setData = function setData(newdata) {
data = newdata;
if (busy) {
todo = [];
todo.push({job: jobrunner, object: null});
} else {
update();
}
};
}
function LoadingFile(file) {
"use strict";
var data,
error,
readRequests = [];
function load(callback) {
var reader = new FileReader();
reader.onloadend = function (evt) {
data = runtime.byteArrayFromString(evt.target.result, "binary");
error = evt.target.error && String(evt.target.error);
var i = 0;
for (i = 0; i < readRequests.length; i += 1) {
readRequests[i]();
}
readRequests = undefined;
reader = undefined;
callback(error, data);
};
reader.readAsBinaryString(file);
}
this.file = file;
this.readFile = function (callback) {
function readFile() {
if (error) {
return callback(error);
}
if (data) {
return callback(error, data);
}
readRequests.push(readFile);
}
readFile();
};
this.load = load;
}
function Docnosis(element) {
"use strict";
var doc = element.ownerDocument,
form,
diagnoses = doc.createElement("div"),
openedFiles = {},
datarenderer = new DataRenderer(diagnoses),
jobrunner = new JobRunner(datarenderer),
jobrunnerdata = [];
function dragHandler(evt) {
var over = evt.type === "dragover" && evt.target.nodeName !== "INPUT";
if (over || evt.type === "drop") {
evt.stopPropagation();
evt.preventDefault();
}
if (evt.target.style) {
evt.target.style.background = (over ? "#CCCCCC" : "inherit");
}
}
function fileSelectHandler(evt) {
// cancel event and hover styling
dragHandler(evt);
function diagnoseFile(file) {
var loadingfile, path;
path = file.name;
loadingfile = new LoadingFile(file);
openedFiles[path] = loadingfile;
loadingfile.load(function (error, data) {
if (error) {
runtime.log(error);
}
jobrunnerdata.push({file: {
path: path,
data: data
}});
jobrunner.setData(jobrunnerdata);
});
}
// process all File objects
var i, files, div;
files = (evt.target && evt.target.files) ||
(evt.dataTransfer && evt.dataTransfer.files);
if (files) {
for (i = 0; files && i < files.length; i += 1) {
div = doc.createElement("div");
diagnoses.appendChild(div);
diagnoseFile(files[i]);
}
} else {
alert("File(s) could not be opened in this browser.");
}
}
function createForm() {
var newform = doc.createElement("form"),
fieldset = doc.createElement("fieldset"),
legend = doc.createElement("legend"),
input = doc.createElement("input");
newform.appendChild(fieldset);
fieldset.appendChild(legend);
input.setAttribute("type", "file");
input.setAttribute("name", "fileselect[]");
input.setAttribute("multiple", "multiple");
input.addEventListener("change", fileSelectHandler, false);
fieldset.appendChild(input);
fieldset.appendChild(doc.createTextNode("or drop files here"));
legend.appendChild(doc.createTextNode("docnosis"));
newform.addEventListener("dragover", dragHandler, false);
newform.addEventListener("dragleave", dragHandler, false);
newform.addEventListener("drop", fileSelectHandler, false);
return newform;
}
function enhanceRuntime() {
var readFile = runtime.readFile;
runtime.readFile = function (path, encoding, callback) {
if (openedFiles.hasOwnProperty(path)) {
return openedFiles[path].readFile(callback);
}
return readFile(path, encoding, callback);
};
}
form = createForm();
element.appendChild(form);
element.appendChild(diagnoses);
enhanceRuntime();
}