import * as vm from 'vm'; import { FoamWorkspace } from '../model/workspace'; import { FoamGraph } from '../model/graph'; import { Logger } from '../utils/log'; import { URI } from '../model/uri'; import { QueryFilter, QueryResult, SourceReader, requiresSource } from '.'; import { toSlug } from '../utils/slug'; import dayjs from 'dayjs'; import { escapeHtml, renderList, renderResults, MarkdownRenderer, QueryRender, ToHref, } from './html'; import { RenderContext } from './render-context'; const EXECUTION_TIMEOUT = 10_000; const JS_PLACEHOLDER = `

Use \`\`\`foam-query-js blocks to write a script to query notes. For example:

\`\`\`foam-query-js
render(foam.pages('#my-tag').sortBy('title').format('list'));
\`\`\`

Read the full documentation here

`; export interface RenderJsQueryOptions { workspace: FoamWorkspace; graph: FoamGraph; trusted: boolean; toHref: ToHref; currentResource?: URI | null; readSource?: SourceReader; renderMarkdown?: MarkdownRenderer; context?: RenderContext; } export function renderJsQuery( code: string, opts: RenderJsQueryOptions ): QueryRender { const { workspace, graph, trusted, toHref, currentResource, readSource, renderMarkdown, context, } = opts; // JS scripts can call `render()` zero or more times with arbitrary values, // so the output is always an opaque concatenation as far as the consumer const opaque = (html: string): QueryRender => ({ html, shape: 'unknown' }); if (code.trim() === '') { return opaque(JS_PLACEHOLDER); } if (!trusted) { return opaque( `
foam-query-js requires a trusted workspace. Manage trust
` ); } const htmlParts: string[] = []; const renderQueryResult = (qr: QueryResult): string => { const desc = qr.descriptor; const format = desc.format ?? (desc.select && desc.select.length > 1 ? 'table' : 'list'); if (format === 'list') { const listFields = desc.select ?? [{ field: 'title', label: 'title' }]; return renderList( qr.select(listFields).toArray(), listFields, toHref, renderMarkdown, context ).html; } return renderResults( qr.toArray(), desc, toHref, renderMarkdown, context ).html; }; const render = (value: QueryResult | string | undefined | null) => { if (value instanceof QueryResult) { htmlParts.push(renderQueryResult(value)); } else if (value !== undefined && value !== null) { htmlParts.push(`

${escapeHtml(String(value))}

`); } }; // Globals and helpers exposed to the query script. This is the query API // surface, NOT a security sandbox — see the runInContext note below. // Only non-native values are injected: the ECMAScript built-ins (Object, // Array, Date, JSON, ...) already exist in every vm realm, and injecting host // copies would break in-context `instanceof`. const sandbox: Record = { console: { log: (...args: unknown[]) => Logger.info(`[foam-query-js] ${args[0]}`, ...args.slice(1)), warn: (...args: unknown[]) => Logger.warn(`[foam-query-js] ${args[0]}`, ...args.slice(1)), error: (...args: unknown[]) => Logger.error(`[foam-query-js] ${args[0]}`, ...args.slice(1)), }, dayjs, slugify: toSlug, URI, render, foam: { pages: (filter?: QueryFilter | string) => new QueryResult( workspace, graph, trusted, filter as QueryFilter, readSource ), current: null as URI | null, }, }; try { const context = vm.createContext(sandbox); // Reconstruct foam.current inside the VM context so it is a proper URI // instance from the context's perspective (instanceof checks do not // work correctly across contexts, so we recreate it). if (currentResource) { new vm.Script( `foam.current = new URI({ scheme: ${JSON.stringify( currentResource.scheme )}, authority: ${JSON.stringify( currentResource.authority )}, path: ${JSON.stringify(currentResource.path)} });` ).runInContext(context); } // SECURITY: `vm` is not a sandbox; this runs user code with full RCE // potential. The `if (!trusted)` guard at the top of this function is the // only boundary — this must never be reached for an untrusted workspace. new vm.Script(code).runInContext(context, { timeout: EXECUTION_TIMEOUT }); } catch (e) { return opaque( `
Script error: ${escapeHtml( String(e) )}
` ); } return opaque( htmlParts.join('\n') || '

No output. Did you forget to call render()?

' ); }