openvibers/openvibe.watch Commits
Watch: a link can start the new-watch form filled in (bounded values only) (#48)
Changes
@@@ -508,6 +508,20 @@ ${older ? html`<a class="btn" href="${older}">Older watches</a>` : html`<span clconst getTemplate = (q) => (TEMPLATE_NAMES.includes(String(q)) ? String(q) : 'page');++ /**++ * A link from another OpenVibe site may start the form filled in (Host's "Watch this price": template json, the plan's++ * API URL, value_path plan.price_monthly, op changed). Only form defaults: the person still reviews and submits, and++ * the POST validates everything as usual. Each value is bounded; anything unexpected is dropped, never an error.++ */++ function prefilled(template, q = {}) {++ const fields = { ...blankFields(template), url: typeof q.url === 'string' ? q.url.slice(0, 2000) : '' };++ if (typeof q.name === 'string') fields.name = q.name.replace(/[\u0000-\u001f]/g, '').slice(0, 80);++ if (typeof q.value_path === 'string' && /^[A-Za-z0-9_$][A-Za-z0-9_.$\[\]-]{0,199}$/.test(q.value_path)) fields.value_path = q.value_path;++ if (typeof q.op === 'string' && TEMPLATES[template].ops.includes(q.op)) fields.op = q.op;++ if (typeof q.value === 'string') fields.value = q.value.slice(0, 200);++ return fields;++ }++r.get('/watches/new', (req, res) => {if (!signedIn(req)) return res.redirect(303, `/auth/login?next=${encodeURIComponent(req.originalUrl)}`);const template = getTemplate(req.query.template);@@@ -516,7 +530,7 @@ ${older ? html`<a class="btn" href="${older}">Older watches</a>` : html`<span clbody: html`<h1>New watch</h1><p class="muted">${TEMPLATES[template].title}.</p>${templateChooser(template)}--${watchForm({ template, fields: { ...blankFields(template), url: String(req.query.url || '') }, errors: {}, action: '/watches/new', submit: 'Create this watch' })}`,++${watchForm({ template, fields: prefilled(template, req.query), errors: {}, action: '/watches/new', submit: 'Create this watch' })}`,});});@@@ -141,6 +141,22 @@ t('notify me off stores an event action instead of a notification, and says so',assert.deepStrictEqual(read.body.watch.action, [{ kind: 'event', event_type: 'watch.watch.triggered' }]);});++t('a link can start the form filled in, with bounded values only', async () => {++ const q = new URLSearchParams({ template: 'json', url: 'https://openvibe.host/api/v1/catalog/plans/hpl_x', value_path: 'plan.price_monthly', op: 'changed', name: 'Linode Nanode 1GB price' });++ const form = await svc.get(`/watches/new?${q}`, { cookie: session });++ assert.strictEqual(form.status, 200);++ assert.match(form.text, /value="https:\/\/openvibe\.host\/api\/v1\/catalog\/plans\/hpl_x"/);++ assert.match(form.text, /value="plan\.price_monthly"/);++ assert.match(form.text, /value="Linode Nanode 1GB price"/);++ assert.match(form.text, /<option value="changed" selected/);++ const odd = new URLSearchParams({ template: 'json', value_path: 'a b<script>', op: 'drop-table' });++ const ignored = await svc.get(`/watches/new?${odd}`, { cookie: session });++ assert.strictEqual(ignored.status, 200);++ assert.doesNotMatch(ignored.text, /a b<script>|a b<script>/, 'a malformed path is dropped');++ assert.doesNotMatch(ignored.text, /<option value="drop-table"/, 'an op the template does not have is dropped');++ assert.match(ignored.text, /<option value="lt" selected/, 'the template default op stays selected');++});++t('the plain create and edit forms save an Actor agent action', async () => {const form = await svc.get('/watches/new?template=page', { cookie: session });assert.match(form.text, /Run one of my agents on OpenVibe\.Actor/);
@@ -508,6 +508,20 @@ ${older ? html`<a class="btn" href="${older}">Older watches</a>` : html`<span cl
@@ -508,6 +508,20 @@ ${older ? html`<a class="btn" href="${older}">Older watches</a>` : html`<span cl
const getTemplate = (q) => (TEMPLATE_NAMES.includes(String(q)) ? String(q) : 'page');
const getTemplate = (q) => (TEMPLATE_NAMES.includes(String(q)) ? String(q) : 'page');
+ /**
+ * A link from another OpenVibe site may start the form filled in (Host's "Watch this price": template json, the plan's
+ * API URL, value_path plan.price_monthly, op changed). Only form defaults: the person still reviews and submits, and
+ * the POST validates everything as usual. Each value is bounded; anything unexpected is dropped, never an error.
+ */
+ function prefilled(template, q = {}) {
+ const fields = { ...blankFields(template), url: typeof q.url === 'string' ? q.url.slice(0, 2000) : '' };
+ if (typeof q.name === 'string') fields.name = q.name.replace(/[\u0000-\u001f]/g, '').slice(0, 80);
+ if (typeof q.value_path === 'string' && /^[A-Za-z0-9_$][A-Za-z0-9_.$\[\]-]{0,199}$/.test(q.value_path)) fields.value_path = q.value_path;
+ if (typeof q.op === 'string' && TEMPLATES[template].ops.includes(q.op)) fields.op = q.op;
+ if (typeof q.value === 'string') fields.value = q.value.slice(0, 200);
+ return fields;
+ }
+
r.get('/watches/new', (req, res) => {
r.get('/watches/new', (req, res) => {
if (!signedIn(req)) return res.redirect(303, `/auth/login?next=${encodeURIComponent(req.originalUrl)}`);
if (!signedIn(req)) return res.redirect(303, `/auth/login?next=${encodeURIComponent(req.originalUrl)}`);
const template = getTemplate(req.query.template);
const template = getTemplate(req.query.template);
@@ -516,7 +530,7 @@ ${older ? html`<a class="btn" href="${older}">Older watches</a>` : html`<span cl
@@ -516,7 +530,7 @@ ${older ? html`<a class="btn" href="${older}">Older watches</a>` : html`<span cl
body: html`<h1>New watch</h1>
body: html`<h1>New watch</h1>
<p class="muted">${TEMPLATES[template].title}.</p>
<p class="muted">${TEMPLATES[template].title}.</p>
${templateChooser(template)}
${templateChooser(template)}
-${watchForm({ template, fields: { ...blankFields(template), url: String(req.query.url || '') }, errors: {}, action: '/watches/new', submit: 'Create this watch' })}`,
+${watchForm({ template, fields: prefilled(template, req.query), errors: {}, action: '/watches/new', submit: 'Create this watch' })}`,
});
});
});
});
@@ -141,6 +141,22 @@ t('notify me off stores an event action instead of a notification, and says so',
@@ -141,6 +141,22 @@ t('notify me off stores an event action instead of a notification, and says so',
assert.deepStrictEqual(read.body.watch.action, [{ kind: 'event', event_type: 'watch.watch.triggered' }]);
assert.deepStrictEqual(read.body.watch.action, [{ kind: 'event', event_type: 'watch.watch.triggered' }]);
});
});
+t('a link can start the form filled in, with bounded values only', async () => {
+ const q = new URLSearchParams({ template: 'json', url: 'https://openvibe.host/api/v1/catalog/plans/hpl_x', value_path: 'plan.price_monthly', op: 'changed', name: 'Linode Nanode 1GB price' });
+ const form = await svc.get(`/watches/new?${q}`, { cookie: session });
+ assert.strictEqual(form.status, 200);
+ assert.match(form.text, /value="https:\/\/openvibe\.host\/api\/v1\/catalog\/plans\/hpl_x"/);
+ assert.match(form.text, /value="plan\.price_monthly"/);
+ assert.match(form.text, /value="Linode Nanode 1GB price"/);
+ assert.match(form.text, /<option value="changed" selected/);
+ const odd = new URLSearchParams({ template: 'json', value_path: 'a b<script>', op: 'drop-table' });
+ const ignored = await svc.get(`/watches/new?${odd}`, { cookie: session });
+ assert.strictEqual(ignored.status, 200);
+ assert.doesNotMatch(ignored.text, /a b<script>|a b<script>/, 'a malformed path is dropped');
+ assert.doesNotMatch(ignored.text, /<option value="drop-table"/, 'an op the template does not have is dropped');
+ assert.match(ignored.text, /<option value="lt" selected/, 'the template default op stays selected');
+});
+
t('the plain create and edit forms save an Actor agent action', async () => {
t('the plain create and edit forms save an Actor agent action', async () => {
const form = await svc.get('/watches/new?template=page', { cookie: session });
const form = await svc.get('/watches/new?template=page', { cookie: session });
assert.match(form.text, /Run one of my agents on OpenVibe\.Actor/);
assert.match(form.text, /Run one of my agents on OpenVibe\.Actor/);