diff --git a/gulpfile.js b/gulpfile.js index 93ca2e468eaf6..1ec7387e26e4f 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -1431,7 +1431,7 @@ function buildLib(defines, dir) { return merge([ gulp.src( [ - "src/{core,display,shared}/*.js", + "src/{core,display,shared}/**/*.js", "!src/shared/{cffStandardStrings,fonts_utils}.js", "src/{pdf,pdf.worker}.js", ], diff --git a/src/core/xfa/builder.js b/src/core/xfa/builder.js new file mode 100644 index 0000000000000..ae30ffed95c75 --- /dev/null +++ b/src/core/xfa/builder.js @@ -0,0 +1,148 @@ +/* Copyright 2021 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { $buildXFAObject, NamespaceIds } from "./namespaces.js"; +import { $cleanup, $onChild, XFAObject } from "./xfa_object.js"; +import { NamespaceSetUp } from "./setup.js"; +import { UnknownNamespace } from "./unknown.js"; +import { warn } from "../../shared/util.js"; + +class Root extends XFAObject { + constructor() { + super(-1, "root", Object.create(null)); + this.element = null; + } + + [$onChild](child) { + this.element = child; + } +} + +class Empty extends XFAObject { + constructor() { + super(-1, "", Object.create(null)); + } + + [$onChild](_) {} +} + +class Builder { + constructor() { + this._namespaceStack = []; + + // Each prefix has its own stack + this._namespacePrefixes = new Map(); + this._namespaces = new Map(); + this._nextNsId = Math.max( + ...Object.values(NamespaceIds).map(({ id }) => id) + ); + this._currentNamespace = new UnknownNamespace(++this._nextNsId); + } + + buildRoot() { + return new Root(); + } + + build({ nsPrefix, name, attributes, namespace, prefixes }) { + const hasNamespaceDef = namespace !== null; + if (hasNamespaceDef) { + // Define the current namespace to use. + this._namespaceStack.push(this._currentNamespace); + this._currentNamespace = this._searchNamespace(namespace); + } + + if (prefixes) { + // The xml node may have namespace prefix definitions + this._addNamespacePrefix(prefixes); + } + + const namespaceToUse = this._getNamespaceToUse(nsPrefix); + const node = + (namespaceToUse && namespaceToUse[$buildXFAObject](name, attributes)) || + new Empty(); + + // In case the node has some namespace things, + // we must pop the different stacks. + if (hasNamespaceDef || prefixes) { + node[$cleanup] = { + hasNamespace: hasNamespaceDef, + prefixes, + }; + } + + return node; + } + + _searchNamespace(nsName) { + let ns = this._namespaces.get(nsName); + if (ns) { + return ns; + } + for (const [name, { check }] of Object.entries(NamespaceIds)) { + if (check(nsName)) { + ns = NamespaceSetUp[name]; + if (ns) { + this._namespaces.set(nsName, ns); + return ns; + } + // The namespace is known but not handled. + break; + } + } + + ns = new UnknownNamespace(++this._nextNsId); + this._namespaces.set(nsName, ns); + return ns; + } + + _addNamespacePrefix(prefixes) { + for (const { prefix, value } of prefixes) { + const namespace = this._searchNamespace(value); + let prefixStack = this._namespacePrefixes.get(prefix); + if (!prefixStack) { + prefixStack = []; + this._namespacePrefixes.set(prefix, prefixStack); + } + prefixStack.push(namespace); + } + } + + _getNamespaceToUse(prefix) { + if (!prefix) { + return this._currentNamespace; + } + const prefixStack = this._namespacePrefixes.get(prefix); + if (prefixStack && prefixStack.length > 0) { + return prefixStack[prefixStack.length - 1]; + } + + warn(`Unknown namespace prefix: ${prefix}.`); + return null; + } + + clean(data) { + const { hasNamespace, prefixes } = data; + if (hasNamespace) { + this._currentNamespace = this._namespaceStack.pop(); + } + if (prefixes) { + prefixes.forEach(({ prefix }) => { + this._namespacePrefixes.get(prefix).pop(); + }); + } + } +} + +export { Builder }; diff --git a/src/core/xfa/config.js b/src/core/xfa/config.js new file mode 100644 index 0000000000000..8d8819d78fe55 --- /dev/null +++ b/src/core/xfa/config.js @@ -0,0 +1,193 @@ +/* Copyright 2021 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { $buildXFAObject, NamespaceIds } from "./namespaces.js"; +import { + IntegerObject, + OptionObject, + StringObject, + XFAObject, +} from "./xfa_object.js"; + +const CONFIG_NS_ID = NamespaceIds.config.id; + +class Acrobat extends XFAObject { + constructor(attributes) { + super(CONFIG_NS_ID, "acrobat", /* hasChildren = */ true); + this.acrobat7 = null; + this.autoSave = null; + this.common = null; + this.validate = null; + this.validateApprovalSignatures = null; + this.submitUrl = []; + } +} + +class Acrobat7 extends XFAObject { + constructor(attributes) { + super(CONFIG_NS_ID, "acrobat7", /* hasChildren = */ true); + this.dynamicRender = null; + } +} + +class AdobeExtensionLevel extends IntegerObject { + constructor(attributes) { + super(CONFIG_NS_ID, "adobeExtensionLevel", 0, n => n >= 1 && n <= 8); + } +} + +class AutoSave extends OptionObject { + constructor(attributes) { + super(CONFIG_NS_ID, "autoSave", ["disabled", "enabled"]); + } +} + +class Config extends XFAObject { + constructor(attributes) { + super(CONFIG_NS_ID, "config", /* hasChildren = */ true); + this.acrobat = null; + this.present = null; + this.trace = null; + this.agent = []; + } +} + +class DynamicRender extends OptionObject { + constructor(attributes) { + super(CONFIG_NS_ID, "dynamicRender", ["forbidden", "required"]); + } +} + +class Present extends XFAObject { + constructor(attributes) { + super(CONFIG_NS_ID, "present", /* hasChildren = */ true); + this.behaviorOverride = null; + this.cache = null; + this.common = null; + this.copies = null; + this.destination = null; + this.incrementalMerge = null; + this.layout = null; + this.output = null; + this.overprint = null; + this.pagination = null; + this.paginationOverride = null; + this.script = null; + this.validate = null; + this.xdp = null; + this.driver = []; + this.labelPrinter = []; + this.pcl = []; + this.pdf = []; + this.ps = []; + this.submitUrl = []; + this.webClient = []; + this.zpl = []; + } +} + +class Pdf extends XFAObject { + constructor(attributes) { + super(CONFIG_NS_ID, "pdf", /* hasChildren = */ true); + this.name = attributes.name || ""; + this.adobeExtensionLevel = null; + this.batchOutput = null; + this.compression = null; + this.creator = null; + this.encryption = null; + this.fontInfo = null; + this.interactive = null; + this.linearized = null; + this.openAction = null; + this.pdfa = null; + this.producer = null; + this.renderPolicy = null; + this.scriptModel = null; + this.silentPrint = null; + this.submitFormat = null; + this.tagged = null; + this.version = null; + this.viewerPreferences = null; + this.xdc = null; + } +} + +class SubmitUrl extends StringObject { + constructor(attributes) { + super(CONFIG_NS_ID, "submitUrl"); + } +} + +class Validate extends OptionObject { + constructor(attributes) { + super(CONFIG_NS_ID, "validate", [ + "preSubmit", + "prePrint", + "preExecute", + "preSave", + ]); + } +} + +class ConfigNamespace { + static [$buildXFAObject](name, attributes) { + if (ConfigNamespace.hasOwnProperty(name)) { + return ConfigNamespace[name](attributes); + } + return undefined; + } + + static acrobat(attrs) { + return new Acrobat(attrs); + } + + static acrobat7(attrs) { + return new Acrobat7(attrs); + } + + static adobeExtensionLevel(attrs) { + return new AdobeExtensionLevel(attrs); + } + + static autoSave(attrs) { + return new AutoSave(attrs); + } + + static config(attrs) { + return new Config(attrs); + } + + static dynamicRender(attrs) { + return new DynamicRender(attrs); + } + + static pdf(attrs) { + return new Pdf(attrs); + } + + static present(attrs) { + return new Present(attrs); + } + + static submitUrl(attrs) { + return new SubmitUrl(attrs); + } + + static validate(attrs) { + return new Validate(attrs); + } +} + +export { ConfigNamespace }; diff --git a/src/core/xfa/namespaces.js b/src/core/xfa/namespaces.js new file mode 100644 index 0000000000000..b8fb5484b81c1 --- /dev/null +++ b/src/core/xfa/namespaces.js @@ -0,0 +1,81 @@ +/* Copyright 2021 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +const $buildXFAObject = Symbol(); + +const NamespaceIds = { + config: { + id: 0, + check: ns => ns.startsWith("http://www.xfa.org/schema/xci/"), + }, + connectionSet: { + id: 1, + check: ns => ns.startsWith("http://www.xfa.org/schema/xfa-connection-set/"), + }, + datasets: { + id: 2, + check: ns => ns.startsWith("http://www.xfa.org/schema/xfa-data/"), + }, + form: { + id: 3, + check: ns => ns.startsWith("http://www.xfa.org/schema/xfa-form/"), + }, + localeSet: { + id: 4, + check: ns => ns.startsWith("http://www.xfa.org/schema/xfa-locale-set/"), + }, + pdf: { + id: 5, + check: ns => ns === "http://ns.adobe.com/xdp/pdf/", + }, + signature: { + id: 6, + check: ns => ns === "http://www.w3.org/2000/09/xmldsig#", + }, + sourceSet: { + id: 7, + check: ns => ns.startsWith("http://www.xfa.org/schema/xfa-source-set/"), + }, + stylesheet: { + id: 8, + check: ns => ns === "http://www.w3.org/1999/XSL/Transform", + }, + template: { + id: 9, + check: ns => ns.startsWith("http://www.xfa.org/schema/xfa-template/"), + }, + xdc: { + id: 10, + check: ns => ns.startsWith("http://www.xfa.org/schema/xdc/"), + }, + xdp: { + id: 11, + check: ns => ns === "http://ns.adobe.com/xdp/", + }, + xfdf: { + id: 12, + check: ns => ns === "http://ns.adobe.com/xfdf/", + }, + xhtml: { + id: 13, + check: ns => ns === "http://www.w3.org/1999/xhtml", + }, + xmpmeta: { + id: 14, + check: ns => ns === "http://ns.adobe.com/xmpmeta/", + }, +}; + +export { $buildXFAObject, NamespaceIds }; diff --git a/src/core/xfa/parser.js b/src/core/xfa/parser.js new file mode 100644 index 0000000000000..39fd6ec39796f --- /dev/null +++ b/src/core/xfa/parser.js @@ -0,0 +1,126 @@ +/* Copyright 2021 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { $clean, $finalize, $onChild, $onText } from "./xfa_object.js"; +import { XMLParserBase, XMLParserErrorCode } from "../../shared/xml_parser.js"; +import { Builder } from "./builder.js"; +import { warn } from "../../shared/util.js"; + +class XFAParser extends XMLParserBase { + constructor() { + super(); + this._builder = new Builder(); + this._stack = []; + this._current = this._builder.buildRoot(); + this._errorCode = XMLParserErrorCode.NoError; + this._whiteRegex = /^\s+$/; + } + + parse(data) { + this.parseXml(data); + + if (this._errorCode !== XMLParserErrorCode.NoError) { + return undefined; + } + + return this._current.element; + } + + onText(text) { + if (this._whiteRegex.test(text)) { + return; + } + this._current[$onText](text); + } + + onCdata(text) { + this._current[$onText](text); + } + + _mkAttributes(attributes, tagName) { + // Transform attributes into an object and get out + // namespaces information. + let namespace = null; + let prefixes = null; + const attributeObj = Object.create(null); + for (const { name, value } of attributes) { + if (name === "xmlns") { + if (!namespace) { + namespace = value; + } else { + warn(`XFA - multiple namespace definition in <${tagName}>`); + } + } else if (name.startsWith("xmlns:")) { + const prefix = name.substring("xmlns:".length); + if (!prefixes) { + prefixes = []; + } + prefixes.push({ prefix, value }); + } else { + attributeObj[name] = value; + } + } + + return [namespace, prefixes, attributeObj]; + } + + _getNameAndPrefix(name) { + const i = name.indexOf(":"); + if (i === -1) { + return [name, null]; + } + return [name.substring(i + 1), name.substring(0, i)]; + } + + onBeginElement(tagName, attributes, isEmpty) { + const [namespace, prefixes, attributesObj] = this._mkAttributes( + attributes, + tagName + ); + const [name, nsPrefix] = this._getNameAndPrefix(tagName); + const node = this._builder.build({ + nsPrefix, + name, + attributes: attributesObj, + namespace, + prefixes, + }); + + if (isEmpty) { + // No children: just push the node into its parent. + node[$finalize](); + this._current[$onChild](node); + node[$clean](this._builder); + return; + } + + this._stack.push(this._current); + this._current = node; + } + + onEndElement(name) { + const node = this._current; + node[$finalize](); + this._current = this._stack.pop(); + this._current[$onChild](node); + node[$clean](this._builder); + } + + onError(code) { + this._errorCode = code; + } +} + +export { XFAParser }; diff --git a/src/core/xfa/setup.js b/src/core/xfa/setup.js new file mode 100644 index 0000000000000..d285d5ed740cd --- /dev/null +++ b/src/core/xfa/setup.js @@ -0,0 +1,24 @@ +/* Copyright 2021 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigNamespace } from "./config.js"; +import { XdpNamespace } from "./xdp.js"; + +const NamespaceSetUp = { + config: ConfigNamespace, + xdp: XdpNamespace, +}; + +export { NamespaceSetUp }; diff --git a/src/core/xfa/unknown.js b/src/core/xfa/unknown.js new file mode 100644 index 0000000000000..147863c996759 --- /dev/null +++ b/src/core/xfa/unknown.js @@ -0,0 +1,29 @@ +/* Copyright 2021 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { $buildXFAObject } from "./namespaces.js"; +import { XmlObject } from "./xfa_object.js"; + +class UnknownNamespace { + constructor(nsId) { + this.namespaceId = nsId; + } + + [$buildXFAObject](name, attributes) { + return new XmlObject(this.namespaceId, name, attributes); + } +} + +export { UnknownNamespace }; diff --git a/src/core/xfa/utils.js b/src/core/xfa/utils.js new file mode 100644 index 0000000000000..c7e238506c4a6 --- /dev/null +++ b/src/core/xfa/utils.js @@ -0,0 +1,47 @@ +/* Copyright 2021 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +function getInteger({ data, defaultValue, validate }) { + if (!data) { + return defaultValue; + } + data = data.trim(); + const n = parseInt(data, 10); + if (!isNaN(n) && validate(n)) { + return n; + } + return defaultValue; +} + +function getKeyword({ data, defaultValue, validate }) { + if (!data) { + return defaultValue; + } + data = data.trim(); + if (validate(data)) { + return data; + } + return defaultValue; +} + +function getStringOption(data, options) { + return getKeyword({ + data, + defaultValue: options[0], + validate: k => options.includes(k), + }); +} + +export { getInteger, getKeyword, getStringOption }; diff --git a/src/core/xfa/xdp.js b/src/core/xfa/xdp.js new file mode 100644 index 0000000000000..69c110b2377c3 --- /dev/null +++ b/src/core/xfa/xdp.js @@ -0,0 +1,58 @@ +/* Copyright 2021 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { $buildXFAObject, NamespaceIds } from "./namespaces.js"; +import { + $namespaceId, + $nodeName, + $onChildCheck, + XFAObject, +} from "./xfa_object.js"; + +const XDP_NS_ID = NamespaceIds.xdp.id; + +class Xdp extends XFAObject { + constructor(attributes) { + super(XDP_NS_ID, "xdp", /* hasChildren = */ true); + this.uuid = attributes.uuid || ""; + this.timeStamp = attributes.timeStamp || ""; + this.config = null; + this.connectionSet = null; + this.datasets = null; + this.localeSet = null; + this.stylesheet = []; + this.template = null; + } + + [$onChildCheck](child) { + const ns = NamespaceIds[child[$nodeName]]; + return ns && child[$namespaceId] === ns.id; + } +} + +class XdpNamespace { + static [$buildXFAObject](name, attributes) { + if (XdpNamespace.hasOwnProperty(name)) { + return XdpNamespace[name](attributes); + } + return undefined; + } + + static xdp(attributes) { + return new Xdp(attributes); + } +} + +export { XdpNamespace }; diff --git a/src/core/xfa/xfa_object.js b/src/core/xfa/xfa_object.js new file mode 100644 index 0000000000000..8077dbbb2ebd5 --- /dev/null +++ b/src/core/xfa/xfa_object.js @@ -0,0 +1,244 @@ +/* Copyright 2021 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getInteger, getKeyword } from "./utils.js"; +import { warn } from "../../shared/util.js"; + +// We use these symbols to avoid name conflict between tags +// and properties/methods names. +const $clean = Symbol(); +const $cleanup = Symbol(); +const $content = Symbol("content"); +const $dump = Symbol(); +const $finalize = Symbol(); +const $isTransparent = Symbol(); +const $namespaceId = Symbol("namespaceId"); +const $nodeName = Symbol("nodeName"); +const $onChild = Symbol(); +const $onChildCheck = Symbol(); +const $onText = Symbol(); + +const _defaultValue = Symbol(); +const _hasChildren = Symbol(); +const _options = Symbol(); +const _parent = Symbol(); +const _validator = Symbol(); + +class XFAObject { + constructor(nsId, name, hasChildren = false) { + this[$namespaceId] = nsId; + this[$nodeName] = name; + this[_hasChildren] = hasChildren; + this[_parent] = null; + } + + [$onChild](child) { + if (!this[_hasChildren] || !this[$onChildCheck](child)) { + return; + } + + const name = child[$nodeName]; + const node = this[name]; + if (Array.isArray(node)) { + node.push(child); + child[_parent] = this; + } else if (node === null) { + this[name] = child; + child[_parent] = this; + } else { + warn(`XFA - node "${this[$nodeName]}" accepts only one child: ${name}`); + } + } + + [$onChildCheck](child) { + return ( + this.hasOwnProperty(child[$nodeName]) && + child[$namespaceId] === this[$namespaceId] + ); + } + + [$onText](_) {} + + [$finalize]() {} + + [$clean](builder) { + delete this[_hasChildren]; + if (this[$cleanup]) { + builder.clean(this[$cleanup]); + delete this[$cleanup]; + } + } + + [$isTransparent]() { + return this.name === ""; + } + + [$dump]() { + const dumped = Object.create(null); + if (this[$content]) { + dumped.$content = this[$content]; + } + + for (const name of Object.getOwnPropertyNames(this)) { + const value = this[name]; + if (value === null || (Array.isArray(value) && value.length === 0)) { + continue; + } + if (value instanceof XFAObject) { + dumped[name] = value[$dump](); + } else if (Array.isArray(value)) { + if (value[0] instanceof XFAObject) { + dumped[name] = + value.length === 1 ? value[0][$dump]() : value.map(x => x[$dump]()); + } else { + dumped[name] = value; + } + } else { + dumped[name] = value; + } + } + + return dumped; + } +} + +class XmlObject extends XFAObject { + constructor(nsId, name, attributes = Object.create(null)) { + super(nsId, name); + this[$content] = ""; + if (name !== "#text") { + this.attributes = attributes; + this.children = []; + } + } + + [$onChild](child) { + if (this[$content]) { + const node = new XmlObject(this[$namespaceId], "#text"); + node[$content] = this[$content]; + this[$content] = ""; + this.children.push(node); + } + this.children.push(child); + } + + [$onText](str) { + this[$content] += str; + } + + [$finalize]() { + if (this[$content] && this.children.length > 0) { + const node = new XmlObject(this[$namespaceId], "#text"); + node[$content] = this[$content]; + this.children.push(node); + delete this[$content]; + } + } +} + +class ContentObject extends XFAObject { + constructor(nsId, name) { + super(nsId, name); + this[$content] = ""; + } + + [$onText](text) { + this[$content] += text; + } + + [$finalize]() {} +} + +class OptionObject extends ContentObject { + constructor(nsId, name, options) { + super(nsId, name); + this[_options] = options; + } + + [$finalize]() { + this[$content] = getKeyword({ + data: this[$content], + defaultValue: this[_options][0], + validate: k => this[_options].includes(k), + }); + } + + [$clean](builder) { + super[$clean](builder); + delete this[_options]; + } +} + +class StringObject extends ContentObject { + [$finalize]() { + this[$content] = this[$content].trim(); + } +} + +class IntegerObject extends ContentObject { + constructor(nsId, name, defaultValue, validator) { + super(nsId, name); + this[_defaultValue] = defaultValue; + this[_validator] = validator; + } + + [$finalize]() { + this[$content] = getInteger({ + data: this[$content], + defaultValue: this[_defaultValue], + validate: this[_validator], + }); + } + + [$clean](builder) { + super[$clean](builder); + delete this[_defaultValue]; + delete this[_validator]; + } +} + +class Option01 extends IntegerObject { + constructor(nsId, name) { + super(nsId, name, 0, n => n === 1); + } +} + +class Option10 extends IntegerObject { + constructor(nsId, name) { + super(nsId, name, 1, n => n === 0); + } +} + +export { + $clean, + $cleanup, + $content, + $dump, + $finalize, + $isTransparent, + $namespaceId, + $nodeName, + $onChild, + $onChildCheck, + $onText, + ContentObject, + IntegerObject, + Option01, + Option10, + OptionObject, + StringObject, + XFAObject, + XmlObject, +}; diff --git a/src/shared/xml_parser.js b/src/shared/xml_parser.js index 582280b6a68bc..6f3c94b8015db 100644 --- a/src/shared/xml_parser.js +++ b/src/shared/xml_parser.js @@ -63,6 +63,8 @@ class XMLParserBase { return "&"; case "quot": return '"'; + case "apos": + return "'"; } return this.onResolveEntity(entity); }); @@ -455,14 +457,6 @@ class SimpleXMLParser extends XMLParserBase { return { documentElement }; } - onResolveEntity(name) { - switch (name) { - case "apos": - return "'"; - } - return super.onResolveEntity(name); - } - onText(text) { if (isWhitespaceString(text)) { return; @@ -509,4 +503,4 @@ class SimpleXMLParser extends XMLParserBase { } } -export { SimpleDOMNode, SimpleXMLParser }; +export { SimpleDOMNode, SimpleXMLParser, XMLParserBase, XMLParserErrorCode }; diff --git a/test/unit/clitests.json b/test/unit/clitests.json index 1f6ed900ff5a0..1d7b19bad8081 100644 --- a/test/unit/clitests.json +++ b/test/unit/clitests.json @@ -39,6 +39,7 @@ "unicode_spec.js", "util_spec.js", "writer_spec.js", + "xfa_parser_spec.js", "xml_spec.js" ] } diff --git a/test/unit/jasmine-boot.js b/test/unit/jasmine-boot.js index 5d436d08a5014..55e6bdfe7c7d9 100644 --- a/test/unit/jasmine-boot.js +++ b/test/unit/jasmine-boot.js @@ -85,6 +85,7 @@ async function initializePDFJS(callback) { "pdfjs-test/unit/unicode_spec.js", "pdfjs-test/unit/util_spec.js", "pdfjs-test/unit/writer_spec.js", + "pdfjs-test/unit/xfa_parser_spec.js", "pdfjs-test/unit/xml_spec.js", ].map(function (moduleName) { // eslint-disable-next-line no-unsanitized/method diff --git a/test/unit/xfa_parser_spec.js b/test/unit/xfa_parser_spec.js new file mode 100644 index 0000000000000..47eb2ed1dcfee --- /dev/null +++ b/test/unit/xfa_parser_spec.js @@ -0,0 +1,93 @@ +/* Copyright 2020 Mozilla Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { $dump } from "../../src/core/xfa/xfa_object.js"; +import { XFAParser } from "../../src/core/xfa/parser.js"; + +describe("XFAParser", function () { + describe("Parse XFA", function () { + it("should parse a xfa document and create an object to represent it", function () { + const xml = ` + + + + + + + 7 + + + foobar + + + http://a.b.c + + + forbidden + + + enabled + + http://d.e.f + + http://g.h.i + foobar + + + + `; + const root = new XFAParser().parse(xml); + const expected = { + uuid: "1234", + timeStamp: "", + config: { + acrobat: { + acrobat7: { + dynamicRender: { + $content: "forbidden", + }, + }, + autoSave: { + $content: "enabled", + }, + validate: { + $content: "preSubmit", + }, + submitUrl: [ + { + $content: "http://a.b.c", + }, + { + $content: "http://d.e.f", + }, + { + $content: "http://g.h.i", + }, + ], + }, + present: { + pdf: { + name: "hello", + adobeExtensionLevel: { + $content: 7, + }, + }, + }, + }, + }; + expect(root[$dump]()).toEqual(expected); + }); + }); +});