Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@bonniernews/local-esi

Package Overview
Dependencies
Maintainers
6
Versions
73
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@bonniernews/local-esi - npm Package Compare versions

Comparing version 1.0.3 to 1.1.0

lib/expression/evaluate.js

1

index.js

@@ -6,2 +6,3 @@ "use strict";

const {asStream, transform, createESIParser} = require("./lib/transformHtml");
const redirectCodes = [301, 302, 303, 307, 308];

@@ -8,0 +9,0 @@

380

lib/ESIEvaluator.js
"use strict";
const esiExpressionParser = require("./esiExpressionParser");
const evaluateExpression = require("./evaluateExpression");
const HtmlParser = require("atlas-html-stream");
const ListenerContext = require("./ListenerContext");
const request = require("request");
const url = require("url");
const {assign, test, replace} = require("./evaluateExpression");
const {convert, createESIParser} = require("./transformHtml");
const {opentag: toOpenTag, closetag: toCloseTag, voidElements, selfClosingElements} = require("./markup");
const {chunkToMarkup} = require("./markup");
const {Readable} = require("stream");

@@ -44,7 +43,9 @@ module.exports = function ESIEvaluator(context) {

}
const value = data.value;
if (value.startsWith("'''") && value.endsWith("'''")) {
context.assigns[data.name] = value.replace(/'''/ig, "");
} else {
context.assigns[data.name] = removeReservedCharacters(evaluateExpression(value, context));
try {
context.assigns[data.name] = assign(value, context);
} catch (err) {
if (/unknown keyword/i.test(err.message)) context.assigns[data.name] = value;
else return next(err);
}

@@ -58,7 +59,5 @@

open(data, next) {
context.inEsiStatementProcessingContext = true;
next();
},
close(next) {
context.inEsiStatementProcessingContext = false;
next();

@@ -76,3 +75,3 @@ }

}
const listener = ESIEvaluator(ListenerContext(context.req, context.res, context.emitter));
const listener = ESIEvaluator(context.clone());

@@ -100,5 +99,4 @@ const chunks = [];

}
const wasInProcessingContext = context.inEsiStatementProcessingContext;
context.inEsiStatementProcessingContext = false;
const listener = ESIEvaluator(context);
const listener = ESIEvaluator(context.clone(true));
const chunks = [];

@@ -110,3 +108,2 @@ convert(fetchResult)

.on("finish", () => {
context.inEsiStatementProcessingContext = wasInProcessingContext;
writeToResult(chunks, next);

@@ -145,3 +142,3 @@ })

const lastChoose = context.chooses[context.chooses.length - 1];
const result = evaluateExpression(data.test, context);
const result = test(data.test, context);
if (data.matchname) {

@@ -153,3 +150,2 @@ context.assigns[data.matchname] = result;

lastChoose.hasEvaluatedToTrue = lastChoose.hasEvaluatedToTrue || result;
context.inEsiStatementProcessingContext = true;

@@ -159,3 +155,2 @@ return next();

close(next) {
context.inEsiStatementProcessingContext = false;
next();

@@ -169,8 +164,5 @@ }

lastChoose.isCurrentlyEvaluatedTo = !lastChoose.hasEvaluatedToTrue;
context.inEsiStatementProcessingContext = true;
return next();
},
close(next) {
context.inEsiStatementProcessingContext = false;
next();

@@ -180,2 +172,55 @@ }

esiTags["esi:foreach"] = {
open(data, next) {
context.items = assign(data.collection, context);
if (!Array.isArray(context.items)) {
context.items = Object.entries(context.items);
}
context.foreachChunks = [];
return next();
},
close(next) {
const foreachChunks = context.foreachChunks;
delete context.foreachChunks;
let buffered = [];
context.items.forEach((value) => {
if (Array.isArray(value)) value = `[${value.map((v) => typeof v === "string" ? `'${v}'` : v).join(",")}]`;
buffered = buffered.concat([{
name: "esi:assign", data: {name: "item", value: value.toString()}
}, {name: "esi:assign"}], foreachChunks);
});
const localContext = context.subContext();
localContext.inForeach = true;
const listener = ESIEvaluator(localContext);
const chunks = [];
createChunkStream(buffered)
.pipe(createESIParser(listener))
.on("data", function onData(chunk) {
if (chunk.name === "esi:break") {
this.pause();
return process.nextTick(() => this.destroy());
}
chunks.push(chunk);
})
.on("finish", complete)
.on("error", next);
function complete() {
writeToResult(chunks, next);
}
}
};
esiTags["esi:break"] = {
open(data, next) {
if (!context.inForeach) return next(new Error("esi:break outside esi:foreach"));
return shouldWrite() ? next(null, {name: "esi:break"}) : next();
}
};
function fetchIncluded(data, fetchCallback) {

@@ -189,22 +234,7 @@ const options = {

let source = data.src;
if (source.indexOf("$(") > -1 && source.indexOf(")") > -1) {
source = handleProcessingInstructions(source);
let includeUrl = replace(data.src, context);
if (!includeUrl.startsWith("http")) {
includeUrl = url.resolve(`http://localhost:${context.req.socket.server.address().port}`, includeUrl);
}
for (const key in context.assigns) {
if (typeof context.assigns[key] === "string") {
source = source.replace(`$(${key})`, context.assigns[key]);
}
}
source = source.replace(/\$url_encode\((.*?)\)/, (_, b) => {
return encodeURIComponent(b);
});
let includeUrl = source;
if (!includeUrl.startsWith("http")) {
includeUrl = url.resolve(`http://localhost:${context.req.socket.server.address().port}`, source);
}
request.get(includeUrl, options, (err, res, body) => {

@@ -231,19 +261,5 @@ if (!err && res.statusCode > 399) {

function shouldWrite() {
if (context.inExcept && !context.lastAttemptWasError) {
return false;
}
if (context.chooses.length) {
return context.chooses.every((choose) => choose.isCurrentlyEvaluatedTo);
}
return true;
}
function onopentag(name, data, next) {
const [current = {}] = context.tags.slice(-1);
if (current.plainText && current.callExpression) {
current.text += toOpenTag(name, data);
if (context.foreachChunks) {
context.foreachChunks.push({name, data});
return next();

@@ -253,93 +269,18 @@ }

if (name.startsWith("esi:")) {
if (!current.plainText) {
const esiFunc = esiTags[name];
context.tags.push(esiFunc);
if (!esiFunc) {
throw new Error(`ESI tag ${name} not implemented.`);
}
const res = esiFunc.open(data, next);
return res;
const esiFunc = esiTags[name];
const wasInPlainText = context.isInPlainText();
if (!esiFunc && !wasInPlainText) {
throw new Error(`ESI tag ${name} not implemented.`);
}
if (selfClosingElements.includes(name)) {
return writeToResult({name, data: makeAttributes(data)}, next);
}
context.tags.push(esiFunc);
if (!wasInPlainText) return esiFunc.open(data, next);
}
if (name === "!--") {
return writeToResult({name, data: makeAttributes(data)}, next);
}
writeToResult({name, data: makeAttributes(data)}, next);
}
function ontext(text, next) {
const [current = {}] = context.tags.slice(-1);
if (!context.inEsiStatementProcessingContext) {
return writeToResult({text}, next);
}
if (/^\$\w+\(/.test(text)) {
const expression = esiExpressionParser(text);
if (expression.type !== "CallExpression" && !current.plainText) {
context.tags.push({
text: text,
plainText: true,
callExpression: true,
});
return next();
}
try {
return writeToResult(() => {
return {text: handleProcessingInstructions(ensureNoIllegalCharacters(text))};
}, next); //handleProcessingInstructions may cause an (expected) error and we're not sure writeToResult will actually write so we pass a function that it can call if it should write
} catch (err) {
return next(err);
}
}
if (current.callExpression) {
const testText = current.text + text;
const expression = esiExpressionParser(testText);
if (expression.type !== "CallExpression") {
current.text = testText;
return next();
}
context.tags.pop();
try {
return writeToResult(() => {
return {text: handleProcessingInstructions(ensureNoIllegalCharacters(testText))};
}, next); //handleProcessingInstructions may cause an (expected) error and we're not sure writeToResult will actually write so we pass a function that it can call if it should write
} catch (err) {
return next(err);
}
}
if (!current.plainText) {
try {
return writeToResult(() => {
return {text: handleProcessingInstructions(ensureNoIllegalCharacters(text))};
}, next); //handleProcessingInstructions may cause an (expected) error and we're not sure writeToResult will actually write so we pass a function that it can call if it should write
} catch (err) {
return next(err);
}
}
try {
return writeToResult({text}, next);
} catch (err) {
return next(err);
}
}
function onclosetag(name, next) {
const [current = {}] = context.tags.slice(-1);
if (current.plainText && current.callExpression) {
current.text += toCloseTag(name);
if (name !== "esi:foreach" && context.foreachChunks) {
context.foreachChunks.push({name});
return next();

@@ -349,106 +290,45 @@ }

if (name.startsWith("esi:")) {
if (!current.plainText) {
const esiFunc = esiTags[name];
if (!esiFunc) {
throw new Error(`ESI tag ${name} not implemented.`);
}
const popped = context.tags.pop();
if (esiFunc.close) {
return esiFunc.close(next);
}
if (!context.isInPlainText()) {
if (popped && popped.close) return popped.close(next);
return next();
} else if (current.plainText && esiTags[name] === current) {
context.tags.pop();
if (current.close) {
return current.close(next);
}
return next();
}
if (selfClosingElements.includes(name)) {
return next();
}
}
if (voidElements.includes(name)) return next();
writeToResult({name}, next);
}
function ensureNoIllegalCharacters(text) {
// matches
// - dollar signs not part of an esi expression
const pattern = /(\$(?!\w*?\())/g;
let match;
function ontext(text, next) {
if (context.foreachChunks) {
context.foreachChunks.push({text});
return next();
}
while ((match = pattern.exec(text))) {
const {"1": character, index} = match;
if (text.charAt(index - 1) === "\\") continue;
if (!context.isProcessing()) {
return writeToResult({text}, next);
}
const excerptStart = Math.max(index - 30, 0);
const excerpt = text.substr(excerptStart, 60);
throw new Error(`Illegal character "${character}" in "${excerpt}"`);
const current = context.tags[context.tags.length - 1];
if (context.bufferingString && current.text) {
text = current.text + text;
}
return text;
}
function handleProcessingInstructions(text) {
text = removeReservedCharacters(text);
let newText = "";
let inExpression = false;
let expressionStart;
let openParentheses = 0;
for (let i = 0; i < text.length; i++) {
const char = text[i];
if (char === "$") {
if (!inExpression) {
expressionStart = i;
}
inExpression = true;
try {
return writeToResult((currentContext) => {
const result = {text: replace(text, currentContext || context)};
context.bufferingString = false;
return result;
}, next); //handleProcessingInstructions may cause an (expected) error and we're not sure writeToResult will actually write so we pass a function that it can call if it should write
} catch (err) {
if (err.message.includes("Found end of file before end")) {
context.bufferingString = true;
current.text = text;
return next();
}
if (!inExpression) {
newText += char;
}
if (inExpression && char === "(") {
openParentheses++;
}
if (inExpression && char === ")") {
openParentheses--;
if (openParentheses === 0) {
inExpression = false;
const expressionResult = evaluateExpression(text.substring(expressionStart, i + 1), context);
if (expressionResult !== undefined) {
newText += expressionResult;
}
}
}
return next(err);
}
return newText;
}
function removeReservedCharacters(original) {
if (!original || typeof original !== "string") {
return original;
}
let text = original.replace(/\\["]/g, "\"");
text = text.replace(/(^|[^\\])(\\)($|[^\\])/ig, (_, group1, _2, group3) => { //Remove backslashes, but not escaped ones
return `${group1}${group3}`;
});
text = text.replace(/\\\\/g, "\\"); //Escaped backslashes, remove the escaping backslash
return text;
}
function makeAttributes(data) {

@@ -459,5 +339,4 @@ if (!data) return {};

let value = data[key];
const [current = {}] = context.tags.slice(-1);
if (context.inEsiStatementProcessingContext && !current.plainText) {
value = handleProcessingInstructions(value);
if (context.isProcessing()) {
value = replace(value, context);
}

@@ -470,15 +349,50 @@ attributes[key] = value;

function writeToResult(chunk, next) {
if (typeof chunk === "function" && (context.inReplacement || shouldWrite())) {
chunk = chunk();
}
if (context.bufferingString) {
const [current = {}] = context.tags.slice(-1);
if (typeof chunk === "function") {
chunk = chunk();
}
if (context.inReplacement) {
context.replacement += chunk;
current.text += chunkToMarkup(chunk);
return next();
}
if (shouldWrite()) {
if (typeof chunk === "function") {
chunk = chunk();
}
return next(null, chunk);
}
next();
}
function shouldWrite() {
if (context.inExcept && !context.lastAttemptWasError) {
return false;
}
if (context.chooses.length) {
return context.chooses.every((choose) => choose.isCurrentlyEvaluatedTo);
}
return true;
}
};
function createChunkStream(chunks) {
const reader = new Readable({objectMode: true});
reader._read = function () {
if (!chunks.length) return reader.push(null);
while (reader.push(chunks.shift())) {
if (!chunks.length) {
reader.push(null);
break;
}
}
};
return reader;
}

@@ -1,157 +0,59 @@

/* eslint-disable camelcase */
"use strict";
const esiExpressionParser = require("./esiExpressionParser");
module.exports = function evaluateExpression(test, context) {
const funcs = {
Identifier(node, nodeContext = context.assigns) {
return nodeContext[node.name];
},
exists([arg]) {
return !!getFunc(arg.type)(arg);
},
int([arg]) {
return parseInt(getFunc(arg.type)(arg)) || 0;
},
index([arg1, arg2]) {
return getFunc(arg1.type)(arg1).indexOf(getFunc(arg2.type)(arg2));
},
base64_decode([arg]) {
const string = getFunc(arg.type)(arg);
if (!string) {
return "";
}
return Buffer.from(string, "base64").toString("utf8");
},
base64_encode([arg]) {
const string = getFunc(arg.type)(arg);
if (!string) {
return "";
}
return Buffer.from(string, "utf8").toString("base64");
},
url_encode([arg]) {
const string = getFunc(arg.type)(arg);
if (!string) {
return "";
}
return encodeURIComponent(string);
},
add_header([name, value]) {
context.emit("add_header", getFunc(name.type)(name), getFunc(value.type)(value));
},
set_redirect([location]) {
context.emit("set_redirect", 302, getFunc(location.type)(location));
context.redirected = true;
},
set_response_code([code, body]) {
if (body) {
return context.emit("set_response_code", getFunc(code.type)(code), getFunc(body.type)(body));
}
const evaluate = require("./expression/evaluate");
const {parse, split} = require("./expression/parser");
context.emit("set_response_code", getFunc(code.type)(code));
},
substr([arg1, arg2, arg3]) {
const string = getFunc(arg1.type)(arg1);
if (typeof string !== "string") {
throw new Error("substr invoked on non-string");
}
let startIndex;
let length;
module.exports = {
assign,
test,
replace,
};
if (arg2) {
startIndex = getFunc(arg2.type)(arg2);
}
function assign(value, context) {
return evaluate(parse(value), context);
}
if (typeof startIndex !== "number") {
throw new Error("substr invoked with non-number as start index");
}
function test(expression, context) {
return evaluate(parse(expression), context);
}
if (arg3) {
length = getFunc(arg3.type)(arg3);
}
function replace(text, context) {
if (!text) return;
if (length < 0) {
length = string.length - startIndex + length;
}
return string.substr(startIndex, length);
},
time() {
return Math.round(Date.now() / 1000);
},
http_time([seconds]) {
const secondsInt = parseInt(getFunc(seconds.type)(seconds));
const now = new Date(secondsInt * 1000);
return now.toUTCString();
},
CallExpression(node) {
return getFunc(node.callee.name)(node.arguments);
},
LogicalExpression(node) {
const left = getFunc(node.left.type);
const right = getFunc(node.right.type);
const expressions = split(text);
if (!expressions.length) return removeReservedCharacters(text);
if (node.operator === "&" || node.operator === "&&") return left(node.left) && right(node.right);
if (node.operator === "|" || node.operator === "||") return left(node.left) || right(node.right);
let newText = "";
throw new Error(`Uknown BinaryExpression operator ${node.operator}`);
},
BinaryExpression(node) {
const left = getFunc(node.left.type)(node.left);
const right = getFunc(node.right.type)(node.right);
for (const expr of expressions) {
if (expr.type === "TEXT") {
newText += removeReservedCharacters(expr.text);
continue;
}
if (node.operator === "==") return left === right;
if (node.operator === "!=") return left !== right;
if (node.operator === ">=") return left >= right;
if (node.operator === "<=") return left <= right;
if (node.operator === "<") return left < right;
if (node.operator === ">") return left > right;
if (node.operator === "+") return left + right;
if (node.operator === "-") return left - right;
if (node.operator === "*") return left * right;
if (node.operator === "/") return left / right;
if (node.operator === "%") return left % right;
if (node.operator === "has") return left.indexOf(right) > -1;
if (node.operator === "has_i") return left.toLowerCase().indexOf(right.toLowerCase()) > -1;
if (node.operator === "matches") {
if (!left) {
return;
}
return left.match(right);
}
if (node.operator === "matches_i") {
if (!left) {
return;
}
return left.match(new RegExp(right, "i"));
}
let result = evaluate(expr.expression, context);
if (Array.isArray(result)) result = `[${result.join(", ")}]`;
throw new Error(`Uknown BinaryExpression operator ${node.operator}`);
},
MemberExpression(node) {
const object = getFunc(node.object.type)(node.object);
if (result === undefined) continue;
if (!object) return;
newText += result;
}
return getFunc(node.property.type)(node.property, object);
},
Literal(node) {
return node.value;
},
UnaryExpression(node) {
if (node.operator !== "!") {
throw new Error(`Unary operator ${node.operator} not implemented`);
}
return newText;
}
return !getFunc(node.argument.type)(node.argument);
}
};
function removeReservedCharacters(original) {
if (!original || typeof original !== "string") {
return original;
}
const parsedTree = esiExpressionParser(test);
return getFunc(parsedTree.type)(parsedTree);
let text = original.replace(/\\["]/g, "\"");
function getFunc(name) {
if (!funcs[name]) throw new Error(`${name} is not implemented`);
return funcs[name];
}
};
text = text.replace(/(^|[^\\])(\\)($|[^\\])/ig, (_, group1, _2, group3) => { //Remove backslashes, but not escaped ones
return `${group1}${group3}`;
});
text = text.replace(/\\\\/g, "\\"); //Escaped backslashes, remove the escaping backslash
return text;
}

@@ -6,14 +6,5 @@ "use strict";

module.exports = function ListenerContext(req, res, emitter) {
const buildHeaderVariables = (headers) => {
if (!headers) return {};
return Object.entries(headers).reduce((acc, pair) => {
const httpKey = pair[0].replace(/-/g, "_").toUpperCase();
acc[`HTTP_${httpKey}`] = pair[1];
return acc;
}, {});
};
emitter = emitter || new EventEmitter();
const context = {
return {
assigns: Object.assign(buildHeaderVariables(req && req.headers), {

@@ -26,3 +17,8 @@ "HTTP_COOKIE": req.cookies || {},

res,
inEsiStatementProcessingContext: false,
isProcessing() {
return Boolean((this.tags.length || this.isSubContext) && !this.isInPlainText());
},
isInPlainText() {
return this.tags.some((tag) => tag.plainText);
},
inAttempt: false,

@@ -53,5 +49,29 @@ lastAttemptWasError: false,

},
clone(linkAssigns) {
const c = ListenerContext(req, res, emitter);
if (linkAssigns) {
c.assigns = this.assigns;
}
return c;
},
subContext() {
const clone = this.clone(true);
clone.isSubContext = true;
return clone;
}
};
return context;
function buildHeaderVariables(headers) {
if (!headers) return {};
return Object.entries(headers).reduce((acc, pair) => {
const header = pair[0];
if (header === "x-forwarded-for") {
acc.REMOTE_ADDR = pair[1];
}
const httpKey = header.replace(/-/g, "_").toUpperCase();
acc[`HTTP_${httpKey}`] = pair[1];
return acc;
}, {});
}
};
"use strict";
const voidElements = ["area", "base", "br", "col", "embed", "hr", "img", "input", "link", "meta", "param", "source", "track", "wbr"];
const selfClosingElements = ["esi:include", "esi:eval", "esi:assign", "esi:debug"];
const selfClosingElements = ["esi:include", "esi:eval", "esi:assign", "esi:debug", "esi:break"];

@@ -6,0 +6,0 @@ module.exports = {

{
"name": "@bonniernews/local-esi",
"version": "1.0.3",
"version": "1.1.0",
"description": "Local Edge Side Includes parser",
"main": "index.js",
"scripts": {
"test": "mocha && eslint . --cache"
"test": "mocha",
"posttest": "eslint . --cache"
},

@@ -9,0 +10,0 @@ "keywords": [

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc