@nodecfdi/cfdiutils-core
Version:
Core of CfdiUtils
648 lines (497 loc) • 15.8 kB
JavaScript
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