1 /**
  2  * Copyright (C) 2012 KO GmbH <copyright@kogmbh.com>
  3  *
  4  * @licstart
  5  * This file is part of WebODF.
  6  *
  7  * WebODF is free software: you can redistribute it and/or modify it
  8  * under the terms of the GNU Affero General Public License (GNU AGPL)
  9  * as published by the Free Software Foundation, either version 3 of
 10  * the License, or (at your option) any later version.
 11  *
 12  * WebODF is distributed in the hope that it will be useful, but
 13  * WITHOUT ANY WARRANTY; without even the implied warranty of
 14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 15  * GNU Affero General Public License for more details.
 16  *
 17  * You should have received a copy of the GNU Affero General Public License
 18  * along with WebODF.  If not, see <http://www.gnu.org/licenses/>.
 19  * @licend
 20  *
 21  * @source: http://www.webodf.org/
 22  * @source: https://github.com/kogmbh/WebODF/
 23  */
 24 
 25 /*global Node, runtime, xmldom*/
 26 
 27 /**
 28  * RelaxNG can check a DOM tree against a Relax NG schema
 29  * The RelaxNG implementation is currently not complete. Relax NG should not
 30  * report errors on valid DOM trees, but it will not check all constraints that
 31  * a Relax NG file can define. The current implementation does not load external
 32  * parts of a Relax NG file.
 33  * The main purpose of this Relax NG engine is to validate runtime ODF
 34  * documents. The DOM tree is traversed via a TreeWalker. A custom TreeWalker
 35  * implementation can hide parts of a DOM tree. This is useful in WebODF, where
 36  * special elements and attributes in the runtime DOM tree.
 37  */
 38 /**
 39  * @constructor
 40  */
 41 xmldom.RelaxNG2 = function RelaxNG2() {
 42     "use strict";
 43     var start,
 44         validateNonEmptyPattern,
 45         nsmap;
 46 
 47     /**
 48      * @constructor
 49      * @param {!string} error
 50      * @param {Node=} context
 51      */
 52     function RelaxNGParseError(error, context) {
 53         this.message = function () {
 54             if (context) {
 55                 error += (context.nodeType === Node.ELEMENT_NODE) ? " Element " : " Node ";
 56                 error += context.nodeName;
 57                 if (context.nodeValue) {
 58                     error += " with value '" + context.nodeValue + "'";
 59                 }
 60                 error += ".";
 61             }
 62             return error;
 63         };
 64 //        runtime.log("[" + p.slice(0, depth) + this.message() + "]");
 65     }
 66     /**
 67      * @param elementdef
 68      * @param walker
 69      * @param {Element} element
 70      * @return {Array.<RelaxNGParseError>}
 71      */
 72     function validateOneOrMore(elementdef, walker, element) {
 73         // The list of definitions in the elements list should be completely
 74         // traversed at least once. If a second or later round fails, the walker
 75         // should go back to the start of the last successful traversal
 76         var node, i = 0, err;
 77         do {
 78             node = walker.currentNode;
 79             err = validateNonEmptyPattern(elementdef.e[0], walker, element);
 80             i += 1;
 81         } while (!err && node !== walker.currentNode);
 82         if (i > 1) { // at least one round was without error
 83             // set position back to position of before last failed round
 84             walker.currentNode = node;
 85             return null;
 86         }
 87         return err;
 88     }
 89     /**
 90      * @param {!Node} node
 91      * @return {!string}
 92      */
 93     function qName(node) {
 94         return nsmap[node.namespaceURI] + ":" + node.localName;
 95     }
 96     /**
 97      * @param {!Node} node
 98      * @return {!boolean}
 99      */
100     function isWhitespace(node) {
101         return node && node.nodeType === Node.TEXT_NODE && /^\s+$/.test(node.nodeValue);
102     }
103     /**
104      * @param elementdef
105      * @param walker
106      * @param {Element} element
107      * @param {string=} data
108      * @return {Array.<RelaxNGParseError>}
109      */
110     function validatePattern(elementdef, walker, element, data) {
111         if (elementdef.name === "empty") {
112             return null;
113         }
114         return validateNonEmptyPattern(elementdef, walker, element, data);
115     }
116     /**
117      * @param elementdef
118      * @param walker
119      * @param {Element} element
120      * @return {Array.<RelaxNGParseError>}
121      */
122     function validateAttribute(elementdef, walker, element) {
123         if (elementdef.e.length !== 2) {
124             throw "Attribute with wrong # of elements: " + elementdef.e.length;
125         }
126         var att, a, l = elementdef.localnames.length, i;
127         for (i = 0; i < l; i += 1) {
128             // with older browsers getAttributeNS for a non-existing attribute
129             // can return an empty string still, so explicitly check before
130             // if the attribute is set
131             if (element.hasAttributeNS(elementdef.namespaces[i], elementdef.localnames[i])) {
132                 a = element.getAttributeNS(elementdef.namespaces[i], elementdef.localnames[i]);
133             } else {
134                 a = undefined;
135             }
136 
137             if (att !== undefined && a !== undefined) {
138                 return [new RelaxNGParseError("Attribute defined too often.",
139                         element)];
140             }
141             att = a;
142         }
143         if (att === undefined) {
144             return [new RelaxNGParseError("Attribute not found: " +
145                     elementdef.names, element)];
146         }
147         return validatePattern(elementdef.e[1], walker, element, att);
148     }
149     /**
150      * @param elementdef
151      * @param walker
152      * @param {Element} element
153      * @return {Array.<RelaxNGParseError>}
154      */
155     function validateTop(elementdef, walker, element) {
156         // notAllowed not implemented atm
157         return validatePattern(elementdef, walker, element);
158     }
159     /**
160      * Validate an element.
161      * Function forwards the walker until an element is met.
162      * If element if of the right type, it is entered and the validation
163      * continues inside the element. After validation, regardless of whether an
164      * error occurred, the walker is at the same depth in the dom tree.
165      * @param elementdef
166      * @param walker
167      * @return {Array.<RelaxNGParseError>}
168      */
169     function validateElement(elementdef, walker) {
170         if (elementdef.e.length !== 2) {
171             throw "Element with wrong # of elements: " + elementdef.e.length;
172         }
173         // forward until an element is seen, then check the name
174         var /**@type{Node}*/ node = walker.currentNode,
175             /**@type{number}*/ type = node ? node.nodeType : 0,
176             error = null;
177         // find the next element, skip text nodes with only whitespace
178         while (type > Node.ELEMENT_NODE) {
179             if (type !== Node.COMMENT_NODE &&
180                     (type !== Node.TEXT_NODE ||
181                      !/^\s+$/.test(walker.currentNode.nodeValue))) {
182                 return [new RelaxNGParseError("Not allowed node of type " +
183                         type + ".")];
184             }
185             node = walker.nextSibling();
186             type = node ? node.nodeType : 0;
187         }
188         if (!node) {
189             return [new RelaxNGParseError("Missing element " +
190                     elementdef.names)];
191         }
192         if (elementdef.names && elementdef.names.indexOf(qName(node)) === -1) {
193             return [new RelaxNGParseError("Found " + node.nodeName +
194                     " instead of " + elementdef.names + ".", node)];
195         }
196         // the right element was found, now parse the contents
197         if (walker.firstChild()) {
198             // currentNode now points to the first child node of this element
199             error = validateTop(elementdef.e[1], walker, node);
200             // there should be no content left
201             while (walker.nextSibling()) {
202                 type = walker.currentNode.nodeType;
203                 if (!isWhitespace(walker.currentNode) && type !== Node.COMMENT_NODE) {
204                     return [new RelaxNGParseError("Spurious content.",
205                             walker.currentNode)];
206                 }
207             }
208             if (walker.parentNode() !== node) {
209                 return [new RelaxNGParseError("Implementation error.")];
210             }
211         } else {
212             error = validateTop(elementdef.e[1], walker, node);
213         }
214         // move to the next node
215         node = walker.nextSibling();
216         return error;
217     }
218     /**
219      * @param elementdef
220      * @param walker
221      * @param {Element} element
222      * @param {string=} data
223      * @return {Array.<RelaxNGParseError>}
224      */
225     function validateChoice(elementdef, walker, element, data) {
226         // loop through child definitions and return if a match is found
227         if (elementdef.e.length !== 2) {
228             throw "Choice with wrong # of options: " + elementdef.e.length;
229         }
230         var node = walker.currentNode, err;
231         // if the first option is empty, just check the second one for debugging
232         // but the total choice is alwasy ok
233         if (elementdef.e[0].name === "empty") {
234             err = validateNonEmptyPattern(elementdef.e[1], walker, element,
235                     data);
236             if (err) {
237                 walker.currentNode = node;
238             }
239             return null;
240         }
241         err = validatePattern(elementdef.e[0], walker, element, data);
242         if (err) {
243             walker.currentNode = node;
244             err = validateNonEmptyPattern(elementdef.e[1], walker, element,
245                     data);
246         }
247         return err;
248     }
249     /**
250      * @param elementdef
251      * @param walker
252      * @param {Element} element
253      * @return {Array.<RelaxNGParseError>}
254      */
255     function validateInterleave(elementdef, walker, element) {
256         var l = elementdef.e.length, n = [l], err, i, todo = l,
257             donethisround, node, subnode, e;
258         // the interleave is done when all items are 'true' and no
259         while (todo > 0) {
260             donethisround = 0;
261             node = walker.currentNode;
262             for (i = 0; i < l; i += 1) {
263                 subnode = walker.currentNode;
264                 if (n[i] !== true && n[i] !== subnode) {
265                     e = elementdef.e[i];
266                     err = validateNonEmptyPattern(e, walker, element);
267                     if (err) {
268                         walker.currentNode = subnode;
269                         if (n[i] === undefined) {
270                             n[i] = false;
271                         }
272                     } else if (subnode === walker.currentNode ||
273                             // this is a bit dodgy, there should be a rule to
274                             // see if multiple elements are allowed
275                             e.name === "oneOrMore" ||
276                             (e.name === "choice" &&
277                             (e.e[0].name === "oneOrMore" ||
278                                     e.e[1].name === "oneOrMore"))) {
279                         donethisround += 1;
280                         n[i] = subnode; // no error and try this one again later
281                     } else {
282                         donethisround += 1;
283                         n[i] = true; // no error and progress
284                     }
285                 }
286             }
287             if (node === walker.currentNode && donethisround === todo) {
288                 return null;
289             }
290             if (donethisround === 0) {
291                 for (i = 0; i < l; i += 1) {
292                     if (n[i] === false) {
293                         return [new RelaxNGParseError(
294                                 "Interleave does not match.", element
295                         )];
296                     }
297                 }
298                 return null;
299             }
300             todo = 0;
301             for (i = 0; i < l; i += 1) {
302                 if (n[i] !== true) {
303                     todo += 1;
304                 }
305             }
306         }
307         return null;
308     }
309     /**
310      * @param elementdef
311      * @param walker
312      * @param {Element} element
313      * @return {Array.<RelaxNGParseError>}
314      */
315     function validateGroup(elementdef, walker, element) {
316         if (elementdef.e.length !== 2) {
317             throw "Group with wrong # of members: " + elementdef.e.length;
318         }
319         //runtime.log(elementdef.e[0].name + " " + elementdef.e[1].name);
320         return validateNonEmptyPattern(elementdef.e[0], walker, element) ||
321             validateNonEmptyPattern(elementdef.e[1], walker, element);
322     }
323 /*jslint unparam: true*/
324     /**
325      * @param elementdef
326      * @param walker
327      * @param {Element} element
328      * @return {Array.<RelaxNGParseError>}
329      */
330     function validateText(elementdef, walker, element) {
331         var /**@type{Node}*/ node = walker.currentNode,
332             /**@type{number}*/ type = node ? node.nodeType : 0;
333         // find the next element, skip text nodes with only whitespace
334         while (node !== element && type !== 3) {
335             if (type === 1) {
336                 return [new RelaxNGParseError(
337                         "Element not allowed here.", node
338                 )];
339             }
340             node = walker.nextSibling();
341             type = node ? node.nodeType : 0;
342         }
343         walker.nextSibling();
344         return null;
345     }
346 /*jslint unparam: false*/
347     /**
348      * @param elementdef
349      * @param walker
350      * @param {Element} element
351      * @param {string=} data
352      * @return {Array.<RelaxNGParseError>}
353      */
354     validateNonEmptyPattern = function validateNonEmptyPattern(elementdef,
355                 walker, element, data) {
356         var name = elementdef.name, err = null;
357         if (name === "text") {
358             err = validateText(elementdef, walker, element);
359         } else if (name === "data") {
360             err = null; // data not implemented
361         } else if (name === "value") {
362             if (data !== elementdef.text) {
363                 err = [new RelaxNGParseError("Wrong value, should be '" +
364                         elementdef.text + "', not '" + data + "'", element)];
365             }
366         } else if (name === "list") {
367             err = null; // list not implemented
368         } else if (name === "attribute") {
369             err = validateAttribute(elementdef, walker, element);
370         } else if (name === "element") {
371             err = validateElement(elementdef, walker);
372         } else if (name === "oneOrMore") {
373             err = validateOneOrMore(elementdef, walker, element);
374         } else if (name === "choice") {
375             err = validateChoice(elementdef, walker, element, data);
376         } else if (name === "group") {
377             err = validateGroup(elementdef, walker, element);
378         } else if (name === "interleave") {
379             err = validateInterleave(elementdef, walker, element);
380         } else {
381             throw name + " not allowed in nonEmptyPattern.";
382         }
383         return err;
384     };
385     /**
386      * Validate the elements pointed to by the TreeWalker
387      * @param {!TreeWalker} walker
388      * @param {!function(Array.<RelaxNGParseError>):undefined} callback
389      * @return {undefined}
390      */
391     this.validate = function validate(walker, callback) {
392         walker.currentNode = walker.root;
393         var errors = validatePattern(start.e[0], walker,
394                        /**@type{?Element}*/(walker.root));
395         callback(errors);
396     };
397     this.init = function init(start1, nsmap1) {
398         start = start1;
399         nsmap = nsmap1;
400     };
401 };
402