import assert from 'node:assert/strict'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; import vue from '@vitejs/plugin-vue'; import tailwind from '@tailwindcss/vite'; import { createServer } from 'vite'; import { createSSRApp, h } from 'vue'; import { renderToString } from 'vue/server-renderer'; const serve = process.argv.includes('--serve'); const server = await createServer({ configFile: false, plugins: [vue(), ...(serve ? [tailwind()] : [])], resolve: { alias: { '@': fileURLToPath(new URL('../../resources/js', import.meta.url)), }, }, server: serve ? { host: '127.0.0.1', port: 4186, strictPort: true, hmr: false } : { middlewareMode: true, hmr: false, watch: null }, appType: 'custom', }); if (serve) { server.middlewares.use(async (request, response) => { response.setHeader('Content-Type', 'text/html'); response.end( await server.transformIndexHtml( request.url, '
', ), ); }); await server.listen(); console.log('Description fixture ready on port 4186'); } else { try { const { safeDescriptionLink, supportsVisualDescription, parseDescription, renderDescriptionNode, } = await server.ssrLoadModule( '/resources/js/lib/ticket-description.ts', ); const { default: Description } = await server.ssrLoadModule( '/resources/js/components/tickets/TicketDescription.vue', ); const { default: Editor } = await server.ssrLoadModule( '/resources/js/components/tickets/TicketDescriptionEditor.vue', ); const { createLocalization } = await server.ssrLoadModule( '/resources/js/lib/localization.ts', ); const render = (description) => renderToString( createSSRApp({ render: () => h(Description, { description }) }), ); const markdown = '# Title\n\n**Bold** and *italic* and ~~strike~~\n\n- One\n - Nested\n\n3. Third\n4. Fourth\n\n> Quote\n\n`inline`\n\n```js\n', 'inline', '<tag>', 'rel="noopener noreferrer"', '
', ]) { assert.ok( rendered.includes(snippet), `Missing formatted output: ${snippet}`, ); } assert.match(await render('First\nSecond'), /First
Second/); assert.match( await render('München 🚀 & details'), /München 🚀 & details/, ); assert.match( await render('<img src=x onerror=alert(1)>'), /<img/, ); for (const value of [ 'https://example.test/a?b=1&c=2', 'http://example.test', 'mailto:test@example.test', '/projects/1', '#section', ]) { assert.equal(safeDescriptionLink(value), value); } for (const value of [ 'javascript:alert(1)', 'JaVaScRiPt:alert(1)', 'data:text/html,test', 'vbscript:test', '//evil.test', '/\\evil.test', '\u0000https://example.test', 'https://', 'https://exa mple.test', 'java\nscript:alert(1)', null, ]) { assert.equal(safeDescriptionLink(value), null); } for (const value of [ '', '', '| A | B |\n| - | - |\n| a | b |', '', '- [x] Done', '[unsafe](javascript:alert%281%29)', ]) { assert.equal(supportsVisualDescription(value), false, value); const html = await render(value); assert.doesNotMatch(html, /', marks: [ { type: 'link', attrs: { href: 'javascript:bad()' } }, ], }, ], }; const safe = await renderToString( createSSRApp({ render: () => h('div', {}, [renderDescriptionNode(maliciousNode)]), }), ); assert.doesNotMatch(safe, /