mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-05 18:20:39 -08:00
Fix: security errors (#648)
* refactor: outdated usage
* fix: use JSON.stringify to prevent '`' break the string to execute any script
* fix: use json5 parse LLM tool calling
* fix Potential file system race condition
* fix Indirect uncontrolled command line
* Update callImageGenerationAPI.ts
* While the shell path is now validated, arguments_.join(' ') could still contain shell metacharacters that enable command injection. The arguments array should be validated or passed in a way that prevents shell interpretation, such as using the array form of execSync instead of string interpolation.
* directly Remove TiddlyWiki special characters that could cause parsing issues
* prevent race condition
* fix: subscribe to latest pauseNotification
* Return a lightweight mock notification object to avoid creating real browser notifications
* Update responsePatternUtility.ts
This commit is contained in:
parent
69cc703b18
commit
ea78df0ab3
10 changed files with 797 additions and 126 deletions
253
src/__tests__/security/injection-prevention.test.ts
Normal file
253
src/__tests__/security/injection-prevention.test.ts
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
|
||||
/**
|
||||
* Security tests for injection prevention
|
||||
* These tests verify that the fixes for the RCE vulnerability are effective
|
||||
*/
|
||||
describe('Injection Prevention', () => {
|
||||
describe('JSON.stringify escaping', () => {
|
||||
it('should wrap input in quotes, making backticks safe', () => {
|
||||
const maliciousInput = '`+alert(1)+`';
|
||||
const escaped = JSON.stringify(maliciousInput);
|
||||
// JSON.stringify returns a quoted string: "`+alert(1)+`"
|
||||
// When used in code like: let x = ${escaped};
|
||||
// It becomes: let x = "`+alert(1)+`";
|
||||
// The backticks are inside quotes, so they're treated as regular characters
|
||||
expect(escaped).toBe('"`+alert(1)+`"');
|
||||
expect(escaped.startsWith('"')).toBe(true);
|
||||
expect(escaped.endsWith('"')).toBe(true);
|
||||
});
|
||||
|
||||
it('should escape template literal syntax safely', () => {
|
||||
const maliciousInput = '${process.binding("spawn_sync")}';
|
||||
const escaped = JSON.stringify(maliciousInput);
|
||||
expect(escaped).toBe('"${process.binding(\\"spawn_sync\\")}"');
|
||||
// Verify it's wrapped in quotes and inner quotes are escaped
|
||||
expect(escaped.startsWith('"')).toBe(true);
|
||||
expect(escaped.endsWith('"')).toBe(true);
|
||||
});
|
||||
|
||||
it('should handle newlines safely', () => {
|
||||
const maliciousInput = '`\nalert(1)\n`';
|
||||
const escaped = JSON.stringify(maliciousInput);
|
||||
// Newlines are escaped as \n in the JSON string
|
||||
expect(escaped).toContain('\\n');
|
||||
expect(escaped).toBe('"`\\nalert(1)\\n`"');
|
||||
});
|
||||
|
||||
it('should escape quotes', () => {
|
||||
const maliciousInput = '"; alert(1); "';
|
||||
const escaped = JSON.stringify(maliciousInput);
|
||||
// JSON.stringify escapes the quotes
|
||||
expect(escaped).toContain('\\"');
|
||||
expect(escaped.startsWith('"')).toBe(true);
|
||||
expect(escaped.endsWith('"')).toBe(true);
|
||||
});
|
||||
|
||||
it('should preserve CJK characters', () => {
|
||||
const cjkInput = '新条目';
|
||||
const escaped = JSON.stringify(cjkInput);
|
||||
expect(escaped).toBe('"新条目"');
|
||||
});
|
||||
|
||||
it('should preserve Unicode characters', () => {
|
||||
const unicodeInput = 'Tëst Ñame 日本語 한글';
|
||||
const escaped = JSON.stringify(unicodeInput);
|
||||
expect(escaped).toContain('Tëst');
|
||||
expect(escaped).toContain('日本語');
|
||||
expect(escaped).toContain('한글');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Script generation safety', () => {
|
||||
it('should prevent template literal breakout in openTiddler', () => {
|
||||
const maliciousName = '`+void window.service.wiki.wikiOperationInServer()+`';
|
||||
|
||||
// Simulate the fixed version
|
||||
const script = `
|
||||
let trimmedTiddlerName = ${JSON.stringify(maliciousName)};
|
||||
let currentHandlerWidget = $tw.rootWidget;
|
||||
`;
|
||||
|
||||
// The malicious code should be trapped inside a quoted string
|
||||
// It becomes: let trimmedTiddlerName = "`+void...+`";
|
||||
// The backticks are inside quotes, so they're just characters
|
||||
expect(script).toContain('"`+void window.service');
|
||||
expect(script).not.toContain('void window.service.wiki.wikiOperationInServer()+`;\n');
|
||||
});
|
||||
|
||||
it('should prevent injection in setTiddlerText', () => {
|
||||
const maliciousTitle = 'dummy`),process.binding("spawn_sync"),console.log(1);//';
|
||||
const maliciousValue = 'New content';
|
||||
|
||||
// Simulate the fixed version
|
||||
const script = `return $tw.wiki.setText(${JSON.stringify(maliciousTitle)}, 'text', undefined, ${JSON.stringify(maliciousValue)});`;
|
||||
|
||||
// The script should have quoted strings, not executable code
|
||||
expect(script).toContain('"dummy`),process.binding');
|
||||
expect(script).toContain('"New content"');
|
||||
});
|
||||
|
||||
it('should prevent injection in renderWikiText', () => {
|
||||
const maliciousContent = '`+alert(1)+`';
|
||||
|
||||
// Simulate the fixed version
|
||||
const script = `return $tw.wiki.renderText("text/html", "text/vnd.tiddlywiki", ${JSON.stringify(maliciousContent)});`;
|
||||
|
||||
// alert(1) should be inside a quoted string
|
||||
expect(script).toContain('"`+alert(1)+`"');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Real-world attack vectors', () => {
|
||||
it('should block the reported PoC payload', () => {
|
||||
const pocPayload =
|
||||
"dummy`),process.binding('spawn_sync').spawn({file:'/usr/bin/open',args:['/usr/bin/open','/System/Applications/Calculator.app/Contents/MacOS/Calculator'],stdio:[{type:'pipe',readable:!0},{type:'pipe',writable:!0},{type:'pipe',writable:!0}]}),console.log(1);//";
|
||||
|
||||
const escaped = JSON.stringify(pocPayload);
|
||||
|
||||
// Should be wrapped in quotes, making the backticks safe
|
||||
expect(escaped.startsWith('"')).toBe(true);
|
||||
expect(escaped.endsWith('"')).toBe(true);
|
||||
// The payload is now a string literal, not executable code
|
||||
expect(escaped).toContain('"dummy`),process.binding');
|
||||
});
|
||||
|
||||
it('should block XSS in renderer via openTiddler', () => {
|
||||
const xssPayload = "`+alert('XSS')+`";
|
||||
|
||||
const script = `let trimmedTiddlerName = ${JSON.stringify(xssPayload)};`;
|
||||
|
||||
// alert('XSS') should be inside a quoted string
|
||||
expect(script).toContain('"`+alert(\'XSS\')+`"');
|
||||
});
|
||||
|
||||
it('should block various injection techniques', () => {
|
||||
const payloads = [
|
||||
'`+alert(1)+`',
|
||||
'${alert(1)}',
|
||||
'\\`+alert(1)+\\`',
|
||||
"'; alert(1); '",
|
||||
'"); alert(1); ("',
|
||||
'`; alert(1); `',
|
||||
'`\\nalert(1)\\n`',
|
||||
];
|
||||
|
||||
for (const payload of payloads) {
|
||||
const escaped = JSON.stringify(payload);
|
||||
// All should be wrapped in quotes, making them safe string literals
|
||||
expect(escaped.startsWith('"')).toBe(true);
|
||||
expect(escaped.endsWith('"')).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Deep link sanitization', () => {
|
||||
/**
|
||||
* Simulates the sanitizeTiddlerName method from DeepLinkService
|
||||
*/
|
||||
function sanitizeTiddlerName(tiddlerName: string): string {
|
||||
let sanitized = tiddlerName;
|
||||
|
||||
// Remove backticks and template literal syntax
|
||||
sanitized = sanitized.replace(/[`${}]/g, '');
|
||||
|
||||
// Remove HTML tags and script tags
|
||||
sanitized = sanitized.replace(/<\/?[^>]+(>|$)/g, '');
|
||||
|
||||
// Remove newlines and other control characters
|
||||
sanitized = sanitized.replace(/[\r\n\t]/g, ' ');
|
||||
|
||||
// Remove null bytes
|
||||
sanitized = sanitized.replace(/\0/g, '');
|
||||
|
||||
// Trim whitespace
|
||||
sanitized = sanitized.trim();
|
||||
|
||||
// Limit length to prevent DoS
|
||||
if (sanitized.length > 1000) {
|
||||
sanitized = sanitized.substring(0, 1000);
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
it('should remove backticks', () => {
|
||||
const input = '`malicious`';
|
||||
const sanitized = sanitizeTiddlerName(input);
|
||||
expect(sanitized).toBe('malicious');
|
||||
expect(sanitized).not.toContain('`');
|
||||
});
|
||||
|
||||
it('should remove template literal syntax', () => {
|
||||
const input = '${process.binding()}';
|
||||
const sanitized = sanitizeTiddlerName(input);
|
||||
expect(sanitized).toBe('process.binding()');
|
||||
expect(sanitized).not.toContain('$');
|
||||
expect(sanitized).not.toContain('{');
|
||||
expect(sanitized).not.toContain('}');
|
||||
});
|
||||
|
||||
it('should remove HTML tags', () => {
|
||||
const input = '<script>alert(1)</script>';
|
||||
const sanitized = sanitizeTiddlerName(input);
|
||||
expect(sanitized).toBe('alert(1)');
|
||||
expect(sanitized).not.toContain('<script>');
|
||||
});
|
||||
|
||||
it('should remove control characters', () => {
|
||||
const input = 'line1\nline2\ttab';
|
||||
const sanitized = sanitizeTiddlerName(input);
|
||||
// Control characters should be replaced with spaces
|
||||
expect(sanitized).toBe('line1 line2 tab');
|
||||
});
|
||||
|
||||
it('should preserve normal tiddler names', () => {
|
||||
const normalNames = [
|
||||
'MyTiddler',
|
||||
'My Tiddler',
|
||||
'My-Tiddler',
|
||||
'My_Tiddler',
|
||||
'Tiddler 123',
|
||||
];
|
||||
|
||||
for (const name of normalNames) {
|
||||
const sanitized = sanitizeTiddlerName(name);
|
||||
expect(sanitized).toBe(name);
|
||||
}
|
||||
});
|
||||
|
||||
it('should preserve CJK characters', () => {
|
||||
const cjkNames = [
|
||||
'新条目',
|
||||
'日本語タイトル',
|
||||
'한글 제목',
|
||||
'中文标题',
|
||||
];
|
||||
|
||||
for (const name of cjkNames) {
|
||||
const sanitized = sanitizeTiddlerName(name);
|
||||
expect(sanitized).toBe(name);
|
||||
}
|
||||
});
|
||||
|
||||
it('should limit length to prevent DoS', () => {
|
||||
const longInput = 'a'.repeat(2000);
|
||||
const sanitized = sanitizeTiddlerName(longInput);
|
||||
expect(sanitized.length).toBe(1000);
|
||||
});
|
||||
|
||||
it('should block the PoC attack', () => {
|
||||
const pocInput = '`+void window.service.wiki.wikiOperationInServer()';
|
||||
const sanitized = sanitizeTiddlerName(pocInput);
|
||||
|
||||
// Backticks, dollar signs, and braces should be removed
|
||||
expect(sanitized).not.toContain('`');
|
||||
expect(sanitized).not.toContain('$');
|
||||
expect(sanitized).not.toContain('{');
|
||||
expect(sanitized).not.toContain('}');
|
||||
// But the text content remains (though harmless)
|
||||
expect(sanitized).toBe('+void window.service.wiki.wikiOperationInServer()');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,10 +1,8 @@
|
|||
import { Channels, WorkspaceChannel } from '@/constants/channels';
|
||||
import { webFrame } from 'electron';
|
||||
import '../services/wiki/wikiOperations/executor/wikiOperationInBrowser';
|
||||
import type { IPossibleWindowMeta, WindowMeta } from '@services/windows/WindowProperties';
|
||||
import { WindowNames } from '@services/windows/WindowProperties';
|
||||
import { browserViewMetaData, windowName } from './common/browserViewMetaData';
|
||||
import { menu, preference, workspace, workspaceView } from './common/services';
|
||||
|
||||
let handled = false;
|
||||
const handleLoaded = (event: string): void => {
|
||||
|
|
@ -20,11 +18,6 @@ const handleLoaded = (event: string): void => {
|
|||
};
|
||||
|
||||
async function executeJavaScriptInBrowserView(): Promise<void> {
|
||||
// Fix Can't show file list of Google Drive
|
||||
// https://github.com/electron/electron/issues/16587
|
||||
// Fix chrome.runtime.sendMessage is undefined for FastMail
|
||||
// https://github.com/atomery/singlebox/issues/21
|
||||
const initialShouldPauseNotifications = await preference.get('pauseNotifications');
|
||||
const { workspaceID } = browserViewMetaData as IPossibleWindowMeta<WindowMeta[WindowNames.view]>;
|
||||
|
||||
try {
|
||||
|
|
@ -32,22 +25,55 @@ async function executeJavaScriptInBrowserView(): Promise<void> {
|
|||
(function() {
|
||||
// Customize Notification behavior
|
||||
// https://stackoverflow.com/questions/53390156/how-to-override-javascript-web-api-notification-object
|
||||
// TODO: fix logic here, get latest pauseNotifications from preference, and focusWorkspace
|
||||
const oldNotification = window.Notification;
|
||||
|
||||
// Cache pause status and keep it in sync with preferences observable
|
||||
let pauseNotifications = false;
|
||||
window.observables?.preference?.preference$?.subscribe?.((preferences) => {
|
||||
pauseNotifications = preferences?.pauseNotifications || false;
|
||||
});
|
||||
|
||||
let shouldPauseNotifications = ${
|
||||
typeof initialShouldPauseNotifications === 'string' && initialShouldPauseNotifications.length > 0 ? `"${initialShouldPauseNotifications}"` : 'undefined'
|
||||
};
|
||||
|
||||
window.Notification = function() {
|
||||
if (!shouldPauseNotifications) {
|
||||
const notification = new oldNotification(...arguments);
|
||||
notification.addEventListener('click', () => {
|
||||
window.postMessage({ type: '${WorkspaceChannel.focusWorkspace}', workspaceID: "${workspaceID ?? '-'}" });
|
||||
});
|
||||
return notification;
|
||||
// Use modern rest parameters instead of arguments for better performance and clarity
|
||||
window.Notification = function(...args) {
|
||||
// Check cached value first to avoid notification flash
|
||||
if (pauseNotifications) {
|
||||
// Return a lightweight mock notification object to avoid creating real browser notifications
|
||||
// This mock implements the minimal Notification API that calling code might expect
|
||||
return {
|
||||
close: () => {},
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => true,
|
||||
// Common Notification properties that might be accessed
|
||||
title: args[0] || '',
|
||||
body: (args[1] && args[1].body) || '',
|
||||
tag: 'paused',
|
||||
// Prevent any actual notification behavior
|
||||
onclick: null,
|
||||
onclose: null,
|
||||
onerror: null,
|
||||
onshow: null
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
||||
// Create and show notification
|
||||
const notification = new oldNotification(...args);
|
||||
|
||||
// Add click handler to focus workspace
|
||||
notification.addEventListener('click', async () => {
|
||||
const workspaceID = ${JSON.stringify(workspaceID ?? '-')};
|
||||
try {
|
||||
const targetWorkspace = await window.service.workspace.get(workspaceID);
|
||||
if (targetWorkspace !== undefined) {
|
||||
await window.service.workspaceView.setActiveWorkspaceView(workspaceID);
|
||||
await window.service.menu.buildMenu();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to handle notification click:', error);
|
||||
}
|
||||
});
|
||||
|
||||
return notification;
|
||||
}
|
||||
window.Notification.requestPermission = oldNotification.requestPermission;
|
||||
Object.defineProperty(Notification, 'permission', {
|
||||
|
|
@ -73,14 +99,4 @@ if (windowName === WindowNames.view) {
|
|||
window.addEventListener('load', () => {
|
||||
handleLoaded('window.on("onload")');
|
||||
});
|
||||
window.addEventListener('message', async (event?: MessageEvent<{ type?: Channels; workspaceID?: string } | undefined>) => {
|
||||
// set workspace to active when its notification is clicked
|
||||
if (event?.data?.type === WorkspaceChannel.focusWorkspace) {
|
||||
const id = event.data.workspaceID;
|
||||
if (id !== undefined && (await workspace.get(id)) !== undefined) {
|
||||
await workspaceView.setActiveWorkspaceView(id);
|
||||
await menu.buildMenu();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,290 @@
|
|||
import { describe, expect, it } from 'vitest';
|
||||
import { matchToolCalling } from '../responsePatternUtility';
|
||||
|
||||
/**
|
||||
* Security tests for agent response parsing
|
||||
* These tests verify that malicious AI responses cannot execute arbitrary code
|
||||
*/
|
||||
describe('Agent Response Parsing Security', () => {
|
||||
describe('Dangerous pattern detection', () => {
|
||||
it('should reject parameters with require() calls', () => {
|
||||
const responseText = `
|
||||
<tool_use name="malicious-tool">
|
||||
{
|
||||
test: require('child_process').execSync('whoami')
|
||||
}
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
// The dangerous code should not be executed, fallback to string
|
||||
expect(result.parameters).toEqual({
|
||||
input: expect.stringContaining('require'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject parameters with process.binding', () => {
|
||||
const responseText = `
|
||||
<tool_use name="malicious-tool">
|
||||
{
|
||||
exploit: process.binding('spawn_sync').spawn({file:'/usr/bin/whoami'})
|
||||
}
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.parameters).toEqual({
|
||||
input: expect.stringContaining('process.binding'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject parameters with eval()', () => {
|
||||
const responseText = `
|
||||
<tool_use name="malicious-tool">
|
||||
{
|
||||
code: eval('malicious code here')
|
||||
}
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.parameters).toEqual({
|
||||
input: expect.stringContaining('eval'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject parameters with Function constructor', () => {
|
||||
const responseText = `
|
||||
<tool_use name="malicious-tool">
|
||||
{
|
||||
fn: new Function('return process')()
|
||||
}
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.parameters).toEqual({
|
||||
input: expect.stringContaining('Function'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject parameters with constructor access', () => {
|
||||
const responseText = `
|
||||
<tool_use name="malicious-tool">
|
||||
{
|
||||
hack: ({}).__proto__.constructor('return process')()
|
||||
}
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.parameters).toEqual({
|
||||
input: expect.stringContaining('constructor'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject parameters with global object access', () => {
|
||||
const responseText = `
|
||||
<tool_use name="malicious-tool">
|
||||
{
|
||||
test: global.process.exit(1)
|
||||
}
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.parameters).toEqual({
|
||||
input: expect.stringContaining('global'),
|
||||
});
|
||||
});
|
||||
|
||||
it('should reject parameters with __dirname or __filename', () => {
|
||||
const responseText = `
|
||||
<tool_use name="malicious-tool">
|
||||
{
|
||||
path: __dirname + '/sensitive-file.txt'
|
||||
}
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.parameters).toEqual({
|
||||
input: expect.stringContaining('__dirname'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Length limits', () => {
|
||||
it('should handle very long parameter strings safely', () => {
|
||||
const longString = 'a'.repeat(15000);
|
||||
const responseText = `
|
||||
<tool_use name="test-tool">
|
||||
{
|
||||
data: "${longString}"
|
||||
}
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
// Should be parsed as JSON (safe path)
|
||||
if (result.parameters?.data) {
|
||||
expect(typeof result.parameters.data).toBe('string');
|
||||
}
|
||||
});
|
||||
|
||||
it('should truncate fallback input to prevent DoS', () => {
|
||||
const longNonJson = 'not json '.repeat(200);
|
||||
const responseText = `
|
||||
<tool_use name="test-tool">
|
||||
${longNonJson}
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
if (result.parameters?.input && typeof result.parameters.input === 'string') {
|
||||
expect(result.parameters.input.length).toBeLessThanOrEqual(1000);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('Safe parsing paths', () => {
|
||||
it('should safely parse valid JSON', () => {
|
||||
const responseText = `
|
||||
<tool_use name="safe-tool">
|
||||
{
|
||||
"name": "test",
|
||||
"value": 123,
|
||||
"nested": {"key": "value"}
|
||||
}
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.parameters).toEqual({
|
||||
name: 'test',
|
||||
value: 123,
|
||||
nested: { key: 'value' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should safely parse JavaScript object literals via regex conversion', () => {
|
||||
const responseText = `
|
||||
<tool_use name="safe-tool">
|
||||
{ key: "value", number: 42 }
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.parameters?.key).toBe('value');
|
||||
expect(result.parameters?.number).toBe(42);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Prompt injection scenarios', () => {
|
||||
it('should handle AI trying to inject code via parameters', () => {
|
||||
const responseText = `
|
||||
<tool_use name="wiki-search">
|
||||
{
|
||||
"query": "normal query",
|
||||
"filter": "[tag[test]]"
|
||||
}
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
// Should parse safely as JSON
|
||||
expect(result.parameters?.query).toBe('normal query');
|
||||
expect(result.parameters?.filter).toBe('[tag[test]]');
|
||||
});
|
||||
|
||||
it('should handle nested malicious code in valid JSON', () => {
|
||||
const responseText = `
|
||||
<tool_use name="test-tool">
|
||||
{
|
||||
"normal": "value",
|
||||
"nested": {
|
||||
"attack": "'; require('fs').readFileSync('/etc/passwd'); '"
|
||||
}
|
||||
}
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
// Should parse as JSON, making the attack string just data
|
||||
expect(result.parameters?.normal).toBe('value');
|
||||
expect(result.parameters?.nested).toEqual({
|
||||
attack: "'; require('fs').readFileSync('/etc/passwd'); '",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Edge cases and robustness', () => {
|
||||
it('should handle empty parameters', () => {
|
||||
const responseText = `
|
||||
<tool_use name="test-tool">
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.parameters).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle whitespace-only parameters', () => {
|
||||
const responseText = `
|
||||
<tool_use name="test-tool">
|
||||
|
||||
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.parameters).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle special characters in string values safely', () => {
|
||||
const responseText = `
|
||||
<tool_use name="test-tool">
|
||||
{
|
||||
"text": "Contains 'quotes' and \\"escapes\\" and symbols @#$%"
|
||||
}
|
||||
</tool_use>
|
||||
`;
|
||||
|
||||
const result = matchToolCalling(responseText);
|
||||
|
||||
expect(result.found).toBe(true);
|
||||
expect(result.parameters?.text).toContain('quotes');
|
||||
expect(result.parameters?.text).toContain('escapes');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,5 +1,5 @@
|
|||
/* eslint-disable @typescript-eslint/no-implied-eval */
|
||||
import { logger } from '@services/libs/log';
|
||||
import JSON5 from 'json5';
|
||||
import { ToolCallingMatch } from './interface';
|
||||
|
||||
interface ToolPattern {
|
||||
|
|
@ -10,9 +10,17 @@ interface ToolPattern {
|
|||
extractOriginalText: (match: RegExpExecArray) => string;
|
||||
}
|
||||
|
||||
const MAX_FALLBACK_INPUT_LENGTH = 1000;
|
||||
/**
|
||||
* Parse tool parameters from text content
|
||||
* Supports JSON, YAML-like, and key-value formats
|
||||
* Supports JSON and JSON5 (relaxed JSON) formats
|
||||
*
|
||||
* This function does NOT execute any code - it only parses data formats.
|
||||
* The use of JSON5 allows parsing of common AI mistakes like:
|
||||
* - Trailing commas: { "key": "value", }
|
||||
* - Single quotes: { 'key': 'value' }
|
||||
* - Unquoted keys: { key: "value" }
|
||||
* - Comments in JSON
|
||||
*/
|
||||
function parseToolParameters(parametersText: string): Record<string, unknown> {
|
||||
if (!parametersText || !parametersText.trim()) {
|
||||
|
|
@ -21,92 +29,39 @@ function parseToolParameters(parametersText: string): Record<string, unknown> {
|
|||
|
||||
const trimmedText = parametersText.trim();
|
||||
|
||||
// Try JSON parsing first
|
||||
// Try standard JSON parsing first (fastest and most secure)
|
||||
try {
|
||||
return JSON.parse(trimmedText) as Record<string, unknown>;
|
||||
} catch {
|
||||
// JSON parsing failed, try other formats
|
||||
// JSON parsing failed, try JSON5
|
||||
}
|
||||
|
||||
// Try parsing as JavaScript object literal using new Function
|
||||
// Try JSON5 parsing (handles relaxed JSON syntax)
|
||||
// JSON5 is a superset of JSON that supports:
|
||||
// - Single quotes, unquoted keys, trailing commas, comments, etc.
|
||||
// - Pure data parsing, NO code execution
|
||||
try {
|
||||
// Wrap the object in a return statement to make it a valid function body
|
||||
const functionBody = `return (${trimmedText});`;
|
||||
// Using the Function constructor here is intentional: we need to parse
|
||||
// JavaScript-like object literals that may not be valid JSON. The use of
|
||||
// Function is restricted and the input is user-provided; we guard and
|
||||
// catch errors below.
|
||||
const parseFunction = new Function(functionBody) as unknown as () => Record<string, unknown>;
|
||||
const parsed = parseFunction();
|
||||
|
||||
logger.debug('Successfully parsed JavaScript object using new Function', {
|
||||
original: trimmedText,
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
|
||||
const parsed = JSON5.parse(trimmedText);
|
||||
logger.debug('Successfully parsed parameters using JSON5', {
|
||||
original: trimmedText.substring(0, 100),
|
||||
parsed: typeof parsed,
|
||||
});
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-return
|
||||
return parsed;
|
||||
} catch (functionError) {
|
||||
logger.debug('Failed to parse using new Function', {
|
||||
original: trimmedText,
|
||||
error: functionError instanceof Error ? functionError.message : String(functionError),
|
||||
} catch (json5Error) {
|
||||
logger.debug('Failed to parse parameters as JSON/JSON5', {
|
||||
original: trimmedText.substring(0, 100),
|
||||
error: json5Error instanceof Error ? json5Error.message : String(json5Error),
|
||||
});
|
||||
}
|
||||
|
||||
// Try parsing as JavaScript object literal (with regex conversion to JSON)
|
||||
try {
|
||||
// Convert JavaScript object syntax to JSON
|
||||
let jsonText = trimmedText;
|
||||
|
||||
// Replace unquoted keys with quoted keys
|
||||
// This regex matches object property names that aren't already quoted
|
||||
jsonText = jsonText.replace(/([{,]\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g, '$1"$2":');
|
||||
|
||||
// Handle edge case where the object starts with an unquoted key
|
||||
jsonText = jsonText.replace(/^(\s*)([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/, '$1"$2":');
|
||||
|
||||
const parsed = JSON.parse(jsonText) as Record<string, unknown>;
|
||||
logger.debug('Successfully parsed JavaScript object literal as JSON', {
|
||||
original: trimmedText,
|
||||
converted: jsonText,
|
||||
});
|
||||
return parsed;
|
||||
} catch (jsonError) {
|
||||
logger.debug('Failed to parse as JavaScript object literal', {
|
||||
original: trimmedText,
|
||||
error: jsonError instanceof Error ? jsonError.message : String(jsonError),
|
||||
});
|
||||
}
|
||||
|
||||
// Check which format is most likely being used
|
||||
const lines = trimmedText.split('\n').map(line => line.trim()).filter(Boolean);
|
||||
const hasEqualSigns = lines.some(line => line.includes('='));
|
||||
|
||||
// If we have equal signs, prefer key=value parsing
|
||||
if (hasEqualSigns) {
|
||||
const kvResult: Record<string, unknown> = {};
|
||||
let hasValidKvPairs = false;
|
||||
|
||||
for (const line of lines) {
|
||||
const equalIndex = line.indexOf('=');
|
||||
if (equalIndex > 0) {
|
||||
const key = line.slice(0, equalIndex).trim();
|
||||
const value = line.slice(equalIndex + 1).trim();
|
||||
// Try to parse as JSON value, fallback to string
|
||||
try {
|
||||
kvResult[key] = JSON.parse(value);
|
||||
} catch {
|
||||
kvResult[key] = value;
|
||||
}
|
||||
hasValidKvPairs = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (hasValidKvPairs) {
|
||||
return kvResult;
|
||||
}
|
||||
}
|
||||
|
||||
// Return as single parameter if all parsing failed
|
||||
return { input: trimmedText };
|
||||
// Limit length to prevent potential issues
|
||||
logger.debug('All parsing methods failed, returning as raw input', {
|
||||
original: trimmedText.substring(0, 100),
|
||||
});
|
||||
return { input: trimmedText.substring(0, MAX_FALLBACK_INPUT_LENGTH) };
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -11,6 +11,42 @@ export class DeepLinkService implements IDeepLinkService {
|
|||
constructor(
|
||||
@inject(serviceIdentifier.Workspace) private readonly workspaceService: IWorkspaceService,
|
||||
) {}
|
||||
/**
|
||||
* Sanitize tiddler name to prevent injection attacks.
|
||||
* This escapes potentially dangerous characters while preserving the original content.
|
||||
* TiddlyWiki recommends avoiding: | [ ] { } in tiddler titles
|
||||
*
|
||||
* And in the place that use this (wikiOperations/executor/scripts/*.ts), we also use JSON.stringify to exclude "`".
|
||||
* @param tiddlerName The tiddler name to sanitize
|
||||
* @returns Sanitized tiddler name
|
||||
*/
|
||||
private sanitizeTiddlerName(tiddlerName: string): string {
|
||||
let sanitized = tiddlerName;
|
||||
|
||||
// Remove null bytes (these should never appear in valid text)
|
||||
sanitized = sanitized.replace(/\0/g, '');
|
||||
|
||||
// Replace newlines and tabs with spaces to prevent breaking out of string context
|
||||
sanitized = sanitized.replace(/[\r\n\t]/g, ' ');
|
||||
|
||||
// Remove HTML tags to prevent XSS
|
||||
sanitized = sanitized.replace(/<\/?[^>]+(>|$)/g, '');
|
||||
|
||||
// Remove TiddlyWiki special characters that could cause parsing issues
|
||||
sanitized = sanitized.replace(/[|[\]{}]/g, '');
|
||||
|
||||
// Trim whitespace
|
||||
sanitized = sanitized.trim();
|
||||
|
||||
// Limit length to prevent DoS
|
||||
if (sanitized.length > 1000) {
|
||||
sanitized = sanitized.substring(0, 1000);
|
||||
logger.warn(`Tiddler name truncated to 1000 characters for security`, { original: tiddlerName.substring(0, 50), function: 'sanitizeTiddlerName' });
|
||||
}
|
||||
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle link and open the workspace.
|
||||
* @param requestUrl like `tidgi://lxqsftvfppu_z4zbaadc0/#:Index` or `tidgi://lxqsftvfppu_z4zbaadc0/#%E6%96%B0%E6%9D%A1%E7%9B%AE`
|
||||
|
|
@ -40,6 +76,16 @@ export class DeepLinkService implements IDeepLinkService {
|
|||
}
|
||||
// Support CJK
|
||||
tiddlerName = decodeURIComponent(tiddlerName);
|
||||
|
||||
// Sanitize tiddler name to prevent injection attacks
|
||||
tiddlerName = this.sanitizeTiddlerName(tiddlerName);
|
||||
|
||||
// Validate that tiddler name is not empty after sanitization
|
||||
if (!tiddlerName || tiddlerName.length === 0) {
|
||||
logger.warn(`Invalid or empty tiddler name after sanitization`, { original: hash, function: 'deepLinkHandler' });
|
||||
return;
|
||||
}
|
||||
|
||||
logger.info(`Open deep link`, { workspaceId: workspace.id, tiddlerName, function: 'deepLinkHandler' });
|
||||
await this.workspaceService.openWorkspaceTiddler(workspace, tiddlerName);
|
||||
} catch (error) {
|
||||
|
|
|
|||
|
|
@ -177,6 +177,8 @@ async function generateImageFromComfyUI(
|
|||
}
|
||||
|
||||
// Read the workflow JSON file
|
||||
// Note: This is user-provided configuration data (not sensitive system files)
|
||||
// The workflow file contains ComfyUI node configurations and is meant to be sent to the API
|
||||
let workflow: Record<string, unknown>;
|
||||
try {
|
||||
const workflowContent = await fs.readFile(workflowPath, 'utf-8');
|
||||
|
|
|
|||
|
|
@ -1,5 +1,8 @@
|
|||
/**
|
||||
* This fixes https://github.com/google/zx/issues/230
|
||||
*/
|
||||
import { isWin } from '@/helpers/system';
|
||||
import { execSync } from 'child_process';
|
||||
import { execFileSync } from 'child_process';
|
||||
import { userInfo } from 'os';
|
||||
import process from 'process';
|
||||
import stripAnsi from 'strip-ansi';
|
||||
|
|
@ -43,13 +46,42 @@ const parseEnvironment = (environment_: string): Record<string, string> => {
|
|||
return returnValue;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates that a shell path is safe to execute
|
||||
* Only allows absolute paths without shell metacharacters
|
||||
*/
|
||||
function validateShellPath(shellPath: string): boolean {
|
||||
// Must be an absolute path
|
||||
if (!shellPath.startsWith('/')) {
|
||||
return false;
|
||||
}
|
||||
// Must not contain shell metacharacters that could enable injection
|
||||
const dangerousChars = /[;&|`$(){}[\]<>'"\\]/;
|
||||
if (dangerousChars.test(shellPath)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function shellEnvironmentSync(shell?: string): NodeJS.ProcessEnv {
|
||||
if (isWin) {
|
||||
return process.env;
|
||||
}
|
||||
|
||||
const shellToUse = shell ?? defaultShell;
|
||||
|
||||
// Validate shell path to prevent command injection
|
||||
if (!validateShellPath(shellToUse)) {
|
||||
console.warn(`[fixPath] Invalid shell path rejected: ${shellToUse}`);
|
||||
return process.env;
|
||||
}
|
||||
|
||||
try {
|
||||
const stdout = execSync(`${shell ?? defaultShell} ${arguments_.join(' ')}`, { env: environment });
|
||||
// Use execFileSync to prevent argument injection - arguments are passed as array, not shell-interpolated string
|
||||
// This prevents shell metacharacters in arguments from being interpreted
|
||||
const stdout = execFileSync(shellToUse, arguments_, {
|
||||
env: environment,
|
||||
});
|
||||
return parseEnvironment(String(stdout));
|
||||
} catch (error) {
|
||||
if (shell === undefined) {
|
||||
|
|
|
|||
|
|
@ -44,7 +44,9 @@ export function updateSubWikiPluginContent(
|
|||
): void {
|
||||
const FileSystemPathsTiddlerPath = getFileSystemPathsTiddlerPath(mainWikiPath);
|
||||
|
||||
const FileSystemPathsFile = fs.existsSync(FileSystemPathsTiddlerPath) ? fs.readFileSync(FileSystemPathsTiddlerPath, 'utf8') : emptyFileSystemPathsTiddler;
|
||||
// Read file content atomically - re-read just before write to minimize race condition window
|
||||
const readFileContent = () => fs.existsSync(FileSystemPathsTiddlerPath) ? fs.readFileSync(FileSystemPathsTiddlerPath, 'utf8') : emptyFileSystemPathsTiddler;
|
||||
const FileSystemPathsFile = readFileContent();
|
||||
let newFileSystemPathsFile = '';
|
||||
// ignore the tags, title and type, 3 lines, and an empty line
|
||||
const header = take(FileSystemPathsFile.split('\n\n'), 1);
|
||||
|
|
@ -90,7 +92,82 @@ export function updateSubWikiPluginContent(
|
|||
newFileSystemPathsFile = `${FileSystemPathsFile}\n${newConfigLine}`;
|
||||
}
|
||||
}
|
||||
fs.writeFileSync(FileSystemPathsTiddlerPath, newFileSystemPathsFile);
|
||||
|
||||
// Helper function to recalculate file content from fresh data
|
||||
const recalculateContent = (freshFileContent: string): string => {
|
||||
const lines = freshFileContent.split('\n');
|
||||
const freshHeader = lines.filter((line) => line.startsWith('\\'));
|
||||
const freshFileSystemPaths = lines.filter((line) => !line.startsWith('\\') && line.length > 0);
|
||||
|
||||
if (newConfig === undefined) {
|
||||
// Delete operation
|
||||
if (oldConfig === undefined || typeof oldConfig.tagName !== 'string' || typeof oldConfig.subWikiFolderName !== 'string') {
|
||||
throw new Error('Invalid oldConfig in delete operation');
|
||||
}
|
||||
const { tagName: oldTagName, subWikiFolderName: oldSubWikiFolderName } = oldConfig;
|
||||
const newPaths = freshFileSystemPaths.filter((line) =>
|
||||
!(line.includes(getMatchPart(oldTagName)) && line.includes(getPathPart(oldSubWikiFolderName, subWikiPathDirectoryName)))
|
||||
);
|
||||
return `${freshHeader.join('\n')}\n\n${newPaths.join('\n')}`;
|
||||
} else {
|
||||
// Add or update operation
|
||||
const { tagName: newTagName, subWikiFolderName: newSubWikiFolderName } = newConfig;
|
||||
if (typeof newTagName !== 'string' || typeof newSubWikiFolderName !== 'string') {
|
||||
throw new Error('Invalid newConfig in add/update operation');
|
||||
}
|
||||
|
||||
const newConfigLine = '[' + getMatchPart(newTagName) + andPart + getPathPart(newSubWikiFolderName, subWikiPathDirectoryName);
|
||||
|
||||
if (oldConfig !== undefined && typeof oldConfig.tagName === 'string' && typeof oldConfig.subWikiFolderName === 'string') {
|
||||
// Update: replace old line with new line
|
||||
const { tagName: oldTagName, subWikiFolderName: oldSubWikiFolderName } = oldConfig;
|
||||
const newPaths = freshFileSystemPaths.map((line) => {
|
||||
if (line.includes(oldTagName) && line.includes(oldSubWikiFolderName)) {
|
||||
return newConfigLine;
|
||||
}
|
||||
return line;
|
||||
});
|
||||
return `${freshHeader.join('\n')}\n\n${newPaths.join('\n')}`;
|
||||
} else {
|
||||
// Add: append new line
|
||||
return `${freshFileContent}\n${newConfigLine}`;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Retry mechanism to handle race conditions
|
||||
const MAX_RETRIES = 3;
|
||||
let retryCount = 0;
|
||||
let success = false;
|
||||
|
||||
while (retryCount < MAX_RETRIES && !success) {
|
||||
try {
|
||||
const currentContent = readFileContent();
|
||||
|
||||
// If file hasn't changed since initial read, write our calculated content
|
||||
if (currentContent === FileSystemPathsFile) {
|
||||
fs.writeFileSync(FileSystemPathsTiddlerPath, newFileSystemPathsFile);
|
||||
success = true;
|
||||
} else if (retryCount < MAX_RETRIES - 1) {
|
||||
// File was modified by another process, retry with fresh data to avoid data loss
|
||||
console.warn(`[subWikiPlugin] File was modified during update, retrying with fresh data (attempt ${retryCount + 1}/${MAX_RETRIES})`);
|
||||
|
||||
// Recalculate content based on fresh file data
|
||||
newFileSystemPathsFile = recalculateContent(currentContent);
|
||||
|
||||
retryCount++;
|
||||
} else {
|
||||
// Final attempt: recalculate one last time and write
|
||||
console.error('[subWikiPlugin] Max retries reached, forcing write with latest data. Concurrent modifications may be lost.');
|
||||
newFileSystemPathsFile = recalculateContent(currentContent);
|
||||
fs.writeFileSync(FileSystemPathsTiddlerPath, newFileSystemPathsFile);
|
||||
success = true;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[subWikiPlugin] Error writing file:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ interface IAddTiddlerOptionOptions {
|
|||
|
||||
export const wikiOperationScripts = {
|
||||
[WikiChannel.setState]: (stateKey: string, content: string) => `
|
||||
return $tw.wiki.addTiddler({ title: '$:/state/${stateKey}', text: \`${content}\` });
|
||||
return $tw.wiki.addTiddler({ title: '$:/state/${stateKey}', text: ${JSON.stringify(content)} });
|
||||
`,
|
||||
/**
|
||||
* add tiddler
|
||||
|
|
@ -27,7 +27,7 @@ export const wikiOperationScripts = {
|
|||
${
|
||||
options.withDate === true
|
||||
? `
|
||||
const existedTiddler = $tw.wiki.getTiddler(\`${title}\`);
|
||||
const existedTiddler = $tw.wiki.getTiddler(${JSON.stringify(title)});
|
||||
let created = existedTiddler?.fields?.created;
|
||||
const modified = $tw.utils.stringifyDate(new Date());
|
||||
if (!existedTiddler) {
|
||||
|
|
@ -38,42 +38,42 @@ export const wikiOperationScripts = {
|
|||
`
|
||||
: ''
|
||||
}
|
||||
return $tw.wiki.addTiddler({ title: \`${title}\`, text: \`${text}\`, ...${extraMeta}, ...dateObject });
|
||||
return $tw.wiki.addTiddler({ title: ${JSON.stringify(title)}, text: ${JSON.stringify(text)}, ...${extraMeta}, ...dateObject });
|
||||
`;
|
||||
},
|
||||
[WikiChannel.getTiddlerText]: (title: string) => `
|
||||
return $tw.wiki.getTiddlerText(\`${title}\`);
|
||||
return $tw.wiki.getTiddlerText(${JSON.stringify(title)});
|
||||
`,
|
||||
[WikiChannel.runFilter]: (filter: string) => `
|
||||
return $tw.wiki.compileFilter(\`${filter}\`)()
|
||||
return $tw.wiki.compileFilter(${JSON.stringify(filter)})()
|
||||
`,
|
||||
/**
|
||||
* Modified from `$tw.wiki.getTiddlersAsJson` (it will turn tags into string, so we are not using it.)
|
||||
* This modified version will return Object
|
||||
*/
|
||||
[WikiChannel.getTiddlersAsJson]: (filter: string) => `
|
||||
return $tw.wiki.filterTiddlers(\`${filter}\`).map(title => {
|
||||
return $tw.wiki.filterTiddlers(${JSON.stringify(filter)}).map(title => {
|
||||
const tiddler = $tw.wiki.getTiddler(title);
|
||||
return tiddler?.fields;
|
||||
}).filter(item => item !== undefined)
|
||||
`,
|
||||
[WikiChannel.setTiddlerText]: (title: string, value: string) => `
|
||||
return $tw.wiki.setText(\`${title}\`, 'text', undefined, \`${value}\`);
|
||||
return $tw.wiki.setText(${JSON.stringify(title)}, 'text', undefined, ${JSON.stringify(value)});
|
||||
`,
|
||||
[WikiChannel.renderWikiText]: (content: string) => `
|
||||
return $tw.wiki.renderText("text/html", "text/vnd.tiddlywiki", \`${content.replaceAll('`', '\\`')}\`);
|
||||
return $tw.wiki.renderText("text/html", "text/vnd.tiddlywiki", ${JSON.stringify(content)});
|
||||
`,
|
||||
[WikiChannel.dispatchEvent]: (actionMessage: string) => `
|
||||
return $tw.rootWidget.dispatchEvent({ type: \`${actionMessage}\` });
|
||||
return $tw.rootWidget.dispatchEvent({ type: ${JSON.stringify(actionMessage)} });
|
||||
`,
|
||||
[WikiChannel.deleteTiddler]: (title: string) => `
|
||||
return $tw.wiki.deleteTiddler(\`${title}\`);
|
||||
return $tw.wiki.deleteTiddler(${JSON.stringify(title)});
|
||||
`,
|
||||
[WikiChannel.getTiddler]: (title: string) => `
|
||||
return $tw.wiki.getTiddler(\`${title}\`);
|
||||
return $tw.wiki.getTiddler(${JSON.stringify(title)});
|
||||
`,
|
||||
[WikiChannel.invokeActionsByTag]: (tag: string, stringifiedData: string) => `
|
||||
const event = new Event('TidGi-invokeActionByTag');
|
||||
return $tw.rootWidget.invokeActionsByTag("${tag}",event,${stringifiedData});
|
||||
return $tw.rootWidget.invokeActionsByTag(${JSON.stringify(tag)},event,${stringifiedData});
|
||||
`,
|
||||
} as const;
|
||||
|
|
|
|||
|
|
@ -39,17 +39,17 @@ async function generateHTML(title: string, tiddlerDiv: HTMLElement): Promise<str
|
|||
export const wikiOperationScripts = {
|
||||
...common,
|
||||
[WikiChannel.syncProgress]: (message: string) => `
|
||||
$tw.wiki.addTiddler({ title: '$:/state/notification/${WikiChannel.syncProgress}', text: \`${message}\` });
|
||||
$tw.wiki.addTiddler({ title: '$:/state/notification/${WikiChannel.syncProgress}', text: ${JSON.stringify(message)} });
|
||||
return $tw.notifier.display('$:/state/notification/${WikiChannel.syncProgress}');
|
||||
`,
|
||||
|
||||
[WikiChannel.generalNotification]: (message: string) => `
|
||||
$tw.wiki.addTiddler({ title: \`$:/state/notification/${WikiChannel.generalNotification}\`, text: \`${message}\` });
|
||||
$tw.wiki.addTiddler({ title: \`$:/state/notification/${WikiChannel.generalNotification}\`, text: ${JSON.stringify(message)} });
|
||||
return $tw.notifier.display(\`$:/state/notification/${WikiChannel.generalNotification}\`);
|
||||
`,
|
||||
|
||||
[WikiChannel.openTiddler]: (tiddlerName: string) => `
|
||||
let trimmedTiddlerName = \`${tiddlerName.replaceAll('\n', '')}\`;
|
||||
let trimmedTiddlerName = ${JSON.stringify(tiddlerName)};
|
||||
let currentHandlerWidget = $tw.rootWidget;
|
||||
let handled = false;
|
||||
while (currentHandlerWidget && !handled) {
|
||||
|
|
@ -60,9 +60,9 @@ export const wikiOperationScripts = {
|
|||
return handled;
|
||||
`,
|
||||
[WikiChannel.renderTiddlerOuterHTML]: (title: string) => `
|
||||
const tiddlerDiv = document.querySelector('div[data-tiddler-title="${title}"]');
|
||||
const tiddlerDiv = document.querySelector('div[data-tiddler-title=' + ${JSON.stringify(title)} + ']');
|
||||
if (tiddlerDiv) {
|
||||
return await (${generateHTML.toString()})('${title}', tiddlerDiv);
|
||||
return await (${generateHTML.toString()})(${JSON.stringify(title)}, tiddlerDiv);
|
||||
}
|
||||
return '';
|
||||
`,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue