diff --git a/packages/cli/src/ui/utils/exportUtils.test.ts b/packages/cli/src/ui/utils/exportUtils.test.ts index 8a8fcb046..b58b5a73e 100644 --- a/packages/cli/src/ui/utils/exportUtils.test.ts +++ b/packages/cli/src/ui/utils/exportUtils.test.ts @@ -4,7 +4,7 @@ * SPDX-License-Identifier: Apache-2.0 */ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; +import { describe, it, expect } from 'vitest'; import { extractTextFromContent, transformToMarkdown, @@ -256,51 +256,12 @@ describe('exportUtils', () => { }); describe('loadHtmlTemplate', () => { - beforeEach(() => { - vi.stubGlobal('fetch', vi.fn()); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - }); - - it('should load HTML template from URL', async () => { - const mockTemplate = 'Test Template'; - const mockResponse = { - ok: true, - text: vi.fn().mockResolvedValue(mockTemplate), - }; - vi.mocked(fetch).mockResolvedValue(mockResponse as unknown as Response); - + it('should load HTML template from bundled constant', async () => { const result = await loadHtmlTemplate(); - expect(result).toBe(mockTemplate); - expect(fetch).toHaveBeenCalledWith( - 'https://raw.githubusercontent.com/QwenLM/qwen-code/main/template_portable.html', - ); - }); - - it('should throw error when fetch fails', async () => { - const mockResponse = { - ok: false, - status: 404, - statusText: 'Not Found', - }; - vi.mocked(fetch).mockResolvedValue(mockResponse as unknown as Response); - - await expect(loadHtmlTemplate()).rejects.toThrow( - 'Failed to fetch HTML template: 404 Not Found', - ); - }); - - it('should throw error when network request fails', async () => { - const networkError = new Error('Network error'); - vi.mocked(fetch).mockRejectedValue(networkError); - - await expect(loadHtmlTemplate()).rejects.toThrow( - 'Failed to load HTML template', - ); - await expect(loadHtmlTemplate()).rejects.toThrow('Network error'); + expect(result).toContain(''); + expect(result).toContain('Qwen Code Chat Export'); + expect(result).toContain('id="chat-data"'); }); }); @@ -371,6 +332,42 @@ describe('exportUtils', () => { expect(result).toContain('"sessionId": "test"'); expect(result).not.toContain('DATA_PLACEHOLDER'); }); + + it('should escape unsafe JSON sequences in HTML', () => { + const template = ` + + + + + + `; + + const data = { + sessionId: 'test-session-id', + startTime: '2025-01-01T00:00:00Z', + messages: [ + { + uuid: 'uuid-1', + parentUuid: null, + sessionId: 'test-session-id', + timestamp: '2025-01-01T00:00:00Z', + type: 'user', + cwd: '/test', + version: '1.0.0', + message: { + parts: [{ text: '
unsafe
' }] as Part[], + } as Content, + }, + ] as ChatRecord[], + }; + + const result = injectDataIntoHtmlTemplate(template, data); + + expect(result).toContain('\\u003c/script'); + expect(result).not.toContain('
unsafe
'); + }); }); describe('generateExportFilename', () => { diff --git a/packages/cli/src/ui/utils/exportUtils.ts b/packages/cli/src/ui/utils/exportUtils.ts index 165e55996..8e3d53ba4 100644 --- a/packages/cli/src/ui/utils/exportUtils.ts +++ b/packages/cli/src/ui/utils/exportUtils.ts @@ -7,8 +7,205 @@ import type { Part, Content } from '@google/genai'; import type { ChatRecord } from '@qwen-code/qwen-code-core'; -const HTML_TEMPLATE_URL = - 'https://raw.githubusercontent.com/QwenLM/qwen-code/main/template_portable.html'; +const HTML_TEMPLATE = ` + + + + + + Qwen Code Chat Export + + + + + + + + + + + + + + + + + +
+
+

Qwen Code Chat Export

+
+
+ Session ID: + - +
+
+ Date: + - +
+
+
+ +
+
+ + + + + + + +`; + +function escapeJsonForHtml(json: string): string { + return json + .replace(/&/g, '\\u0026') + .replace(//g, '\\u003e'); +} /** * Extracts text content from a Content object's parts. @@ -98,26 +295,10 @@ export function transformToMarkdown( } /** - * Loads the HTML template from a remote URL via fetch. - * Throws an error if the fetch fails. + * Loads the HTML template from a bundled string constant. */ export async function loadHtmlTemplate(): Promise { - try { - const response = await fetch(HTML_TEMPLATE_URL); - if (!response.ok) { - throw new Error( - `Failed to fetch HTML template: ${response.status} ${response.statusText}`, - ); - } - const template = await response.text(); - return template; - } catch (error) { - throw new Error( - `Failed to load HTML template from ${HTML_TEMPLATE_URL}: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } + return HTML_TEMPLATE; } /** @@ -151,9 +332,10 @@ export function injectDataIntoHtmlTemplate( }, ): string { const jsonData = JSON.stringify(data, null, 2); + const escapedJsonData = escapeJsonForHtml(jsonData); const html = template.replace( /`, + ``, ); return html; }