mirror of
https://github.com/tiddly-gittly/TidGi-Desktop.git
synced 2025-12-05 18:20:39 -08:00
* fix: lint
* chore: upgrade electron-ipc-cat to add try catch but useless
IPC Server: Sending response {
channel: 'ContextChannel',
request: { type: 'apply', propKey: 'get', args: [ 'supportedLanguagesMap' ] },
correlationId: '0.36061460136077916',
result: {}
}
Error sending from webFrameMain: Error: Failed to serialize arguments
at WebFrameMain.s.send (node:electron/js2c/browser_init:2:94282)
at WebContents.b.send (node:electron/js2c/browser_init:2:78703)
at I:\github\TidGi-Desktop.vite\build\main-BW_u7Pqi.js:39200:28
IPC Server: Sending response {
channel: 'ContextChannel',
request: { type: 'apply', propKey: 'get', args: [ 'supportedLanguagesMap' ] },
correlationId: '0.7064988939670734',
result: {}
}
Error sending from webFrameMain: Error: Failed to serialize arguments
at WebFrameMain.s.send (node:electron/js2c/browser_init:2:94282)
at WebContents.b.send (node:electron/js2c/browser_init:2:78703)
at I:\github\TidGi-Desktop.vite\build\main-BW_u7Pqi.js:39200:28
Proxy 对象不能被序列化
* fix: process.resourcesPath changes during app initialization, need to wait for it when start with scheme
* fix: Realign workspace view when reopening window to ensure browser view is properly positioned
fixes #626
* feat: api for git-sync-js to get deleted files
* fix: wikiWorker methods should be async
* log debug not info
* fix: database should init frist before i18n
* fix: better error log when workspace config error
* chore: add maker-msix for windows
* fix: window.meta is not a function when view on browser
* feat: add more git services
* fix: discard file content cause lots of logs
fixes #653
* Update wiki
* test: Git Log window auto-refreshes when files change (only when window is open)
* test: use test id to wait and make test id debug log
* update i18n
* i18n
* lint
* Update test.yml
* Update test.yml
* Update index.tsx
170 lines
6.5 KiB
TypeScript
170 lines
6.5 KiB
TypeScript
import { TIDGI_PROTOCOL_SCHEME } from '@/constants/protocol';
|
|
import { logger } from '@services/libs/log';
|
|
import serviceIdentifier from '@services/serviceIdentifier';
|
|
import type { IWorkspaceService } from '@services/workspaces/interface';
|
|
import { app } from 'electron';
|
|
import { inject, injectable } from 'inversify';
|
|
import path from 'node:path';
|
|
import type { IDeepLinkService } from './interface';
|
|
|
|
@injectable()
|
|
export class DeepLinkService implements IDeepLinkService {
|
|
private pendingDeepLink: string | undefined;
|
|
|
|
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`
|
|
*/
|
|
private readonly deepLinkHandler: (requestUrl: string) => Promise<void> = async (requestUrl) => {
|
|
logger.info(`Receiving deep link`, { requestUrl, function: 'deepLinkHandler' });
|
|
try {
|
|
// hostname is workspace id or name
|
|
const { hostname, hash, pathname } = new URL(requestUrl);
|
|
let workspace = await this.workspaceService.get(hostname);
|
|
if (workspace === undefined) {
|
|
logger.info(`Workspace not found, try get by name`, { hostname, function: 'deepLinkHandler' });
|
|
let workspaceName = hostname;
|
|
// Host name can't use Chinese or it becomes `xn--1-376ap73a`, so use `w` host, and get workspace name from path
|
|
if (hostname === 'w') {
|
|
workspaceName = decodeURIComponent(pathname.split('/')[1] ?? '');
|
|
logger.info(`Workspace name from w/`, { hostname, pathname, workspaceName, function: 'deepLinkHandler' });
|
|
}
|
|
workspace = await this.workspaceService.getByWikiName(workspaceName);
|
|
if (workspace === undefined) {
|
|
// Workspace doesn't exist yet, save for later processing
|
|
logger.info(`Workspace not found, saving deep link for later`, { requestUrl, function: 'deepLinkHandler' });
|
|
this.pendingDeepLink = requestUrl;
|
|
return;
|
|
}
|
|
}
|
|
let tiddlerName = hash.substring(1); // remove '#:'
|
|
if (tiddlerName.includes(':')) {
|
|
tiddlerName = tiddlerName.split(':')[1];
|
|
}
|
|
// 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) {
|
|
logger.error(`Invalid URL`, { requestUrl, error, function: 'deepLinkHandler' });
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Process any pending deep link after workspaces are initialized
|
|
*/
|
|
public async processPendingDeepLink(): Promise<void> {
|
|
if (this.pendingDeepLink) {
|
|
const url = this.pendingDeepLink;
|
|
this.pendingDeepLink = undefined;
|
|
logger.info(`Processing pending deep link`, { url, function: 'processPendingDeepLink' });
|
|
await this.deepLinkHandler(url);
|
|
}
|
|
}
|
|
|
|
public initializeDeepLink(protocol: string) {
|
|
if (process.defaultApp) {
|
|
if (process.argv.length >= 2) {
|
|
app.setAsDefaultProtocolClient(protocol, process.execPath, [path.resolve(process.argv[1])]);
|
|
}
|
|
} else {
|
|
app.setAsDefaultProtocolClient(protocol);
|
|
}
|
|
|
|
if (process.platform === 'darwin') {
|
|
this.setupMacOSHandler();
|
|
} else {
|
|
this.setupWindowsLinuxHandler();
|
|
}
|
|
}
|
|
|
|
private setupMacOSHandler(): void {
|
|
app.on('open-url', (_event, url) => {
|
|
_event.preventDefault();
|
|
void this.deepLinkHandler(url);
|
|
});
|
|
}
|
|
|
|
private setupWindowsLinuxHandler(): void {
|
|
const gotTheLock = app.requestSingleInstanceLock();
|
|
|
|
if (gotTheLock) {
|
|
// Handle second instance (when app is already running)
|
|
app.on('second-instance', (_event, commandLine) => {
|
|
const url = commandLine.pop();
|
|
if (url !== undefined && url !== '') {
|
|
void this.deepLinkHandler(url);
|
|
}
|
|
});
|
|
|
|
// Handle first instance startup with URL parameter
|
|
// On Windows/Linux, protocol URLs are passed as command line arguments
|
|
if (process.argv.length >= 2) {
|
|
// Find the protocol URL in command line arguments
|
|
const protocolUrl = process.argv.find(argument => argument.startsWith(`${TIDGI_PROTOCOL_SCHEME}://`));
|
|
if (protocolUrl) {
|
|
logger.info(`Processing initial deep link from command line`, { protocolUrl, function: 'setupWindowsLinuxHandler' });
|
|
// Process after app is ready
|
|
if (app.isReady()) {
|
|
void this.deepLinkHandler(protocolUrl);
|
|
} else {
|
|
app.once('ready', () => {
|
|
void this.deepLinkHandler(protocolUrl);
|
|
});
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
app.quit();
|
|
}
|
|
}
|
|
}
|