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
11 changes: 11 additions & 0 deletions plugins/profullstack-footer/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"name": "@tsbb/plugin-profullstack-footer",
"version": "0.1.0",
"private": true,
"type": "module",
"main": "./src/index.ts",
"dependencies": {
"@profullstack/footer": "^0.2.6",
"@tsbb/plugin-api": "workspace:*"
}
}
8 changes: 8 additions & 0 deletions plugins/profullstack-footer/plugin.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"slug": "profullstack-footer",
"name": "Profullstack Footer",
"description": "The Profullstack network footer: the << Profullstack >> webring row and the built.indie and pwamart badges. Off by default; for boards run by Profullstack.",
"author": "Profullstack, Inc.",
"homepage": "https://github.com/profullstack/footer",
"license": "MIT"
}
96 changes: 96 additions & 0 deletions plugins/profullstack-footer/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { definePlugin } from '@tsbb/plugin-api';
import { footerHtml, latestTemplate } from '@profullstack/footer';

/**
* The Profullstack network footer (@profullstack/footer), for boards that
* Profullstack runs: the << Profullstack >> webring row, the built.indie and
* pwamart badges and the copyright line, under the board's own footer.
*
* Ships DISABLED. Joining a webring and showing someone else's badges is the
* operator's choice, not something an upgrade should decide for every board.
*
* Two things shape it:
*
* 1. It renders on the server, into the page. The ring's verifier reads the
* HTML and never runs script, so a footer drawn by the browser would never
* count as a ring member.
*
* 2. Its CSS is served from the board, not inlined. The board's policy is
* style-src 'self' with no 'unsafe-inline', which refuses the package's
* inline <style>, so the markup is rendered with css:false and the same CSS
* comes from /p/profullstack-footer/footer.css. Both follow the @latest
* template the package fetches (15 min cache), so a footer release reaches
* the board without a redeploy.
*/

const BADGE_HOSTS = ['https://built.indie.pit.world', 'https://pwamart.com'];

export default definePlugin({
manifest: {
slug: 'profullstack-footer',
name: 'Profullstack Footer',
version: '0.1.0',
description:
'The Profullstack network footer: webring row plus the built.indie and pwamart badges. Off by default; for boards run by Profullstack.',
author: 'Profullstack, Inc.',
homepage: 'https://github.com/profullstack/footer',
license: 'MIT',
defaultEnabled: false,
capabilities: ['render:pages'],
settings: [
{
key: 'site',
label: 'Site homepage',
type: 'string',
default: '',
placeholder: 'https://example.com/',
help:
'The homepage that is the ring member and the badge listing. Leave empty to use the board URL.',
},
{
key: 'since',
label: 'First copyright year',
type: 'number',
default: 0,
help: 'Renders "since–this year" on the copyright line. 0 shows only this year.',
},
],
},

setup(ctx) {
const site = () => String(ctx.settings.get('site') ?? '').trim() || ctx.url('/');

// img-src is normally https: already; name the two badge hosts anyway so a
// board that narrows img-src keeps its badges.
ctx.filter('security:csp', (directives) => {
const img = directives['img-src'];
if (!img || img.includes('https:') || img.includes('*')) return directives;
return { ...directives, 'img-src': [...img, ...BADGE_HOSTS.filter((h) => !img.includes(h))] };
});

ctx.slot('layout:head', () => `<link rel="stylesheet" href="${ctx.url('/p/profullstack-footer/footer.css')}">`);

ctx.slot(
'layout:footer',
async () => {
const since = Number(ctx.settings.get('since') ?? 0);
return footerHtml({ site: site(), css: false, ...(since > 0 ? { since } : {}) });
},
// Last in the footer, below anything another plugin puts there.
1000,
);

ctx.route('GET', '/footer.css', async () => {
const template = await latestTemplate();
return new Response(template.css, {
headers: {
'Content-Type': 'text/css; charset=utf-8',
'Cache-Control': 'public, max-age=900',
'X-Content-Type-Options': 'nosniff',
},
});
});

ctx.log.info(`Profullstack footer for ${site()}`);
},
});
21 changes: 21 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

30 changes: 29 additions & 1 deletion test/plugins.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ process.env.TSBB_DATABASE_URL = `file:${join(scratch, 'board.db')}`;
process.env.TSBB_BASE_URL = 'http://localhost:3998';
process.env.TSBB_SESSION_SECRET = 'test-secret';
process.env.TSBB_MAIL_TRANSPORT = 'console';
// The footer plugin renders the template bundled with @profullstack/footer, never the CDN.
process.env.PROFULLSTACK_FOOTER_OFFLINE = '1';

const { seed } = await import('../packages/db/src/seed.ts');
const { boot } = await import('../apps/server/src/index.ts');
Expand All @@ -31,7 +33,7 @@ describe('the plugin host', () => {
after(() => db.setDb(null));

it('discovers the bundled plugins', () => {
assert.deepEqual([...registry.plugins.keys()].sort(), ['crawlproof-ads', 'hello-world', 'openemoji']);
assert.deepEqual([...registry.plugins.keys()].sort(), ['crawlproof-ads', 'hello-world', 'openemoji', 'profullstack-footer']);
assert.deepEqual([...registry.errors.entries()], [], 'no plugin failed to load');
});

Expand Down Expand Up @@ -105,6 +107,32 @@ describe('the plugin host', () => {
await registry.setEnabled('crawlproof-ads', true);
});

it('draws the Profullstack footer server-side, only once enabled', async () => {
assert.ok(!registry.enabled.has('profullstack-footer'), 'the network footer ships off');
let body = await (await app.fetch(new Request(url('/')))).text();
assert.ok(!body.includes('pfs-footer'), 'a board that never opted in carries no ring');

await registry.setEnabled('profullstack-footer', true);
await db.run("UPDATE plugins SET config = ? WHERE slug = 'profullstack-footer'", [
JSON.stringify({ site: 'https://tsbb.dev/' }),
]);
await registry.reload();
const response = await app.fetch(new Request(url('/')));
body = await response.text();
assert.ok(body.includes('rssamplifier.com/ring/profullstack'), 'the webring row is in the HTML');
assert.ok(body.includes('built.indie.pit.world/for.svg?site=https://tsbb.dev/'), 'built.indie badge');
assert.ok(body.includes('pwamart.com/for.svg'), 'pwamart badge');
assert.ok(!/<style[^>]*>\.pfs-footer/.test(body), 'no inline footer style: style-src has no unsafe-inline');
assert.ok(body.includes('/p/profullstack-footer/footer.css'), 'the CSS comes from the board');

const css = await app.fetch(new Request(url('/p/profullstack-footer/footer.css')));
assert.equal(css.status, 200);
assert.match(css.headers.get('content-type') ?? '', /text\/css/);
assert.ok((await css.text()).includes('.pfs-footer'));

await registry.setEnabled('profullstack-footer', false);
});

it('runs a plugin migration and mounts its routes when enabled', async () => {
await registry.setEnabled('hello-world', true);
assert.ok(registry.enabled.has('hello-world'));
Expand Down
Loading