Move from xml2js to xmlbuilder2
This commit is contained in:
parent
e8a99001ed
commit
e212793337
|
@ -685,6 +685,74 @@
|
|||
"chalk": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"@oozcitak/dom": {
|
||||
"version": "1.15.5",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/dom/-/dom-1.15.5.tgz",
|
||||
"integrity": "sha512-L6v3Mwb0TaYBYgeYlIeBaHnc+2ZEaDSbFiRm5KmqZQSoBlbPlf+l6aIH/sD5GUf2MYwULw00LT7+dOnEuAEC0A==",
|
||||
"requires": {
|
||||
"@oozcitak/infra": "1.0.5",
|
||||
"@oozcitak/url": "1.0.0",
|
||||
"@oozcitak/util": "8.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oozcitak/util": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.0.0.tgz",
|
||||
"integrity": "sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@oozcitak/infra": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.5.tgz",
|
||||
"integrity": "sha512-o+zZH7M6l5e3FaAWy3ojaPIVN5eusaYPrKm6MZQt0DKNdgXa2wDYExjpP0t/zx+GoQgQKzLu7cfD8rHCLt8JrQ==",
|
||||
"requires": {
|
||||
"@oozcitak/util": "8.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oozcitak/util": {
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.0.0.tgz",
|
||||
"integrity": "sha512-+9Hq6yuoq/3TRV/n/xcpydGBq2qN2/DEDMqNTG7rm95K6ZE2/YY/sPyx62+1n8QsE9O26e5M1URlXsk+AnN9Jw=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@oozcitak/url": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/url/-/url-1.0.0.tgz",
|
||||
"integrity": "sha512-LGrMeSxeLzsdaitxq3ZmBRVOrlRRQIgNNci6L0VRnOKlJFuRIkNm4B+BObXPCJA6JT5bEJtrrwjn30jueHJYZQ==",
|
||||
"requires": {
|
||||
"@oozcitak/infra": "1.0.3",
|
||||
"@oozcitak/util": "1.0.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oozcitak/infra": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/infra/-/infra-1.0.3.tgz",
|
||||
"integrity": "sha512-9O2wxXGnRzy76O1XUxESxDGsXT5kzETJPvYbreO4mv6bqe1+YSuux2cZTagjJ/T4UfEwFJz5ixanOqB0QgYAag==",
|
||||
"requires": {
|
||||
"@oozcitak/util": "1.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@oozcitak/util": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-1.0.1.tgz",
|
||||
"integrity": "sha512-dFwFqcKrQnJ2SapOmRD1nQWEZUtbtIy9Y6TyJquzsalWNJsKIPxmTI0KG6Ypyl8j7v89L2wixH9fQDNrF78hKg=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@oozcitak/util": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-1.0.2.tgz",
|
||||
"integrity": "sha512-4n8B1cWlJleSOSba5gxsMcN4tO8KkkcvXhNWW+ADqvq9Xj+Lrl9uCa90GRpjekqQJyt84aUX015DG81LFpZYXA=="
|
||||
}
|
||||
}
|
||||
},
|
||||
"@oozcitak/util": {
|
||||
"version": "8.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@oozcitak/util/-/util-8.3.3.tgz",
|
||||
"integrity": "sha512-Ufpab7G5PfnEhQyy5kDg9C8ltWJjsVT1P/IYqacjstaqydG4Q21HAT2HUZQYBrC/a1ZLKCz87pfydlDvv8y97w=="
|
||||
},
|
||||
"@opencensus/core": {
|
||||
"version": "0.0.9",
|
||||
"resolved": "https://registry.npmjs.org/@opencensus/core/-/core-0.0.9.tgz",
|
||||
|
@ -1170,15 +1238,6 @@
|
|||
"integrity": "sha512-l42BggppR6zLmpfU6fq9HEa2oGPEI8yrSPL3GITjfRInppYFahObbIQOQK3UGxEnyQpltZLaPe75046NOZQikw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/xml2js": {
|
||||
"version": "0.4.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.5.tgz",
|
||||
"integrity": "sha512-yohU3zMn0fkhlape1nxXG2bLEGZRc1FeqF80RoHaYXJN7uibaauXfhzhOJr1Xh36sn+/tx21QAOf07b/xYVk1w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/yargs": {
|
||||
"version": "15.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.4.tgz",
|
||||
|
@ -8199,20 +8258,16 @@
|
|||
"integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==",
|
||||
"dev": true
|
||||
},
|
||||
"xml2js": {
|
||||
"version": "0.4.23",
|
||||
"resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz",
|
||||
"integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==",
|
||||
"xmlbuilder2": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder2/-/xmlbuilder2-2.1.0.tgz",
|
||||
"integrity": "sha512-hWCfRTKFhhLv1QlSRn+PMwu0knJUQ5PSQanHFoY9Zy1q8cMEm0/37PHbeiBwlplgNafDzDJRdwQkMYVdXt77Nw==",
|
||||
"requires": {
|
||||
"sax": ">=0.6.0",
|
||||
"xmlbuilder": "~11.0.0"
|
||||
"@oozcitak/dom": "1.15.5",
|
||||
"@oozcitak/infra": "1.0.5",
|
||||
"@oozcitak/util": "8.3.3"
|
||||
}
|
||||
},
|
||||
"xmlbuilder": {
|
||||
"version": "11.0.1",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz",
|
||||
"integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="
|
||||
},
|
||||
"xmlchars": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz",
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
"react": "^16.13.0",
|
||||
"react-dom": "^16.13.0",
|
||||
"smtp-server": "^3.6.0",
|
||||
"xml2js": "^0.4.23"
|
||||
"xmlbuilder2": "^2.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.3",
|
||||
|
@ -24,7 +24,6 @@
|
|||
"@types/react": "^16.9.29",
|
||||
"@types/react-dom": "^16.9.5",
|
||||
"@types/smtp-server": "^3.5.4",
|
||||
"@types/xml2js": "^0.4.5",
|
||||
"axios": "^0.19.2",
|
||||
"concurrently": "^5.1.0",
|
||||
"jest": "^25.2.4",
|
||||
|
|
16
src/check.ts
16
src/check.ts
|
@ -1,21 +1,19 @@
|
|||
import xml2js from "xml2js";
|
||||
import * as xmlbuilder2 from "xmlbuilder2";
|
||||
import fs from "fs";
|
||||
|
||||
(async () => {
|
||||
for (const feed of fs
|
||||
for (const feed of fs
|
||||
.readdirSync("static/feeds")
|
||||
.filter(file => !file.startsWith("."))) {
|
||||
try {
|
||||
const xml = await new xml2js.Parser().parseStringPromise(
|
||||
fs.readFileSync(`static/feeds/${feed}`, "utf8")
|
||||
const xml: any = xmlbuilder2.convert(
|
||||
fs.readFileSync(`static/feeds/${feed}`, "utf8"),
|
||||
{ format: "object", wellFormed: true }
|
||||
);
|
||||
if (xml?.feed?.updated === undefined)
|
||||
throw new Error("Can’t find xml.feed.updated");
|
||||
new xml2js.Builder().buildObject(xml);
|
||||
console.log(`OK ${feed}`);
|
||||
} catch (error) {
|
||||
console.log(`ERROR ${feed}: ${error}`);
|
||||
}
|
||||
}
|
||||
console.log("FINISHED");
|
||||
})();
|
||||
}
|
||||
console.log("FINISHED");
|
||||
|
|
|
@ -3,7 +3,7 @@ import { SMTPServer } from "smtp-server";
|
|||
import mailparser from "mailparser";
|
||||
import React from "react";
|
||||
import ReactDOMServer from "react-dom/server";
|
||||
import xml2js from "xml2js";
|
||||
import * as xmlbuilder2 from "xmlbuilder2";
|
||||
import { promises as fs } from "fs";
|
||||
import cryptoRandomString from "crypto-random-string";
|
||||
|
||||
|
@ -82,9 +82,10 @@ export const emailServer = new SMTPServer({
|
|||
const path = feedPath(identifier);
|
||||
const xmlText = await fs.readFile(path, "utf8").catch(() => null);
|
||||
if (xmlText === null) continue;
|
||||
const xml = await new xml2js.Parser().parseStringPromise(xmlText);
|
||||
const xml = parseXML(xmlText);
|
||||
xml.feed.updated = now();
|
||||
if (xml.feed.entry === undefined) xml.feed.entry = [];
|
||||
if (!Array.isArray(xml.feed.entry)) xml.feed.entry = [xml.feed.entry];
|
||||
xml.feed.entry.unshift(entry);
|
||||
while (xml.feed.entry.length > 0 && renderXML(xml).length > 500_000)
|
||||
xml.feed.entry.pop();
|
||||
|
@ -94,7 +95,7 @@ export const emailServer = new SMTPServer({
|
|||
})().catch(error => {
|
||||
console.error(error);
|
||||
stream.resume();
|
||||
callback(error);
|
||||
callback(new Error("Failed to receive message. Please try again."));
|
||||
});
|
||||
}
|
||||
}).listen(process.env.EMAIL_PORT ?? 2525);
|
||||
|
@ -207,21 +208,17 @@ function Created({ identifier }: { identifier: string }) {
|
|||
function Feed({ name, identifier }: { name: string; identifier: string }) {
|
||||
return {
|
||||
feed: {
|
||||
$: { xmlns: "http://www.w3.org/2005/Atom" },
|
||||
"@xmlns": "http://www.w3.org/2005/Atom",
|
||||
link: [
|
||||
{
|
||||
$: {
|
||||
rel: "self",
|
||||
type: "application/atom+xml",
|
||||
href: feedURL(identifier)
|
||||
}
|
||||
"@rel": "self",
|
||||
"@type": "application/atom+xml",
|
||||
"@href": feedURL(identifier)
|
||||
},
|
||||
{
|
||||
$: {
|
||||
rel: "alternate",
|
||||
type: "text/html",
|
||||
href: "https://www.kill-the-newsletter.com/"
|
||||
}
|
||||
"@rel": "alternate",
|
||||
"@type": "text/html",
|
||||
"@href": "https://www.kill-the-newsletter.com/"
|
||||
}
|
||||
],
|
||||
id: urn(identifier),
|
||||
|
@ -258,13 +255,11 @@ function Entry({
|
|||
author: { name: author },
|
||||
updated: now(),
|
||||
link: {
|
||||
$: {
|
||||
rel: "alternate",
|
||||
type: "text/html",
|
||||
href: "https://www.kill-the-newsletter.com/entry"
|
||||
}
|
||||
"@rel": "alternate",
|
||||
"@type": "text/html",
|
||||
"@href": "https://www.kill-the-newsletter.com/entry"
|
||||
},
|
||||
content: { $: { type: "html" }, _: content }
|
||||
content: { "@type": "html", "#": content }
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -301,5 +296,18 @@ function renderHTML(component: React.ReactElement): string {
|
|||
}
|
||||
|
||||
function renderXML(xml: object): string {
|
||||
return new xml2js.Builder().buildObject(xml);
|
||||
return xmlbuilder2.convert({ invalidCharReplacement: "" }, xml, {
|
||||
format: "xml",
|
||||
wellFormed: true,
|
||||
noDoubleEncoding: true,
|
||||
prettyPrint: true
|
||||
});
|
||||
}
|
||||
|
||||
function parseXML(xml: string): any {
|
||||
return xmlbuilder2.convert({ invalidCharReplacement: "" }, xml, {
|
||||
format: "object",
|
||||
wellFormed: true,
|
||||
noDoubleEncoding: true
|
||||
});
|
||||
}
|
||||
|
|
41
src/test.ts
41
src/test.ts
|
@ -11,6 +11,21 @@ test("create feed", async () => {
|
|||
});
|
||||
|
||||
describe("receive email", () => {
|
||||
test("‘updated’ field is updated", async () => {
|
||||
const identifier = await createFeed();
|
||||
const before = await getFeed(identifier);
|
||||
await emailClient.sendMail({
|
||||
from: "publisher@example.com",
|
||||
to: `${identifier}@kill-the-newsletter.com`,
|
||||
subject: "New Message",
|
||||
html: "<p>HTML content</p>"
|
||||
});
|
||||
const after = await getFeed(identifier);
|
||||
expect(after.match(/<updated>(.*)<\/updated>/)![1]).not.toMatch(
|
||||
before.match(/<updated>(.*)<\/updated>/)![1]
|
||||
);
|
||||
});
|
||||
|
||||
test("HTML content", async () => {
|
||||
const identifier = await createFeed();
|
||||
await emailClient.sendMail({
|
||||
|
@ -50,6 +65,32 @@ describe("receive email", () => {
|
|||
expect(feed).toMatch(`href="https://www.kill-the-newsletter.com"`);
|
||||
});
|
||||
|
||||
test("invalid XML character in HTML", async () => {
|
||||
const identifier = await createFeed();
|
||||
await emailClient.sendMail({
|
||||
from: "publisher@example.com",
|
||||
to: `${identifier}@kill-the-newsletter.com`,
|
||||
subject: "New Message",
|
||||
html: "<p>Invalid XML character (backspace): ‘\b’</p>"
|
||||
});
|
||||
const feed = await getFeed(identifier);
|
||||
expect(feed).toMatch("Invalid XML character (backspace): ‘’");
|
||||
});
|
||||
|
||||
test("invalid XML character in text", async () => {
|
||||
const identifier = await createFeed();
|
||||
await emailClient.sendMail({
|
||||
from: "publisher@example.com",
|
||||
to: `${identifier}@kill-the-newsletter.com`,
|
||||
subject: "New Message",
|
||||
text: "Invalid XML character (backspace): ‘\b’"
|
||||
});
|
||||
const feed = await getFeed(identifier);
|
||||
expect(feed).toMatch(
|
||||
"Invalid XML character (backspace): ‘’"
|
||||
);
|
||||
});
|
||||
|
||||
test("missing content", async () => {
|
||||
const identifier = await createFeed();
|
||||
await emailClient.sendMail({
|
||||
|
|
Loading…
Reference in New Issue