openvibers/openvibe.mediahub Commits

Video and file pages: Ask your agent chip beside Share (#319)

OP OpenVibers committed 6 days ago · da44d5374e · 5 files, +25 −5

The video page asks about the video's title and public watch address; the file page asks about the file's name, type and size only, never its contents or a link that grants access. Both go through the new askAgent helper, so kit.askAgent escapes everything.

Changes

ddiff --git a/server/http/pages.js b/server/http/pages.js
iindex a4083df..85eb1de 100644
---- a/server/http/pages.js
++++ b/server/http/pages.js
@@@ -39,7 +39,7 @@ const { asyncRouter } = require('./router');
const { createDiscoveryRoutes, homeJsonLd, homeDescription } = require('./discovery');
const { TWIN_PATHS } = require('../drive/agent-pages');
const { sameOrigin } = require('./principal');
--const { html, raw, table, notice, time, badge, shareButton } = require('../render/html');
++const { html, raw, table, notice, time, badge, shareButton, askAgent } = require('../render/html');
const { send } = require('../render/layout');
const rules = require('../drive/rules');
const ui = require('../drive/ui');
@@@ -463,7 +463,7 @@ ${pathBar(path, row.name)}
<div class="fv-side">
<h1 class="fv-name">${row.name}</h1>
<p class="fv-sub"><span class="fv-kind" data-kind="${kind}">${ui.icon(ui.KINDS[kind].icon)}${ui.kindWord(row)}</span><span>${fileSize(row)}</span><span>Added ${ui.when(row.created_at)}</span></p>
--<div class="fv-actions"><a class="dr-btn is-primary" href="/files/${row.id}/download">${ui.icon('download')}Download</a>${raw(shareButton({ url: `${config.baseUrl}/files/${row.id}`, title: row.name, text: `${row.name} — a file on ${SITE_NAME}.`, image: pic ? `${config.baseUrl}/files/${row.id}/preview?w=640` : '', cls: 'dr-btn' }))}</div>
++<div class="fv-actions"><a class="dr-btn is-primary" href="/files/${row.id}/download">${ui.icon('download')}Download</a>${raw(shareButton({ url: `${config.baseUrl}/files/${row.id}`, title: row.name, text: `${row.name} — a file on ${SITE_NAME}.`, image: pic ? `${config.baseUrl}/files/${row.id}/preview?w=640` : '', cls: 'dr-btn' }))}${raw(askAgent({ text: `About my file “${row.name}” (${ui.kindWord(row)}, ${fileSize(row)}): ` }))}</div>
<section class="fv-card" id="share" aria-labelledby="h-share"><h2 id="h-share">${ui.icon('link')}Share a link</h2>
<form class="fv-share" method="post" action="/files/${row.id}/share">
ddiff --git a/server/render/html.js b/server/render/html.js
iindex dfb151d..c911264 100644
---- a/server/render/html.js
++++ b/server/render/html.js
@@@ -44,6 +44,12 @@ function shareButton({ url = '', title = '', text = '', image = '', timeFrom = n
const summary = line.length > 240 ? `${line.slice(0, 237).trimEnd()}…` : line;
return html`<button type="button" class="${cls}" data-ov-share-sheet${url ? html` data-url="${url}"` : ''}${title ? html` data-title="${title}"` : ''}${summary ? html` data-text="${summary}"` : ''}${image ? html` data-image="${image}"` : ''}${timeFrom ? html` data-time-from="${timeFrom}"` : ''}${kind && id ? html` data-kind="${kind}" data-id="${id}"` : ''}><i class="${icon}" aria-hidden="true"></i> ${label}</button>`;
}
++
++/**
++ * "Ask your agent" (openvibe-shared 4.135.0, plan Z3): one chip beside Share. A tap opens the person's Vibe in the
++ * Dock with this page's context waiting in its box, unsent. text is escaped by kit.askAgent.
++ */
++function askAgent({ text = '' }) { return kit.askAgent({ text }); }
const time = (iso) => (iso ? html`<time datetime="${iso}">${String(iso).replace('T', ' ').replace(/\.\d+Z$/, ' UTC').replace(/Z$/, ' UTC')}</time>` : '—');
/** A <table> from headers and rows of cells (cells are already html or plain values). Each cell names its column
@@@ -65,4 +71,4 @@ const notice = (text, kind = 'info') => html`<div class="notice ${kind}" role="s
const csrfField = (token) => html`<input type="hidden" name="csrf" value="${token}">`;
const badge = (text, kind = '') => html`<span class="badge ${kind}">${text}</span>`;
--module.exports = { esc, html, raw, Raw, render, code, time, table, problemBox, notice, csrfField, badge, shareButton };
++module.exports = { esc, html, raw, Raw, render, code, time, table, problemBox, notice, csrfField, badge, shareButton, askAgent };
ddiff --git a/server/video/pages.js b/server/video/pages.js
iindex 166adb9..21343ca 100644
---- a/server/video/pages.js
++++ b/server/video/pages.js
@@@ -29,7 +29,7 @@ const seo = require('openvibe-shared/seo');
const ovServe = require('openvibe-shared/serve');
const { asyncRouter } = require('../http/router');
const { sameOrigin } = require('../http/principal');
--const { html, raw, notice } = require('../render/html');
++const { html, raw, notice, askAgent } = require('../render/html');
// A recording's or clip's AI overview comes from Live, which filters it; read through the same filter here too.
const frame = require('openvibe-shared/frame');
const { send, asset, assetVersion, SITE_NAME } = require('./layout');
@@@ -1375,7 +1375,7 @@ ${channelName ? html`<a class="ovv-avatar" href="${localChannel || liveChannel}"
<div class="ovv-byline-text">${channelName ? (localChannel ? html`<a class="ovv-channel-name" href="${localChannel}">@${channelName}</a>` : html`<span class="ovv-channel-name">@${channelName}</span>`) : ''}
${metaParts.length ? html`<span class="ovv-meta">${metaParts.map((p, i) => (i === 0 ? p : [html` · `, p]))}</span>` : ''}</div>
</div>
--<div class="ovv-actions">${manageable ? editButton('recording') : ''}${saveForm}${ownOnly ? '' : clipAction(req, id, vod)}${downloadMenu({ kind: 'vod', id, item: vod, has: { transcript: segs.length, chat: chatRows ? chatRows.rows.length + chatRows.leftOut : 0, chapters: mems.length } })}${raw(shareButton({ cls: 'ovv-btn', url: `${base}${watchPath}`, title: vod.title || 'Stream recording', text: about, image: httpsUrl(vod.thumbnail_url) || '', timeFrom: '.ovv-player' }))}${moreItems.length ? actionMenu({ icon: 'fa-solid fa-ellipsis', label: 'More', iconOnly: true, cls: 'ovv-more-menu', body: menuLinks(moreItems) }) : ''}</div>
++<div class="ovv-actions">${manageable ? editButton('recording') : ''}${saveForm}${ownOnly ? '' : clipAction(req, id, vod)}${downloadMenu({ kind: 'vod', id, item: vod, has: { transcript: segs.length, chat: chatRows ? chatRows.rows.length + chatRows.leftOut : 0, chapters: mems.length } })}${raw(shareButton({ cls: 'ovv-btn', url: `${base}${watchPath}`, title: vod.title || 'Stream recording', text: about, image: httpsUrl(vod.thumbnail_url) || '', timeFrom: '.ovv-player' }))}${raw(askAgent({ text: `About the video “${vod.title || 'Stream recording'}” on OpenVibe.Video (${base}${watchPath}): ` }))}${moreItems.length ? actionMenu({ icon: 'fa-solid fa-ellipsis', label: 'More', iconOnly: true, cls: 'ovv-more-menu', body: menuLinks(moreItems) }) : ''}</div>
</div>
${vod.is_recording ? html`<p class="ovv-recording" role="status"><i class="fa-solid fa-circle" aria-hidden="true"></i> This recording is still in progress. <a href="${liveChannel}">Watch the live channel</a>.</p>` : ''}
</div>
ddiff --git a/test/drive-pages.test.js b/test/drive-pages.test.js
iindex 09a0e2c..acdbd63 100644
---- a/test/drive-pages.test.js
++++ b/test/drive-pages.test.js
@@@ -115,6 +115,15 @@ const { SAME, bytes, upload, share } = require('./helpers/drive');
assert.ok(r.text.includes('&lt;script&gt;') || r.text.includes('&lt;img src=x'), `${where} does not show it at all`);
}
++ // The file page's Ask your agent chip (openvibe-shared 4.135.0): the typed name is escaped, and no link is in it.
++ const filePage = await t.get(`/files/${file.id}`, { as: kim });
++ const asks = filePage.text.match(/<button[^>]*data-ov-ask[^>]*>/g) || [];
++ assert.strictEqual(asks.length, 1, 'one Ask your agent chip: ' + asks.join(' | '));
++ assert.ok(asks[0].includes('data-ov-ask="About my file “&lt;script&gt;alert(&quot;x&quot;)') && asks[0].includes('” (Document, 50 B): "'), asks[0]);
++ assert.ok(!asks[0].includes('<script>'), 'the file name is escaped in the chip');
++ assert.ok(!asks[0].includes('http') && !asks[0].includes('/files/'), 'no link to the file in the chip');
++ assert.ok(asks[0].includes('class="ovk-ask ovk-btn'), 'the shared kit chip');
++
// A report note is text somebody typed, and it is escaped where staff read it.
const queue = await t.get('/staff', { as: t.network.addUser('mod', { role: 'global_mod' }) });
assert.ok(queue.text.includes('&lt;b&gt;not bold&lt;/b&gt;'), 'the note is escaped in the queue');
ddiff --git a/test/video-pages.test.js b/test/video-pages.test.js
iindex 2d6da54..738eaa9 100644
---- a/test/video-pages.test.js
++++ b/test/video-pages.test.js
@@@ -379,6 +379,11 @@ function countOccurrences(haystack, needle) {
assert.ok(r.text.includes('Discuss on OpenVibe.Community'));
assert.ok(r.text.includes('data-ovv-chapter'));
assert.ok(r.text.includes('data-ovv-seek'));
++ // The Ask your agent chip (openvibe-shared 4.135.0): beside Share, the video's title and public address.
++ const asks = r.text.match(/<button[^>]*data-ov-ask[^>]*>/g) || [];
++ assert.strictEqual(asks.length, 1, 'one Ask your agent chip: ' + asks.join(' | '));
++ assert.ok(asks[0].includes('data-ov-ask="About the video “A public recording” on OpenVibe.Video (https://openvibe.video/v/101): "'), asks[0]);
++ assert.ok(asks[0].includes('class="ovk-ask ovk-btn'), 'the shared kit chip');
});
await check('the clip watch page attributes its maker and links the full recording', async () => {