tina4-nodejs 3.13.136 → 3.13.138
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +33 -3
- package/COMMERCIAL-LICENSE.md +8 -0
- package/LICENSE +373 -0
- package/NOTICE +11 -0
- package/README.md +24 -2
- package/package.json +44 -6
- package/packages/cli/dist/bin.js +19440 -18017
- package/packages/cli/src/bin.ts +8 -0
- package/packages/cli/src/commands/build.ts +8 -0
- package/packages/cli/src/commands/generate.ts +8 -0
- package/packages/cli/src/commands/init.ts +8 -0
- package/packages/cli/src/commands/lint.ts +8 -0
- package/packages/cli/src/commands/migrate.ts +8 -0
- package/packages/cli/src/commands/migrateCreate.ts +8 -0
- package/packages/cli/src/commands/migrateRollback.ts +8 -0
- package/packages/cli/src/commands/migrateStatus.ts +8 -0
- package/packages/cli/src/commands/queue.ts +8 -0
- package/packages/cli/src/commands/routes.ts +8 -0
- package/packages/cli/src/commands/seed.ts +8 -0
- package/packages/cli/src/commands/serve.ts +11 -0
- package/packages/cli/src/commands/test.ts +8 -0
- package/packages/core/dist/index.js +19432 -18012
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/login/post.ts +8 -0
- package/packages/core/gallery/auth/src/routes/api/gallery/auth/verify/get.ts +8 -0
- package/packages/core/gallery/auth/src/routes/gallery/auth/get.ts +8 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/get.ts +8 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/notes/post.ts +8 -0
- package/packages/core/gallery/database/src/routes/api/gallery/db/tables/get.ts +8 -0
- package/packages/core/gallery/error-overlay/src/routes/api/gallery/crash/get.ts +8 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/get.ts +8 -0
- package/packages/core/gallery/orm/src/routes/api/gallery/products/post.ts +8 -0
- package/packages/core/gallery/queue/src/lib/queueDb.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/consume/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/fail/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/produce/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/retry/post.ts +8 -0
- package/packages/core/gallery/queue/src/routes/api/gallery/queue/status/get.ts +8 -0
- package/packages/core/gallery/queue/src/routes/gallery/queue/get.ts +8 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/get.ts +8 -0
- package/packages/core/gallery/rest-api/src/routes/api/gallery/hello/post.ts +8 -0
- package/packages/core/gallery/templates/src/routes/gallery/page/get.ts +8 -0
- package/packages/core/gallery/templates/src/templates/gallery_page.twig +8 -0
- package/packages/core/public/THIRD-PARTY-NOTICES.md +55 -0
- package/packages/core/public/bundled-component-provenance.json +48 -0
- package/packages/core/public/js/tina4js.min.js +13 -3
- package/packages/core/public/licenses/swagger-ui-3.10.0-original-notice.txt +11 -0
- package/packages/core/public/licenses/swagger-ui-5.17.14-NOTICE.txt +2 -0
- package/packages/core/public/licenses/swagger-ui-Apache-2.0.txt +202 -0
- package/packages/core/public/licenses/tina4-css-MIT.txt +21 -0
- package/packages/core/public/licenses/tina4-dev-admin-components.json +240 -0
- package/packages/core/public/licenses/tina4-dev-admin-third-party.txt +919 -0
- package/packages/core/public/licenses/tina4-js-MIT.txt +20 -0
- package/packages/core/src/__feedback/widget.js +8 -0
- package/packages/core/src/_missing.ts +8 -0
- package/packages/core/src/ai.ts +8 -0
- package/packages/core/src/aiClient.ts +22 -7
- package/packages/core/src/api.ts +15 -3
- package/packages/core/src/auth.ts +21 -8
- package/packages/core/src/authGate.ts +8 -0
- package/packages/core/src/background.ts +8 -0
- package/packages/core/src/cache.ts +43 -4
- package/packages/core/src/constants.ts +8 -0
- package/packages/core/src/container.ts +8 -0
- package/packages/core/src/context/chunker.ts +8 -0
- package/packages/core/src/context/index.ts +8 -0
- package/packages/core/src/devAdmin.ts +185 -59
- package/packages/core/src/devMailbox.ts +8 -0
- package/packages/core/src/dispatchPipeline.ts +8 -0
- package/packages/core/src/docs.ts +8 -0
- package/packages/core/src/docsAutoDiscovery.ts +8 -0
- package/packages/core/src/docsParser.ts +8 -0
- package/packages/core/src/docsScanner.ts +44 -0
- package/packages/core/src/docsSignatures.ts +8 -0
- package/packages/core/src/dotenv.ts +8 -0
- package/packages/core/src/env.ts +8 -0
- package/packages/core/src/errorOverlay.ts +8 -0
- package/packages/core/src/events.ts +8 -0
- package/packages/core/src/fakeData.ts +8 -0
- package/packages/core/src/feedback.ts +8 -0
- package/packages/core/src/graphql.ts +16 -5
- package/packages/core/src/health.ts +8 -0
- package/packages/core/src/htmlElement.ts +8 -0
- package/packages/core/src/i18n.ts +8 -0
- package/packages/core/src/index.ts +8 -0
- package/packages/core/src/job.ts +8 -0
- package/packages/core/src/logger.ts +8 -0
- package/packages/core/src/mcp.test.ts +8 -0
- package/packages/core/src/mcp.ts +8 -0
- package/packages/core/src/messenger.ts +143 -86
- package/packages/core/src/metrics.ts +13 -0
- package/packages/core/src/middleware.ts +1178 -1096
- package/packages/core/src/mqtt.ts +8 -0
- package/packages/core/src/mqttMessage.ts +8 -0
- package/packages/core/src/optionalPackage.ts +48 -0
- package/packages/core/src/plan.ts +8 -0
- package/packages/core/src/portTakeover.ts +8 -0
- package/packages/core/src/projectIndex.ts +8 -0
- package/packages/core/src/projectIndexExtractors.ts +8 -0
- package/packages/core/src/projectIndexStorage.ts +8 -0
- package/packages/core/src/push.ts +8 -0
- package/packages/core/src/queue.ts +8 -0
- package/packages/core/src/queueBackends/kafkaBackend.ts +8 -0
- package/packages/core/src/queueBackends/liteBackend.ts +8 -0
- package/packages/core/src/queueBackends/mongoBackend.ts +21 -8
- package/packages/core/src/queueBackends/rabbitmqBackend.ts +8 -0
- package/packages/core/src/rateLimiter.ts +8 -0
- package/packages/core/src/request.ts +59 -10
- package/packages/core/src/response.ts +79 -5
- package/packages/core/src/routeDiscovery.ts +8 -0
- package/packages/core/src/router.ts +8 -0
- package/packages/core/src/server.ts +73 -24
- package/packages/core/src/service.ts +8 -0
- package/packages/core/src/session.ts +8 -0
- package/packages/core/src/sessionHandlers/childError.ts +8 -0
- package/packages/core/src/sessionHandlers/databaseHandler.ts +8 -0
- package/packages/core/src/sessionHandlers/memcachedHandler.ts +8 -0
- package/packages/core/src/sessionHandlers/mongoClient.ts +8 -0
- package/packages/core/src/sessionHandlers/mongoHandler.ts +8 -0
- package/packages/core/src/sessionHandlers/respClient.ts +8 -0
- package/packages/core/src/sessionHandlers/sqlClient.ts +8 -0
- package/packages/core/src/sessionHandlers/syncBridge.ts +8 -0
- package/packages/core/src/sessionHandlers/syncSocket.ts +8 -0
- package/packages/core/src/sessionHandlers/valkeyHandler.ts +8 -0
- package/packages/core/src/sso.ts +8 -0
- package/packages/core/src/static.ts +8 -0
- package/packages/core/src/test.ts +8 -0
- package/packages/core/src/testClient.ts +8 -0
- package/packages/core/src/testing.ts +8 -0
- package/packages/core/src/transport.ts +369 -0
- package/packages/core/src/trustedProxy.ts +8 -0
- package/packages/core/src/types.ts +13 -0
- package/packages/core/src/validator.ts +8 -0
- package/packages/core/src/version.ts +8 -0
- package/packages/core/src/websocket.ts +8 -0
- package/packages/core/src/websocketBackplane.ts +49 -32
- package/packages/core/src/websocketConnection.ts +8 -0
- package/packages/core/src/wsdl.ts +197 -115
- package/packages/core/templates/errors/302.twig +8 -0
- package/packages/core/templates/errors/401.twig +8 -0
- package/packages/core/templates/errors/403.twig +8 -0
- package/packages/core/templates/errors/404.twig +8 -0
- package/packages/core/templates/errors/500.twig +8 -0
- package/packages/core/templates/errors/502.twig +8 -0
- package/packages/core/templates/errors/503.twig +8 -0
- package/packages/core/templates/errors/base.twig +8 -0
- package/packages/frond/dist/index.js +151 -17
- package/packages/frond/src/engine.ts +177 -17
- package/packages/frond/src/index.ts +8 -0
- package/packages/orm/dist/index.js +8135 -6713
- package/packages/orm/src/adapters/firebird.ts +50 -3
- package/packages/orm/src/adapters/mongodb.ts +8 -0
- package/packages/orm/src/adapters/mssql.ts +29 -7
- package/packages/orm/src/adapters/mysql.ts +17 -1
- package/packages/orm/src/adapters/odbc.ts +11 -0
- package/packages/orm/src/adapters/pg-types.d.ts +8 -0
- package/packages/orm/src/adapters/postgres.ts +34 -8
- package/packages/orm/src/adapters/sqlDialect.ts +57 -0
- package/packages/orm/src/adapters/sqlite.ts +26 -0
- package/packages/orm/src/autoCrud.ts +78 -36
- package/packages/orm/src/baseModel.ts +61 -29
- package/packages/orm/src/cachedDatabase.ts +67 -1
- package/packages/orm/src/connectTimeout.ts +8 -0
- package/packages/orm/src/database.ts +649 -401
- package/packages/orm/src/databaseResult.ts +8 -0
- package/packages/orm/src/databaseUrl.ts +8 -0
- package/packages/orm/src/docstore.ts +23 -1
- package/packages/orm/src/fakeData.ts +8 -0
- package/packages/orm/src/graph/adapters/arango.ts +8 -0
- package/packages/orm/src/graph/adapters/bolt.ts +8 -0
- package/packages/orm/src/graph/adapters/ultipa.ts +8 -0
- package/packages/orm/src/graph/connectTimeout.ts +8 -0
- package/packages/orm/src/graph/errors.ts +8 -0
- package/packages/orm/src/graph/graphAdapter.ts +8 -0
- package/packages/orm/src/graph/graphDatabase.ts +8 -0
- package/packages/orm/src/graph/graphUrl.ts +10 -1
- package/packages/orm/src/graph/shapes.ts +8 -0
- package/packages/orm/src/index.ts +147 -139
- package/packages/orm/src/migration.ts +21 -0
- package/packages/orm/src/model.ts +8 -0
- package/packages/orm/src/modelCollection.ts +8 -0
- package/packages/orm/src/point.ts +8 -0
- package/packages/orm/src/query.ts +149 -33
- package/packages/orm/src/queryBuilder.ts +8 -0
- package/packages/orm/src/realtime/index.ts +8 -0
- package/packages/orm/src/realtime/models/attachment.ts +8 -0
- package/packages/orm/src/realtime/models/channel.ts +8 -0
- package/packages/orm/src/realtime/models/channelMember.ts +8 -0
- package/packages/orm/src/realtime/models/message.ts +8 -0
- package/packages/orm/src/realtime/models/workspace.ts +8 -0
- package/packages/orm/src/realtime/realtime.ts +8 -0
- package/packages/orm/src/realtime/storage.ts +43 -12
- package/packages/orm/src/seeder.ts +8 -0
- package/packages/orm/src/seederTable.ts +8 -0
- package/packages/orm/src/seederTypes.ts +8 -0
- package/packages/orm/src/sqlTranslator.ts +185 -8
- package/packages/orm/src/types.ts +15 -0
- package/packages/orm/src/validation.ts +8 -0
- package/packages/swagger/src/generator.ts +8 -0
- package/packages/swagger/src/index.ts +8 -0
- package/packages/swagger/src/ui.ts +8 -0
- package/types/core/src/aiClient.d.ts +2 -2
- package/types/core/src/background.d.ts +0 -16
- package/types/core/src/cache.d.ts +6 -0
- package/types/core/src/devAdmin.d.ts +18 -12
- package/types/core/src/docs.d.ts +0 -17
- package/types/core/src/errorOverlay.d.ts +0 -24
- package/types/core/src/events.d.ts +0 -12
- package/types/core/src/feedback.d.ts +0 -23
- package/types/core/src/graphql.d.ts +0 -21
- package/types/core/src/htmlElement.d.ts +0 -15
- package/types/core/src/messenger.d.ts +12 -1
- package/types/core/src/metrics.d.ts +2 -0
- package/types/core/src/middleware.d.ts +45 -1
- package/types/core/src/mqtt.d.ts +0 -33
- package/types/core/src/optionalPackage.d.ts +25 -0
- package/types/core/src/plan.d.ts +0 -10
- package/types/core/src/projectIndex.d.ts +0 -10
- package/types/core/src/queueBackends/mongoBackend.d.ts +2 -0
- package/types/core/src/request.d.ts +12 -0
- package/types/core/src/response.d.ts +13 -0
- package/types/core/src/server.d.ts +19 -0
- package/types/core/src/sessionHandlers/childError.d.ts +0 -15
- package/types/core/src/transport.d.ts +28 -0
- package/types/core/src/trustedProxy.d.ts +0 -16
- package/types/core/src/types.d.ts +5 -0
- package/types/core/src/websocketBackplane.d.ts +1 -0
- package/types/core/src/wsdl.d.ts +8 -22
- package/types/frond/src/engine.d.ts +7 -1
- package/types/orm/src/adapters/firebird.d.ts +12 -0
- package/types/orm/src/adapters/mssql.d.ts +11 -0
- package/types/orm/src/adapters/mysql.d.ts +2 -0
- package/types/orm/src/adapters/postgres.d.ts +11 -0
- package/types/orm/src/adapters/sqlDialect.d.ts +25 -0
- package/types/orm/src/adapters/sqlite.d.ts +7 -0
- package/types/orm/src/baseModel.d.ts +5 -0
- package/types/orm/src/cachedDatabase.d.ts +27 -0
- package/types/orm/src/database.d.ts +46 -7
- package/types/orm/src/graph/graphUrl.d.ts +0 -8
- package/types/orm/src/index.d.ts +1 -1
- package/types/orm/src/query.d.ts +48 -2
- package/types/orm/src/realtime/storage.d.ts +2 -1
- package/types/orm/src/sqlTranslator.d.ts +70 -18
- package/types/orm/src/types.d.ts +6 -0
|
@@ -6,6 +6,7 @@ var SafeString = class {
|
|
|
6
6
|
constructor(value) {
|
|
7
7
|
this.value = value;
|
|
8
8
|
}
|
|
9
|
+
value;
|
|
9
10
|
toString() {
|
|
10
11
|
return this.value;
|
|
11
12
|
}
|
|
@@ -365,6 +366,9 @@ function resolveMethodPart(value, part, context) {
|
|
|
365
366
|
if (!methodMatch || typeof value !== "object" || value === null) return { matched: false, value };
|
|
366
367
|
const methodName = methodMatch[1];
|
|
367
368
|
const rawArgs = methodMatch[2] || "";
|
|
369
|
+
if (unsafeKey(methodName) || !Object.prototype.hasOwnProperty.call(value, methodName)) {
|
|
370
|
+
return { matched: false, value };
|
|
371
|
+
}
|
|
368
372
|
const fn = value[methodName];
|
|
369
373
|
if (typeof fn !== "function") return { matched: false, value };
|
|
370
374
|
const args = rawArgs.trim() ? splitArgs(rawArgs).map((a) => evalExpr(a.trim(), context)) : [];
|
|
@@ -384,7 +388,7 @@ function resolvePathKey(part, isBracket, context) {
|
|
|
384
388
|
function resolvePathPart(value, key) {
|
|
385
389
|
if (typeof value !== "object" || value === null) return { found: false, value: null };
|
|
386
390
|
if (Array.isArray(value) && typeof key === "number") return { found: true, value: value[key] };
|
|
387
|
-
if (
|
|
391
|
+
if (unsafeKey(key) || !Object.prototype.hasOwnProperty.call(value, key)) return { found: false, value: null };
|
|
388
392
|
const member = value[key];
|
|
389
393
|
return { found: true, value: typeof member === "function" ? member.call(value) : member };
|
|
390
394
|
}
|
|
@@ -504,7 +508,7 @@ function splitOutsideQuotes(expr, sep2) {
|
|
|
504
508
|
parts.push(expr.slice(currentStart));
|
|
505
509
|
return parts;
|
|
506
510
|
}
|
|
507
|
-
var EXPR_NOT_MATCHED = Symbol("frond-expression-not-matched");
|
|
511
|
+
var EXPR_NOT_MATCHED = /* @__PURE__ */ Symbol("frond-expression-not-matched");
|
|
508
512
|
function parenthesizedInner(expr) {
|
|
509
513
|
if (expr.length < 2 || expr[0] !== "(" || !expr.endsWith(")")) return null;
|
|
510
514
|
let depth = 0;
|
|
@@ -634,7 +638,7 @@ function evalDottedFunction(name, rawArgs, context) {
|
|
|
634
638
|
const lastDot = name.lastIndexOf(".");
|
|
635
639
|
const owner = resolveVar(name.slice(0, lastDot), context);
|
|
636
640
|
const member = name.slice(lastDot + 1);
|
|
637
|
-
if (!owner || typeof owner !== "object" ||
|
|
641
|
+
if (!owner || typeof owner !== "object" || unsafeKey(member) || !Object.prototype.hasOwnProperty.call(owner, member)) {
|
|
638
642
|
return EXPR_NOT_MATCHED;
|
|
639
643
|
}
|
|
640
644
|
const method = owner[member];
|
|
@@ -960,6 +964,7 @@ var VarRef = class {
|
|
|
960
964
|
constructor(name) {
|
|
961
965
|
this.name = name;
|
|
962
966
|
}
|
|
967
|
+
name;
|
|
963
968
|
};
|
|
964
969
|
function coerceArg(t) {
|
|
965
970
|
if (/^-?\d+$/.test(t)) return parseInt(t, 10);
|
|
@@ -1063,6 +1068,113 @@ function splitArgs(raw) {
|
|
|
1063
1068
|
function htmlEscape(str) {
|
|
1064
1069
|
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1065
1070
|
}
|
|
1071
|
+
var JS_SAFE_RE = /[A-Za-z0-9,._]/;
|
|
1072
|
+
function jsEscape(value) {
|
|
1073
|
+
let out = "";
|
|
1074
|
+
for (const ch of String(value)) {
|
|
1075
|
+
if (JS_SAFE_RE.test(ch) && ch.length === 1) {
|
|
1076
|
+
out += ch;
|
|
1077
|
+
continue;
|
|
1078
|
+
}
|
|
1079
|
+
let code = ch.codePointAt(0);
|
|
1080
|
+
if (code < 128) {
|
|
1081
|
+
out += "\\x" + code.toString(16).toUpperCase().padStart(2, "0");
|
|
1082
|
+
} else if (code <= 65535) {
|
|
1083
|
+
out += "\\u" + code.toString(16).toUpperCase().padStart(4, "0");
|
|
1084
|
+
} else {
|
|
1085
|
+
code -= 65536;
|
|
1086
|
+
out += "\\u" + (55296 + (code >> 10)).toString(16).toUpperCase().padStart(4, "0");
|
|
1087
|
+
out += "\\u" + (56320 + (code & 1023)).toString(16).toUpperCase().padStart(4, "0");
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
return out;
|
|
1091
|
+
}
|
|
1092
|
+
function cssEscape(value) {
|
|
1093
|
+
let out = "";
|
|
1094
|
+
for (const ch of String(value)) {
|
|
1095
|
+
out += /[A-Za-z0-9]/.test(ch) ? ch : "\\" + ch.codePointAt(0).toString(16).toUpperCase().padStart(6, "0") + " ";
|
|
1096
|
+
}
|
|
1097
|
+
return out;
|
|
1098
|
+
}
|
|
1099
|
+
function htmlAttrEscape(value) {
|
|
1100
|
+
let out = "";
|
|
1101
|
+
for (const ch of String(value)) {
|
|
1102
|
+
out += /[A-Za-z0-9,.\-_]/.test(ch) ? ch : "&#x" + ch.codePointAt(0).toString(16).toUpperCase().padStart(2, "0") + ";";
|
|
1103
|
+
}
|
|
1104
|
+
return out;
|
|
1105
|
+
}
|
|
1106
|
+
function urlEscape(value) {
|
|
1107
|
+
return encodeURIComponent(String(value)).replace(
|
|
1108
|
+
/[!'()*]/g,
|
|
1109
|
+
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase()
|
|
1110
|
+
);
|
|
1111
|
+
}
|
|
1112
|
+
function escapeStrategy(value, strategy = "html") {
|
|
1113
|
+
const s = String(strategy).trim().replace(/^['"]|['"]$/g, "");
|
|
1114
|
+
switch (s) {
|
|
1115
|
+
case "":
|
|
1116
|
+
case "html":
|
|
1117
|
+
return new SafeString(htmlEscape(String(value)));
|
|
1118
|
+
case "js":
|
|
1119
|
+
return new SafeString(jsEscape(value));
|
|
1120
|
+
case "url":
|
|
1121
|
+
return new SafeString(urlEscape(value));
|
|
1122
|
+
case "css":
|
|
1123
|
+
return new SafeString(cssEscape(value));
|
|
1124
|
+
case "html_attr":
|
|
1125
|
+
case "attr":
|
|
1126
|
+
return new SafeString(htmlAttrEscape(value));
|
|
1127
|
+
default:
|
|
1128
|
+
throw new Error(`Unknown escape strategy: ${strategy}`);
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
var MEDIA_TYPE_RE = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$/;
|
|
1132
|
+
function sanitiseMediaType(type) {
|
|
1133
|
+
const t = String(type).trim();
|
|
1134
|
+
return MEDIA_TYPE_RE.test(t) ? t : "application/octet-stream";
|
|
1135
|
+
}
|
|
1136
|
+
var SANDBOX_EXPR_KEYWORDS = /* @__PURE__ */ new Set([
|
|
1137
|
+
"true",
|
|
1138
|
+
"false",
|
|
1139
|
+
"none",
|
|
1140
|
+
"null",
|
|
1141
|
+
"nil",
|
|
1142
|
+
"and",
|
|
1143
|
+
"or",
|
|
1144
|
+
"not",
|
|
1145
|
+
"in",
|
|
1146
|
+
"is",
|
|
1147
|
+
"loop",
|
|
1148
|
+
"if",
|
|
1149
|
+
"else",
|
|
1150
|
+
"empty",
|
|
1151
|
+
"starts",
|
|
1152
|
+
"ends",
|
|
1153
|
+
"with",
|
|
1154
|
+
"matches"
|
|
1155
|
+
]);
|
|
1156
|
+
var SANDBOX_IDENT_RE = /(?<![.\w'"])([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
1157
|
+
var SANDBOX_STRING_RE = /'[^']*'|"[^"]*"/g;
|
|
1158
|
+
var UNSAFE_KEYS = /* @__PURE__ */ new Set([
|
|
1159
|
+
"constructor",
|
|
1160
|
+
"__proto__",
|
|
1161
|
+
"prototype",
|
|
1162
|
+
"__defineGetter__",
|
|
1163
|
+
"__defineSetter__",
|
|
1164
|
+
"__lookupGetter__",
|
|
1165
|
+
"__lookupSetter__",
|
|
1166
|
+
"__defineProperty__",
|
|
1167
|
+
"valueOf"
|
|
1168
|
+
]);
|
|
1169
|
+
function unsafeKey(key) {
|
|
1170
|
+
return UNSAFE_KEYS.has(String(key));
|
|
1171
|
+
}
|
|
1172
|
+
function isEscapableStructured(value) {
|
|
1173
|
+
return value !== null && typeof value === "object" && !(value instanceof SafeString);
|
|
1174
|
+
}
|
|
1175
|
+
function toOutputString(value) {
|
|
1176
|
+
return String(value);
|
|
1177
|
+
}
|
|
1066
1178
|
function dateFilter(value, fmt) {
|
|
1067
1179
|
let dt;
|
|
1068
1180
|
if (value instanceof Date) {
|
|
@@ -1183,8 +1295,8 @@ var BUILTIN_FILTERS = {
|
|
|
1183
1295
|
default: (v, fallback) => v !== null && v !== void 0 && v !== "" ? v : fallback !== void 0 ? fallback : "",
|
|
1184
1296
|
raw: (v) => v,
|
|
1185
1297
|
safe: (v) => v,
|
|
1186
|
-
escape: (v) =>
|
|
1187
|
-
e: (v) =>
|
|
1298
|
+
escape: (v, ...a) => escapeStrategy(v, a[0] ?? "html"),
|
|
1299
|
+
e: (v, ...a) => escapeStrategy(v, a[0] ?? "html"),
|
|
1188
1300
|
striptags: (v) => String(v).replace(STRIP_TAGS_RE, ""),
|
|
1189
1301
|
nl2br: (v) => new SafeString(htmlEscape(String(v)).replace(/\n/g, "<br />\n")),
|
|
1190
1302
|
abs: (v) => typeof v === "number" ? Math.abs(v) : v,
|
|
@@ -1273,9 +1385,9 @@ var BUILTIN_FILTERS = {
|
|
|
1273
1385
|
base64decode: (v) => Buffer.from(String(v), "base64").toString("utf-8"),
|
|
1274
1386
|
data_uri: (v) => {
|
|
1275
1387
|
if (v && typeof v === "object" && "content" in v) {
|
|
1276
|
-
const ct = v.type ?? "application/octet-stream";
|
|
1388
|
+
const ct = sanitiseMediaType(String(v.type ?? "application/octet-stream"));
|
|
1277
1389
|
const raw = Buffer.isBuffer(v.content) ? v.content : Buffer.from(String(v.content));
|
|
1278
|
-
return `data:${ct};base64,${raw.toString("base64")}
|
|
1390
|
+
return new SafeString(`data:${ct};base64,${raw.toString("base64")}`);
|
|
1279
1391
|
}
|
|
1280
1392
|
return String(v);
|
|
1281
1393
|
},
|
|
@@ -1295,7 +1407,7 @@ var BUILTIN_FILTERS = {
|
|
|
1295
1407
|
// broke byte-parity for the one filter whose whole job is a wire format.
|
|
1296
1408
|
tojson: (v) => jsonSafe(v),
|
|
1297
1409
|
to_json: (v) => jsonSafe(v),
|
|
1298
|
-
js_escape: (v) => new SafeString(
|
|
1410
|
+
js_escape: (v) => new SafeString(jsEscape(v))
|
|
1299
1411
|
};
|
|
1300
1412
|
function _b64url(data) {
|
|
1301
1413
|
return data.toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
@@ -2002,6 +2114,7 @@ var Frond = class _Frond {
|
|
|
2002
2114
|
return this.evalVarInner(expr, context);
|
|
2003
2115
|
}
|
|
2004
2116
|
evalVarRaw(expr, context) {
|
|
2117
|
+
if (this._sandbox && !this.sandboxExprOk(expr)) return null;
|
|
2005
2118
|
const [varName, filters] = parseFilterChain(expr);
|
|
2006
2119
|
let value = evalExpr(varName, context);
|
|
2007
2120
|
for (const [fname, rawArgs] of filters) {
|
|
@@ -2044,16 +2157,33 @@ var Frond = class _Frond {
|
|
|
2044
2157
|
const fn = this.filters[fname];
|
|
2045
2158
|
return fn ? fn(value, ...args) : value;
|
|
2046
2159
|
}
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2160
|
+
/**
|
|
2161
|
+
* Sandbox gate for a WHOLE expression (F6/ADR-0077): refuse a dunder walk or
|
|
2162
|
+
* a root variable outside the allow-list. Applied to output, if conditions,
|
|
2163
|
+
* set right-hand sides and for iterables so none can smuggle a blocked
|
|
2164
|
+
* variable past the per-output gate.
|
|
2165
|
+
*/
|
|
2166
|
+
sandboxExprOk(expr) {
|
|
2167
|
+
if (!this._sandbox) return true;
|
|
2168
|
+
if (expr.includes("__")) return false;
|
|
2169
|
+
if (this._allowedVars === null) return true;
|
|
2170
|
+
const stripped = expr.replace(SANDBOX_STRING_RE, "");
|
|
2171
|
+
let m;
|
|
2172
|
+
SANDBOX_IDENT_RE.lastIndex = 0;
|
|
2173
|
+
while ((m = SANDBOX_IDENT_RE.exec(stripped)) !== null) {
|
|
2174
|
+
const ident = m[1];
|
|
2175
|
+
if (SANDBOX_EXPR_KEYWORDS.has(ident.toLowerCase())) continue;
|
|
2176
|
+
if (ident in this.filters) continue;
|
|
2177
|
+
if (!this._allowedVars.has(ident)) return false;
|
|
2178
|
+
}
|
|
2179
|
+
return true;
|
|
2051
2180
|
}
|
|
2052
2181
|
resolveConcatenation(expr, context) {
|
|
2053
2182
|
if (findOutsideQuotes(expr, "~") < 0) return { handled: false, value: void 0 };
|
|
2054
2183
|
let value = evalExpr(expr, context);
|
|
2055
2184
|
if (value instanceof SafeString) return { handled: true, value: value.value };
|
|
2056
2185
|
if (this._autoEscape && typeof value === "string") value = htmlEscape(value);
|
|
2186
|
+
else if (this._autoEscape && isEscapableStructured(value)) value = htmlEscape(toOutputString(value));
|
|
2057
2187
|
return { handled: true, value };
|
|
2058
2188
|
}
|
|
2059
2189
|
applyRenderedFilters(value, filters, context) {
|
|
@@ -2064,7 +2194,6 @@ var Frond = class _Frond {
|
|
|
2064
2194
|
if (this.filterPermitted(fname)) safe = true;
|
|
2065
2195
|
continue;
|
|
2066
2196
|
}
|
|
2067
|
-
if ((fname === "escape" || fname === "e") && this.filterPermitted(fname)) safe = true;
|
|
2068
2197
|
if (!this.filterPermitted(fname)) continue;
|
|
2069
2198
|
value = this.applyRenderedFilter(value, fname, args);
|
|
2070
2199
|
}
|
|
@@ -2072,7 +2201,7 @@ var Frond = class _Frond {
|
|
|
2072
2201
|
}
|
|
2073
2202
|
evalVarInner(expr, context) {
|
|
2074
2203
|
const [varName, filters] = parseFilterChain(expr);
|
|
2075
|
-
if (!this.
|
|
2204
|
+
if (this._sandbox && !this.sandboxExprOk(expr)) return "";
|
|
2076
2205
|
const concatenated = this.resolveConcatenation(expr, context);
|
|
2077
2206
|
if (concatenated.handled) return concatenated.value;
|
|
2078
2207
|
const applied = this.applyRenderedFilters(evalExpr(varName, context), filters, context);
|
|
@@ -2080,8 +2209,12 @@ var Frond = class _Frond {
|
|
|
2080
2209
|
if (value instanceof SafeString) {
|
|
2081
2210
|
return value.value;
|
|
2082
2211
|
}
|
|
2083
|
-
if (!applied.safe && this._autoEscape
|
|
2084
|
-
value
|
|
2212
|
+
if (!applied.safe && this._autoEscape) {
|
|
2213
|
+
if (typeof value === "string") {
|
|
2214
|
+
value = htmlEscape(value);
|
|
2215
|
+
} else if (isEscapableStructured(value)) {
|
|
2216
|
+
value = new SafeString(htmlEscape(toOutputString(value)));
|
|
2217
|
+
}
|
|
2085
2218
|
}
|
|
2086
2219
|
return value;
|
|
2087
2220
|
}
|
|
@@ -2188,7 +2321,8 @@ var Frond = class _Frond {
|
|
|
2188
2321
|
const var1 = forMatch[1];
|
|
2189
2322
|
const var2 = forMatch[2] || null;
|
|
2190
2323
|
const { bodyTokens, elseTokens, next: i } = this.collectForTokens(tokens, start);
|
|
2191
|
-
const
|
|
2324
|
+
const iterableExpr = forMatch[3].trim();
|
|
2325
|
+
const iterable = this._sandbox && !this.sandboxExprOk(iterableExpr) ? null : evalExpr(iterableExpr, context);
|
|
2192
2326
|
const { items, isDict } = this.forItems(iterable);
|
|
2193
2327
|
if (items.length === 0) return [elseTokens.length ? this.renderTokens([...elseTokens], context) : "", i];
|
|
2194
2328
|
const output = [];
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
/**
|
|
2
10
|
* Tina4 Frond Engine — Lexer, parser, and runtime.
|
|
3
11
|
* Zero-dependency Twig-like template engine.
|
|
@@ -570,6 +578,11 @@ function resolveMethodPart(value: unknown, part: string, context: Record<string,
|
|
|
570
578
|
if (!methodMatch || typeof value !== "object" || value === null) return { matched: false, value };
|
|
571
579
|
const methodName = methodMatch[1];
|
|
572
580
|
const rawArgs = methodMatch[2] || "";
|
|
581
|
+
// F6/ADR-0077: never traverse a prototype/constructor/dunder key -- that is
|
|
582
|
+
// the Function-constructor and prototype-pollution escape.
|
|
583
|
+
if (unsafeKey(methodName) || !Object.prototype.hasOwnProperty.call(value, methodName)) {
|
|
584
|
+
return { matched: false, value };
|
|
585
|
+
}
|
|
573
586
|
const fn = (value as Record<string, unknown>)[methodName];
|
|
574
587
|
if (typeof fn !== "function") return { matched: false, value };
|
|
575
588
|
const args = rawArgs.trim() ? splitArgs(rawArgs).map(a => evalExpr(a.trim(), context)) : [];
|
|
@@ -592,7 +605,9 @@ function resolvePathKey(part: string, isBracket: boolean, context: Record<string
|
|
|
592
605
|
function resolvePathPart(value: unknown, key: string | number): { found: boolean; value: unknown } {
|
|
593
606
|
if (typeof value !== "object" || value === null) return { found: false, value: null };
|
|
594
607
|
if (Array.isArray(value) && typeof key === "number") return { found: true, value: value[key] };
|
|
595
|
-
|
|
608
|
+
// F6/ADR-0077: only own, non-dunder properties are reachable, so an
|
|
609
|
+
// inherited prototype method (constructor, __proto__, ...) cannot be called.
|
|
610
|
+
if (unsafeKey(key) || !Object.prototype.hasOwnProperty.call(value, key)) return { found: false, value: null };
|
|
596
611
|
const member = (value as Record<string, unknown>)[key as string];
|
|
597
612
|
return { found: true, value: typeof member === "function" ? member.call(value) : member };
|
|
598
613
|
}
|
|
@@ -889,7 +904,10 @@ function evalDottedFunction(name: string, rawArgs: string, context: Record<strin
|
|
|
889
904
|
const lastDot = name.lastIndexOf(".");
|
|
890
905
|
const owner = resolveVar(name.slice(0, lastDot), context);
|
|
891
906
|
const member = name.slice(lastDot + 1);
|
|
892
|
-
|
|
907
|
+
// F6/ADR-0077: never call through a prototype/constructor/dunder key or an
|
|
908
|
+
// inherited method -- that is the Function-constructor and prototype escape.
|
|
909
|
+
if (!owner || typeof owner !== "object" || unsafeKey(member) ||
|
|
910
|
+
!Object.prototype.hasOwnProperty.call(owner, member)) {
|
|
893
911
|
return EXPR_NOT_MATCHED;
|
|
894
912
|
}
|
|
895
913
|
const method = (owner as Record<string, unknown>)[member];
|
|
@@ -1361,6 +1379,115 @@ function htmlEscape(str: string): string {
|
|
|
1361
1379
|
.replace(/'/g, "'");
|
|
1362
1380
|
}
|
|
1363
1381
|
|
|
1382
|
+
// -- Escape strategies (ADR-0077) -------------------------------------------
|
|
1383
|
+
// Shared by js_escape and e(strategy); byte-identical to the Python master.
|
|
1384
|
+
// Twig-compatible. Iterates code points so astral characters emit a surrogate
|
|
1385
|
+
// pair for the js strategy.
|
|
1386
|
+
const JS_SAFE_RE = /[A-Za-z0-9,._]/;
|
|
1387
|
+
function jsEscape(value: unknown): string {
|
|
1388
|
+
let out = "";
|
|
1389
|
+
for (const ch of String(value)) {
|
|
1390
|
+
if (JS_SAFE_RE.test(ch) && ch.length === 1) {
|
|
1391
|
+
out += ch;
|
|
1392
|
+
continue;
|
|
1393
|
+
}
|
|
1394
|
+
let code = ch.codePointAt(0)!;
|
|
1395
|
+
if (code < 0x80) {
|
|
1396
|
+
out += "\\x" + code.toString(16).toUpperCase().padStart(2, "0");
|
|
1397
|
+
} else if (code <= 0xffff) {
|
|
1398
|
+
out += "\\u" + code.toString(16).toUpperCase().padStart(4, "0");
|
|
1399
|
+
} else {
|
|
1400
|
+
code -= 0x10000;
|
|
1401
|
+
out += "\\u" + (0xd800 + (code >> 10)).toString(16).toUpperCase().padStart(4, "0");
|
|
1402
|
+
out += "\\u" + (0xdc00 + (code & 0x3ff)).toString(16).toUpperCase().padStart(4, "0");
|
|
1403
|
+
}
|
|
1404
|
+
}
|
|
1405
|
+
return out;
|
|
1406
|
+
}
|
|
1407
|
+
|
|
1408
|
+
function cssEscape(value: unknown): string {
|
|
1409
|
+
let out = "";
|
|
1410
|
+
for (const ch of String(value)) {
|
|
1411
|
+
out += /[A-Za-z0-9]/.test(ch)
|
|
1412
|
+
? ch
|
|
1413
|
+
: "\\" + ch.codePointAt(0)!.toString(16).toUpperCase().padStart(6, "0") + " ";
|
|
1414
|
+
}
|
|
1415
|
+
return out;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
function htmlAttrEscape(value: unknown): string {
|
|
1419
|
+
let out = "";
|
|
1420
|
+
for (const ch of String(value)) {
|
|
1421
|
+
out += /[A-Za-z0-9,.\-_]/.test(ch)
|
|
1422
|
+
? ch
|
|
1423
|
+
: "&#x" + ch.codePointAt(0)!.toString(16).toUpperCase().padStart(2, "0") + ";";
|
|
1424
|
+
}
|
|
1425
|
+
return out;
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function urlEscape(value: unknown): string {
|
|
1429
|
+
return encodeURIComponent(String(value)).replace(
|
|
1430
|
+
/[!'()*]/g,
|
|
1431
|
+
(c) => "%" + c.charCodeAt(0).toString(16).toUpperCase(),
|
|
1432
|
+
);
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
// Dispatch e(strategy)/escape(strategy). Unknown strategy throws so a typo can
|
|
1436
|
+
// never silently pass the value through unescaped.
|
|
1437
|
+
function escapeStrategy(value: unknown, strategy: string = "html"): SafeString {
|
|
1438
|
+
const s = String(strategy).trim().replace(/^['"]|['"]$/g, "");
|
|
1439
|
+
switch (s) {
|
|
1440
|
+
case "":
|
|
1441
|
+
case "html":
|
|
1442
|
+
return new SafeString(htmlEscape(String(value)));
|
|
1443
|
+
case "js":
|
|
1444
|
+
return new SafeString(jsEscape(value));
|
|
1445
|
+
case "url":
|
|
1446
|
+
return new SafeString(urlEscape(value));
|
|
1447
|
+
case "css":
|
|
1448
|
+
return new SafeString(cssEscape(value));
|
|
1449
|
+
case "html_attr":
|
|
1450
|
+
case "attr":
|
|
1451
|
+
return new SafeString(htmlAttrEscape(value));
|
|
1452
|
+
default:
|
|
1453
|
+
throw new Error(`Unknown escape strategy: ${strategy}`);
|
|
1454
|
+
}
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// Confine a client-supplied media type to type/subtype (F3/ADR-0077).
|
|
1458
|
+
const MEDIA_TYPE_RE = /^[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*\/[A-Za-z0-9][A-Za-z0-9!#$&^_.+-]*$/;
|
|
1459
|
+
function sanitiseMediaType(type: string): string {
|
|
1460
|
+
const t = String(type).trim();
|
|
1461
|
+
return MEDIA_TYPE_RE.test(t) ? t : "application/octet-stream";
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// Property keys that must never be traversed/invoked through {{ obj.x }} —
|
|
1465
|
+
// prototype pollution and the Function constructor RCE (F6/ADR-0077).
|
|
1466
|
+
const SANDBOX_EXPR_KEYWORDS = new Set([
|
|
1467
|
+
"true","false","none","null","nil","and","or","not","in","is","loop",
|
|
1468
|
+
"if","else","empty","starts","ends","with","matches",
|
|
1469
|
+
]);
|
|
1470
|
+
const SANDBOX_IDENT_RE = /(?<![.\w'"])([A-Za-z_][A-Za-z0-9_]*)/g;
|
|
1471
|
+
const SANDBOX_STRING_RE = /'[^']*'|"[^"]*"/g;
|
|
1472
|
+
const UNSAFE_KEYS = new Set([
|
|
1473
|
+
"constructor", "__proto__", "prototype", "__defineGetter__",
|
|
1474
|
+
"__defineSetter__", "__lookupGetter__", "__lookupSetter__",
|
|
1475
|
+
"__defineProperty__", "valueOf",
|
|
1476
|
+
]);
|
|
1477
|
+
function unsafeKey(key: string | number): boolean {
|
|
1478
|
+
return UNSAFE_KEYS.has(String(key));
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
// A value whose rendered string form must be auto-escaped: an array/object
|
|
1482
|
+
// (not a SafeString). Strings, numbers, booleans and null are handled by the
|
|
1483
|
+
// caller's own branches. (F4/ADR-0077)
|
|
1484
|
+
function isEscapableStructured(value: unknown): boolean {
|
|
1485
|
+
return value !== null && typeof value === "object" && !(value instanceof SafeString);
|
|
1486
|
+
}
|
|
1487
|
+
function toOutputString(value: unknown): string {
|
|
1488
|
+
return String(value);
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1364
1491
|
function dateFilter(value: unknown, fmt: string): string {
|
|
1365
1492
|
let dt: Date;
|
|
1366
1493
|
if (value instanceof Date) {
|
|
@@ -1514,8 +1641,8 @@ const BUILTIN_FILTERS: Record<string, FilterFn> = {
|
|
|
1514
1641
|
default: (v, fallback) => (v !== null && v !== undefined && v !== "") ? v : (fallback !== undefined ? fallback : ""),
|
|
1515
1642
|
raw: (v) => v,
|
|
1516
1643
|
safe: (v) => v,
|
|
1517
|
-
escape: (v) =>
|
|
1518
|
-
e: (v) =>
|
|
1644
|
+
escape: (v, ...a) => escapeStrategy(v, (a[0] as string) ?? "html"),
|
|
1645
|
+
e: (v, ...a) => escapeStrategy(v, (a[0] as string) ?? "html"),
|
|
1519
1646
|
striptags: (v) => String(v).replace(STRIP_TAGS_RE, ""),
|
|
1520
1647
|
nl2br: (v) => new SafeString(htmlEscape(String(v)).replace(/\n/g, "<br />\n")),
|
|
1521
1648
|
abs: (v) => typeof v === "number" ? Math.abs(v) : v,
|
|
@@ -1604,9 +1731,9 @@ const BUILTIN_FILTERS: Record<string, FilterFn> = {
|
|
|
1604
1731
|
base64decode: (v) => Buffer.from(String(v), "base64").toString("utf-8"),
|
|
1605
1732
|
data_uri: (v) => {
|
|
1606
1733
|
if (v && typeof v === "object" && "content" in v) {
|
|
1607
|
-
const ct = (v as any).type ?? "application/octet-stream";
|
|
1734
|
+
const ct = sanitiseMediaType(String((v as any).type ?? "application/octet-stream"));
|
|
1608
1735
|
const raw = Buffer.isBuffer((v as any).content) ? (v as any).content : Buffer.from(String((v as any).content));
|
|
1609
|
-
return `data:${ct};base64,${raw.toString("base64")}
|
|
1736
|
+
return new SafeString(`data:${ct};base64,${raw.toString("base64")}`);
|
|
1610
1737
|
}
|
|
1611
1738
|
return String(v);
|
|
1612
1739
|
},
|
|
@@ -1626,7 +1753,7 @@ const BUILTIN_FILTERS: Record<string, FilterFn> = {
|
|
|
1626
1753
|
// broke byte-parity for the one filter whose whole job is a wire format.
|
|
1627
1754
|
tojson: (v) => jsonSafe(v),
|
|
1628
1755
|
to_json: (v) => jsonSafe(v),
|
|
1629
|
-
js_escape: (v) => new SafeString(
|
|
1756
|
+
js_escape: (v) => new SafeString(jsEscape(v)),
|
|
1630
1757
|
};
|
|
1631
1758
|
|
|
1632
1759
|
// ── Form Token ────────────────────────────────────────────────
|
|
@@ -2526,6 +2653,9 @@ export class Frond {
|
|
|
2526
2653
|
}
|
|
2527
2654
|
|
|
2528
2655
|
private evalVarRaw(expr: string, context: Record<string, unknown>): unknown {
|
|
2656
|
+
// F6/ADR-0077: gate the whole condition/RHS so a comparison or iterable
|
|
2657
|
+
// cannot leak a blocked variable.
|
|
2658
|
+
if (this._sandbox && !this.sandboxExprOk(expr)) return null;
|
|
2529
2659
|
const [varName, filters] = parseFilterChain(expr);
|
|
2530
2660
|
let value = evalExpr(varName, context);
|
|
2531
2661
|
for (const [fname, rawArgs] of filters) {
|
|
@@ -2576,10 +2706,26 @@ export class Frond {
|
|
|
2576
2706
|
return fn ? fn(value, ...args) : value;
|
|
2577
2707
|
}
|
|
2578
2708
|
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
|
|
2709
|
+
/**
|
|
2710
|
+
* Sandbox gate for a WHOLE expression (F6/ADR-0077): refuse a dunder walk or
|
|
2711
|
+
* a root variable outside the allow-list. Applied to output, if conditions,
|
|
2712
|
+
* set right-hand sides and for iterables so none can smuggle a blocked
|
|
2713
|
+
* variable past the per-output gate.
|
|
2714
|
+
*/
|
|
2715
|
+
private sandboxExprOk(expr: string): boolean {
|
|
2716
|
+
if (!this._sandbox) return true;
|
|
2717
|
+
if (expr.includes("__")) return false;
|
|
2718
|
+
if (this._allowedVars === null) return true;
|
|
2719
|
+
const stripped = expr.replace(SANDBOX_STRING_RE, "");
|
|
2720
|
+
let m: RegExpExecArray | null;
|
|
2721
|
+
SANDBOX_IDENT_RE.lastIndex = 0;
|
|
2722
|
+
while ((m = SANDBOX_IDENT_RE.exec(stripped)) !== null) {
|
|
2723
|
+
const ident = m[1];
|
|
2724
|
+
if (SANDBOX_EXPR_KEYWORDS.has(ident.toLowerCase())) continue;
|
|
2725
|
+
if (ident in this.filters) continue;
|
|
2726
|
+
if (!this._allowedVars.has(ident)) return false;
|
|
2727
|
+
}
|
|
2728
|
+
return true;
|
|
2583
2729
|
}
|
|
2584
2730
|
|
|
2585
2731
|
private resolveConcatenation(
|
|
@@ -2590,6 +2736,7 @@ export class Frond {
|
|
|
2590
2736
|
let value = evalExpr(expr, context);
|
|
2591
2737
|
if (value instanceof SafeString) return { handled: true, value: value.value };
|
|
2592
2738
|
if (this._autoEscape && typeof value === "string") value = htmlEscape(value);
|
|
2739
|
+
else if (this._autoEscape && isEscapableStructured(value)) value = htmlEscape(toOutputString(value));
|
|
2593
2740
|
return { handled: true, value };
|
|
2594
2741
|
}
|
|
2595
2742
|
|
|
@@ -2605,7 +2752,10 @@ export class Frond {
|
|
|
2605
2752
|
if (this.filterPermitted(fname)) safe = true;
|
|
2606
2753
|
continue;
|
|
2607
2754
|
}
|
|
2608
|
-
|
|
2755
|
+
// F2/ADR-0077: do NOT keep a persistent "safe" flag for escape/e. The
|
|
2756
|
+
// filter returns a SafeString; if a later filter (replace/format/join)
|
|
2757
|
+
// transforms it, the result is a plain string and is re-escaped at
|
|
2758
|
+
// output. Keeping a safe flag here let post-escape filters emit raw.
|
|
2609
2759
|
if (!this.filterPermitted(fname)) continue;
|
|
2610
2760
|
value = this.applyRenderedFilter(value, fname, args);
|
|
2611
2761
|
}
|
|
@@ -2614,7 +2764,7 @@ export class Frond {
|
|
|
2614
2764
|
|
|
2615
2765
|
private evalVarInner(expr: string, context: Record<string, unknown>): unknown {
|
|
2616
2766
|
const [varName, filters] = parseFilterChain(expr);
|
|
2617
|
-
if (!this.
|
|
2767
|
+
if (this._sandbox && !this.sandboxExprOk(expr)) return "";
|
|
2618
2768
|
const concatenated = this.resolveConcatenation(expr, context);
|
|
2619
2769
|
if (concatenated.handled) return concatenated.value;
|
|
2620
2770
|
const applied = this.applyRenderedFilters(evalExpr(varName, context), filters, context);
|
|
@@ -2625,9 +2775,16 @@ export class Frond {
|
|
|
2625
2775
|
return value.value;
|
|
2626
2776
|
}
|
|
2627
2777
|
|
|
2628
|
-
// Auto-escape HTML unless marked safe or auto-escape is disabled
|
|
2629
|
-
|
|
2630
|
-
|
|
2778
|
+
// Auto-escape HTML unless marked safe or auto-escape is disabled. A plain
|
|
2779
|
+
// string is escaped directly; an array/object is escaped on its rendered
|
|
2780
|
+
// string form (F4/ADR-0077) -- before this, only string values were
|
|
2781
|
+
// escaped, so {{ items }} holding markup emitted it raw.
|
|
2782
|
+
if (!applied.safe && this._autoEscape) {
|
|
2783
|
+
if (typeof value === "string") {
|
|
2784
|
+
value = htmlEscape(value);
|
|
2785
|
+
} else if (isEscapableStructured(value)) {
|
|
2786
|
+
value = new SafeString(htmlEscape(toOutputString(value)));
|
|
2787
|
+
}
|
|
2631
2788
|
}
|
|
2632
2789
|
|
|
2633
2790
|
return value;
|
|
@@ -2756,7 +2913,10 @@ export class Frond {
|
|
|
2756
2913
|
const { bodyTokens, elseTokens, next: i } = this.collectForTokens(tokens, start);
|
|
2757
2914
|
|
|
2758
2915
|
// Evaluate iterable
|
|
2759
|
-
const
|
|
2916
|
+
const iterableExpr = forMatch[3].trim();
|
|
2917
|
+
const iterable = (this._sandbox && !this.sandboxExprOk(iterableExpr))
|
|
2918
|
+
? null
|
|
2919
|
+
: evalExpr(iterableExpr, context);
|
|
2760
2920
|
const { items, isDict } = this.forItems(iterable);
|
|
2761
2921
|
if (items.length === 0) return [elseTokens.length ? this.renderTokens([...elseTokens], context) : "", i];
|
|
2762
2922
|
|
|
@@ -1,2 +1,10 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Copyright (c) 2026 Code Infinity
|
|
3
|
+
SPDX-License-Identifier: MPL-2.0
|
|
4
|
+
This Source Code Form is subject to the terms of the Mozilla Public
|
|
5
|
+
License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
6
|
+
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
|
7
|
+
*/
|
|
8
|
+
|
|
1
9
|
export { Frond } from "./engine.js";
|
|
2
10
|
export type { FilterFn, TestFn, LiveProvider, LiveRequest, LiveResponse, LiveBroadcaster } from "./engine.js";
|