Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion dist/patchloop-widget.js
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,13 @@ const __pl_shared_format = (() => {
// link hardening (safeLinkUrl / mdLinkUrl) and the screenshot status texts
// stay in their respective owners because their semantics differ per side.

function safeFilePart(value) {
return String(value || "feedback")
.replace(/[^a-zA-Z0-9_-]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 80) || "feedback";
}

function truncateText(value, max) {
const text = String(value ?? "");
return text.length > max ? `${text.slice(0, max)}…` : text;
Expand Down Expand Up @@ -421,7 +428,7 @@ function formatTarget(target) {
return `${target.kind || "point"} at ${present(target.clientX)},${present(target.clientY)}`;
}

return { truncateText, present, escapeHtml, escapeXml, slackEscape, formatSlackCode, formatSlackLink, formatViewport, formatTarget };
return { safeFilePart, truncateText, present, escapeHtml, escapeXml, slackEscape, formatSlackCode, formatSlackLink, formatViewport, formatTarget };
})();
const { pointFromClient, rectFromPoints, rectContainsArea, pointFromStoredTarget, rectFromStoredArea, round, numberOrNull } = __pl_widget_src_geometry;
const { pointAnchorOffsets, areaAnchorOffsets, roundedAnchor, geometryFromAnchor, viewportDiffersFromCreation } = __pl_widget_src_anchoring;
Expand Down
30 changes: 29 additions & 1 deletion scripts/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,30 @@ function parseBindingList(raw, context) {
});
}

function hasTopLevelComma(line) {
const depths = { "(": 0, "[": 0, "{": 0 };
const closing = { ")": "(", "]": "[", "}": "{" };
let quote = null;
let escaped = false;

for (const char of line) {
if (quote) {
if (escaped) escaped = false;
else if (char === "\\") escaped = true;
else if (char === quote) quote = null;
continue;
}
if (char === '"' || char === "'" || char === "`") {
quote = char;
continue;
}
if (char in depths) depths[char] += 1;
else if (char in closing) depths[closing[char]] -= 1;
else if (char === "," && Object.values(depths).every((depth) => depth === 0)) return true;
}
return false;
}

// Parses one module into { imports, exports, bodyLines }. Import statements
// are removed from the body; `export` keywords are stripped in place.
function parseModule(filePath) {
Expand Down Expand Up @@ -75,7 +99,11 @@ function parseModule(filePath) {
});
return;
}
const declMatch = EXPORT_DECL_RE.exec(line) || EXPORT_VAR_RE.exec(line);
const varMatch = EXPORT_VAR_RE.exec(line);
if (varMatch && hasTopLevelComma(line)) {
fail(`${where}: multiple declarators in an export declaration are not supported`);
}
const declMatch = EXPORT_DECL_RE.exec(line) || varMatch;
if (!declMatch) {
fail(`${where}: only named export declarations and export lists are supported`);
}
Expand Down
12 changes: 2 additions & 10 deletions server/receive.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ const fs = require("fs");
const path = require("path");
const crypto = require("crypto");

const { truncateText, present, escapeHtml, slackEscape, formatSlackCode, formatSlackLink, formatViewport, formatTarget } = require("../shared/format.js");
const { createStore } = require("./store.js");
const { safeFilePart, truncateText, present, escapeHtml, slackEscape, formatSlackCode, formatSlackLink, formatViewport, formatTarget } = require("../shared/format.js");
const { createStore, FEEDBACK_STATUSES } = require("./store.js");

const CONFIG_PATH = process.env.PATCHLOOP_RECEIVER_CONFIG || path.join(__dirname, "receiver.config.json");
const config = loadConfig(CONFIG_PATH);
Expand Down Expand Up @@ -95,7 +95,6 @@ const IMPORT_BUNDLE_KIND = "patchloop-feedback-bundle";
// Both are accepted so files exported before the batch-download switch still
// import.
const SUPPORTED_IMPORT_BUNDLE_VERSIONS = new Set([1, 2]);
const FEEDBACK_STATUSES = ["new", "accepted", "fixed", "ignored"];
// Default applied to payloads received before the widget sent schemaVersion,
// so every stored item carries a version going forward.
const DEFAULT_SCHEMA_VERSION = 1;
Expand Down Expand Up @@ -1496,13 +1495,6 @@ function contentTypeForPath(filePath) {
return "application/octet-stream";
}

function safeFilePart(value) {
return String(value || "feedback")
.replace(/[^a-zA-Z0-9_-]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 80) || "feedback";
}

function httpError(message, statusCode) {
const error = new Error(message);
error.statusCode = statusCode;
Expand Down
7 changes: 4 additions & 3 deletions server/store.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
// delete(id) -> item|null returns the removed item (for screenshot cleanup)
// count() -> number
// close()
// Exported FEEDBACK_STATUSES is the shared status allowlist used by the receiver.
//
// A new backend (e.g. createMysqlStore) just needs to implement this shape and
// be wired into createStore() below; the receiver code stays unchanged.
Expand All @@ -38,10 +39,10 @@ function isUniqueViolation(error) {
return /UNIQUE constraint failed/i.test(error && error.message);
}

const VALID_STATUSES = ["new", "accepted", "fixed", "ignored"];
const FEEDBACK_STATUSES = ["new", "accepted", "fixed", "ignored"];

function normalizeStatus(value) {
return VALID_STATUSES.includes(value) ? value : "new";
return FEEDBACK_STATUSES.includes(value) ? value : "new";
}

// Columns extracted from each feedback object for indexed filtering. The full
Expand Down Expand Up @@ -218,4 +219,4 @@ function createStore(config = {}) {
throw new Error(`Unknown store backend: ${backend}`);
}

module.exports = { createStore };
module.exports = { createStore, FEEDBACK_STATUSES };
7 changes: 7 additions & 0 deletions shared/format.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
// link hardening (safeLinkUrl / mdLinkUrl) and the screenshot status texts
// stay in their respective owners because their semantics differ per side.

export function safeFilePart(value) {
return String(value || "feedback")
.replace(/[^a-zA-Z0-9_-]+/g, "-")
.replace(/^-+|-+$/g, "")
.slice(0, 80) || "feedback";
}

export function truncateText(value, max) {
const text = String(value ?? "");
return text.length > max ? `${text.slice(0, max)}…` : text;
Expand Down
10 changes: 10 additions & 0 deletions test/shared-format.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const assert = require("node:assert/strict");
const test = require("node:test");

const {
safeFilePart,
truncateText,
present,
escapeHtml,
Expand All @@ -15,6 +16,15 @@ const {
formatTarget
} = require("../shared/format.js");

test("safeFilePart creates a bounded filesystem-safe component", () => {
assert.equal(safeFilePart("feedback_123"), "feedback_123");
assert.equal(safeFilePart(" --hello, world!!-- "), "hello-world");
assert.equal(safeFilePart(""), "feedback");
assert.equal(safeFilePart(null), "feedback");
assert.equal(safeFilePart(false), "feedback");
assert.equal(safeFilePart("x".repeat(81)), "x".repeat(80));
});

test("truncateText coerces and appends an ellipsis past the limit", () => {
assert.equal(truncateText("hello", 10), "hello");
assert.equal(truncateText("hello world", 5), "hello…");
Expand Down
Loading