UNPKG

@nodecfdi/cfdiutils-core

Version:
648 lines (497 loc) 15.8 kB
import { AbstractBaseRetriever, NodeDownloader, XsltRetriever, XsdRetriever } from '@nodecfdi/xml-resource-retriever'; import { join } from 'path'; import { unlink, existsSync, statSync, accessSync, constants, write, writeFileSync } from 'fs'; import { Certificate } from '@nodecfdi/credentials'; import { Xml } from '@nodecfdi/cfdiutils-common'; import { Mixin } from 'ts-mixer'; import { open, cleanupSync } from 'temp'; import { exec } from 'child_process'; class CerRetriever extends AbstractBaseRetriever { constructor(basePath, downloader) { super(basePath, downloader); } checkIsValidDownloadedFile(source, localPath) { return new Promise((resolve, reject) => { try { Certificate.openFile(localPath); return resolve(); } catch (e) { unlink(localPath, () => { return reject(new Error(`The source ${source} is not a cer file`)); }); } }); } async retrieve(url) { this.clearHistory(); const localFileName = await this.download(url); this.addToHistory(url, localFileName); return localFileName; } } class CfdiDefaultLocations { static location(version) { if ('4.0' === version) { return CfdiDefaultLocations.XSLT_40; } if ('3.3' === version) { return CfdiDefaultLocations.XSLT_33; } if ('3.2' === version) { return CfdiDefaultLocations.XSLT_32; } throw new Error(`Cannot get the default xslt location for version ${version}`); } } CfdiDefaultLocations.XSLT_32 = 'http://www.sat.gob.mx/sitio_internet/cfd/3/cadenaoriginal_3_2/cadenaoriginal_3_2.xslt'; CfdiDefaultLocations.XSLT_33 = 'http://www.sat.gob.mx/sitio_internet/cfd/3/cadenaoriginal_3_3/cadenaoriginal_3_3.xslt'; CfdiDefaultLocations.XSLT_40 = 'http://www.sat.gob.mx/sitio_internet/cfd/4/cadenaoriginal_4_0/cadenaoriginal_4_0.xslt'; /** * XmlResolver - Class to download xml resources from internet to local paths */ class XmlResolver { constructor(localPath = null, downloader = null) { this._localPath = ''; this._downloader = void 0; this.setLocalPath(localPath); this.setDownloader(downloader); } static defaultLocalPath() { // drop 2 dirs: src/xml-resolver return join(__dirname, '..', '..', 'build', 'resources'); } /** * Set the localPath to the specified value. * If localPath is null then the value of defaultLocalPath is used. * * @param localPath - values: '' -- no resolve, null -- default path, anything else is the path */ setLocalPath(localPath = null) { if (localPath === null) { localPath = XmlResolver.defaultLocalPath(); } this._localPath = localPath; } /** * Return the configured localPath. * An empty string means that it is not configured and method resolve will return the same url as received */ getLocalPath() { return this._localPath; } /** * Return when a local path has been set */ hasLocalPath() { return '' !== this._localPath; } /** * Set the downloader object. * If send a null value the object return by defaultDownloader will be set. * * @param downloader - downloader implementation */ setDownloader(downloader = null) { if (!downloader) { downloader = XmlResolver.defaultDownloader(); } this._downloader = downloader; } static defaultDownloader() { return new NodeDownloader(); } getDownloader() { return this._downloader; } async resolve(resource, type = '') { if (!this.hasLocalPath()) { return resource; } if ('' == type) { type = this.obtainTypeFromUrl(resource); } else { type = type.toUpperCase(); } const retriever = this.newRetriever(type); if (!retriever) { throw new Error(`Unable to handle the resource (Type: ${type}) ${resource}`); } const local = retriever.buildPath(resource); if (!existsSync(local)) { await retriever.retrieve(resource); } return local; } obtainTypeFromUrl(url) { if (this.isResourceExtension(url, 'xsd')) { return XmlResolver.TYPE_XSD; } if (this.isResourceExtension(url, 'xslt')) { return XmlResolver.TYPE_XSLT; } if (this.isResourceExtension(url, 'cer')) { return XmlResolver.TYPE_CER; } return ''; } isResourceExtension(resource, extension) { extension = `.${extension}`; if (extension.length > resource.length) { return false; } return resource.toLowerCase().endsWith(extension); } /** * Create a new Retriever depending on the type parameter, only allow TYPE_XSLT and TYPE_XSD * * @param type - */ newRetriever(type) { if (!this.hasLocalPath()) { throw new Error('Cannot create a retriever if no local path was found'); } if (XmlResolver.TYPE_XSLT === type) { return this.newXsltRetriever(); } if (XmlResolver.TYPE_XSD === type) { return this.newXsdRetriever(); } if (XmlResolver.TYPE_CER === type) { return this.newCerRetriever(); } return undefined; } newXsltRetriever() { return new XsltRetriever(this.getLocalPath(), this.getDownloader()); } newXsdRetriever() { return new XsdRetriever(this.getLocalPath(), this.getDownloader()); } newCerRetriever() { return new CerRetriever(this.getLocalPath(), this.getDownloader()); } resolveCadenaOrigenLocation(version) { return this.resolve(CfdiDefaultLocations.location(version), XmlResolver.TYPE_XSLT); } } XmlResolver.TYPE_XSD = 'XSD'; XmlResolver.TYPE_XSLT = 'XSLT'; XmlResolver.TYPE_CER = 'CER'; class XmlResolverPropertyTrait { constructor() { this._xmlResolver = null; } getXmlResolver() { if (!(this._xmlResolver instanceof XmlResolver)) { throw new Error('There is not current xmlResolver'); } return this._xmlResolver; } hasXmlResolver() { return this._xmlResolver instanceof XmlResolver; } setXmlResolver(xmlResolver = null) { this._xmlResolver = xmlResolver; } } class DomElementContainer { constructor(element) { this._element = void 0; this._element = element; } getAttributeValue(attribute) { return this._element.getAttribute(attribute) || ''; } } class NodeContainer { constructor(node) { this._node = void 0; this._node = node; } getAttributeValue(attribute) { return this._node.get(attribute); } } class VersionDiscoverer { discover(container) { for (const [versionNumber, attribute] of Object.entries(this.rules())) { const currentValue = container.getAttributeValue(attribute); if (versionNumber === currentValue) { return versionNumber; } } return ''; } getFromDOMElement(element) { return this.discover(new DomElementContainer(element)); } getFromDOMDocument(document) { return this.getFromDOMElement(Xml.documentElement(document)); } getFromNode(node) { return this.discover(new NodeContainer(node)); } getFromXmlString(contents) { return this.getFromDOMDocument(Xml.newDocumentContent(contents)); } } class TfdVersion extends VersionDiscoverer { static createDiscoverer() { return new TfdVersion(); } rules() { return { '1.1': 'Version', '1.0': 'version' }; } } class XsltBuilderPropertyTrait { constructor() { this._xsltBuilder = null; } getXsltBuilder() { if (!this._xsltBuilder) { throw new Error('There is no current xsltBuilder'); } return this._xsltBuilder; } hasXsltBuilder() { return !!this._xsltBuilder; } setXsltBuilder(xsltBuilder) { this._xsltBuilder = xsltBuilder; } } class AbstractXsltBuilder { assertBuildArgument(xmlContent, xsltLocation) { if ('' === xmlContent) { throw new Error('The XML content to transform is empty'); } if ('' === xsltLocation) { throw new Error('Xslt location was not set'); } return ''; } } class XsltBuildException extends Error {} class SaxonbCliBuilder extends AbstractXsltBuilder { constructor(executablePath) { super(); this._executablePath = void 0; this.setExecutablePath(executablePath); } setExecutablePath(executablePath) { if ('' === executablePath) { throw new SyntaxError('The executable path for SaxonB cannot be empty'); } this._executablePath = executablePath; } getExecutablePath() { return this._executablePath; } retrieveValidExecutable() { const executable = this.getExecutablePath(); if (!existsSync(executable)) { throw new XsltBuildException('The executable path for SaxonB does not exists'); } if (statSync(executable).isDirectory()) { throw new XsltBuildException('The executable path for SaxonB is a directory'); } try { accessSync(executable, constants.X_OK); } catch (e) { throw new XsltBuildException('The executable path for SaxonB is not executable'); } return executable; } isValidXslt(xsltLocation) { if (!existsSync(xsltLocation)) { throw new XsltBuildException('Xslt location was not found'); } } build(xmlContent, xsltLocation) { this.assertBuildArgument(xmlContent, xsltLocation); const executable = this.retrieveValidExecutable(); this.isValidXslt(xsltLocation); return new Promise((resolve, reject) => { open({ suffix: 'xml' }, (err, info) => { if (err) { return reject(new XsltBuildException('Error while loading the xml content')); } write(info.fd, xmlContent, error => { if (error) { return reject(new XsltBuildException('Error while loading the xml content')); } const args = []; args.push(`-s:${info.path}`); args.push(`-xsl:${xsltLocation}`); args.push('-warnings:silent'); const finalCommand = `${executable} ${args.join(' ')}`; exec(finalCommand, { maxBuffer: 1024 * 1024 * 100 // 100MB }, (errorCmd, stdout, stderr) => { cleanupSync(); if (errorCmd) { return reject(new XsltBuildException(`Transformation error: ${stderr}`)); } if ('<?xml version="1.0" encoding="UTF-8"?>' === stdout.trim()) { return reject(new XsltBuildException('Transformation error')); } return resolve(stdout.trim()); }); }); }); }); } } class TfdCadenaDeOrigen extends Mixin(XmlResolverPropertyTrait, XsltBuilderPropertyTrait) { constructor(xmlResolver = null, xsltBuilder = null) { super(); this.setXmlResolver(xmlResolver || new XmlResolver()); this.setXsltBuilder(xsltBuilder || new SaxonbCliBuilder('/usr/bin/saxonb-xslt')); } async build(tfdXmlString, version = '') { // this will throw an exception if no resolver is set const resolver = this.getXmlResolver(); // obtain version if it was not set if (version === '') { version = new TfdVersion().getFromXmlString(tfdXmlString); } // get remote location of the xslt const defaultXslt = TfdCadenaDeOrigen.xsltLocation(version); // get local xslt const localXsd = await resolver.resolve(defaultXslt); // return transformation return this.getXsltBuilder().build(tfdXmlString, localXsd); } static xsltLocation(version) { if ('1.1' === version) { return TfdCadenaDeOrigen.TFD_11; } if ('1.0' === version) { return TfdCadenaDeOrigen.TFD_10; } throw new Error(`Cannot get the xslt location for version ${version}`); } } TfdCadenaDeOrigen.TFD_10 = 'http://www.sat.gob.mx/sitio_internet/timbrefiscaldigital/cadenaoriginal_TFD_1_0.xslt'; TfdCadenaDeOrigen.TFD_11 = 'http://www.sat.gob.mx/sitio_internet/cfd/TimbreFiscalDigital/cadenaoriginal_TFD_1_1.xslt'; class CertificadoPropertyTrait { constructor() { this._certificado = null; } hasCertificado() { return this._certificado instanceof Certificate; } getCertificado() { if (!(this._certificado instanceof Certificate)) { throw new Error('There is no current certificado'); } return this._certificado; } setCertificado(certificado = null) { this._certificado = certificado; } } class NodeCertificado { constructor(comprobante) { this._comprobante = void 0; this._comprobante = comprobante; } /** * Extract the certificate from Comprobante.certificado * If the node does not exist return an empty string * The returned string is no longer base64 encoded * * @throws Error If the certificado attribute is not a valid base64 encoded string */ extract() { const version = this.getVersion(); let attr = ''; if ('3.2' === version) { attr = 'certificado'; } else if ('3.3' === version) { attr = 'Certificado'; } else if ('4.0' === version) { attr = 'Certificado'; } else { throw new Error('Unsupported or unknown version'); } const certificateBase64 = this._comprobante.searchAttribute(attr); if ('' === certificateBase64) { return ''; } let certificateBin = ''; try { certificateBin = Buffer.from(certificateBase64, 'base64').toString('binary'); } catch (e) {// ignore } if ('' === certificateBin) { throw new Error('The certificado attribute is not a valid base64 encoded string'); } return certificateBin; } getVersion() { if ('3.2' === this._comprobante.searchAttribute('version')) { return '3.2'; } if ('3.3' === this._comprobante.searchAttribute('Version')) { return '3.3'; } if ('4.0' === this._comprobante.searchAttribute('Version')) { return '4.0'; } return ''; } /** * Extract and save the certificate into a specified location * * @see {@link NodeCertificado.extract} * * @param filename - * * @throws Error If the filename to store the certificate is empty * @throws Error If the certificado attribute is empty * @throws Error If cannot write the contents of the certificate */ save(filename) { if ('' === filename) { throw new Error('The filename to store the certificate is empty'); } const certificado = this.extract(); if ('' === certificado) { throw new Error('The certificado attribute is empty'); } try { writeFileSync(filename, certificado); } catch (e) { throw new Error(`Unable to write the certificate contents into ${filename}`); } } obtain() { const certificado = this.extract(); if ('' === certificado) { throw new Error('The certificado attribute is empty'); } return new Certificate(certificado); } } class SatCertificateNumber { constructor(id) { this._id = void 0; if (!SatCertificateNumber.isValidCertificateNumber(id)) { throw new Error('The certificate number is not correct'); } this._id = id; } number() { return this._id; } remoteUrl() { return ['https://rdc.sat.gob.mx/rccf', `/${this._id.substring(0, 6)}`, `/${this._id.substring(6, 12)}`, `/${this._id.substring(12, 14)}`, `/${this._id.substring(14, 16)}`, `/${this._id.substring(16, 18)}`, `/${this._id}`, '.cer'].join(''); } static isValidCertificateNumber(id) { return /^\d{20}$/.test(id); } } export { AbstractXsltBuilder, CerRetriever, CertificadoPropertyTrait, CfdiDefaultLocations, DomElementContainer, NodeCertificado, NodeContainer, SatCertificateNumber, SaxonbCliBuilder, TfdCadenaDeOrigen, TfdVersion, VersionDiscoverer, XmlResolver, XmlResolverPropertyTrait, XsltBuildException, XsltBuilderPropertyTrait }; //# sourceMappingURL=cfdiutils-core.modern.js.map