mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-03-22 23:30:36 +08:00
fix(electron): create doc shortcut should follow default type in settings (#14678)
This commit is contained in:
@@ -46,7 +46,10 @@ export function setupEvents(frameworkProvider: FrameworkProvider) {
|
||||
const { workspace } = currentWorkspace;
|
||||
const docsService = workspace.scope.get(DocsService);
|
||||
|
||||
const page = docsService.createDoc({ primaryMode: type });
|
||||
const page =
|
||||
type === 'default'
|
||||
? docsService.createDoc()
|
||||
: docsService.createDoc({ primaryMode: type });
|
||||
workspace.scope.get(WorkbenchService).workbench.openDoc(page.id);
|
||||
})
|
||||
.catch(err => {
|
||||
|
||||
@@ -67,7 +67,7 @@ export function createApplicationMenu() {
|
||||
click: async () => {
|
||||
await initAndShowMainWindow();
|
||||
// fixme: if the window is just created, the new page action will not be triggered
|
||||
applicationMenuSubjects.newPageAction$.next('page');
|
||||
applicationMenuSubjects.newPageAction$.next('default');
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { MainEventRegister } from '../type';
|
||||
import { applicationMenuSubjects } from './subject';
|
||||
import { applicationMenuSubjects, type NewPageAction } from './subject';
|
||||
|
||||
export * from './create';
|
||||
export * from './subject';
|
||||
@@ -11,7 +11,7 @@ export const applicationMenuEvents = {
|
||||
/**
|
||||
* File -> New Doc
|
||||
*/
|
||||
onNewPageAction: (fn: (type: 'page' | 'edgeless') => void) => {
|
||||
onNewPageAction: (fn: (type: NewPageAction) => void) => {
|
||||
const sub = applicationMenuSubjects.newPageAction$.subscribe(fn);
|
||||
return () => {
|
||||
sub.unsubscribe();
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Subject } from 'rxjs';
|
||||
|
||||
export type NewPageAction = 'page' | 'edgeless' | 'default';
|
||||
|
||||
export const applicationMenuSubjects = {
|
||||
newPageAction$: new Subject<'page' | 'edgeless'>(),
|
||||
newPageAction$: new Subject<NewPageAction>(),
|
||||
openJournal$: new Subject<void>(),
|
||||
openInSettingModal$: new Subject<{
|
||||
activeTab: string;
|
||||
|
||||
@@ -9,6 +9,7 @@ import { beforeAppQuit } from './cleanup';
|
||||
import { logger } from './logger';
|
||||
import { powerEvents } from './power';
|
||||
import { recordingEvents } from './recording';
|
||||
import { checkSource } from './security-restrictions';
|
||||
import { sharedStorageEvents } from './shared-storage';
|
||||
import { uiEvents } from './ui/events';
|
||||
import { updaterEvents } from './updater/event';
|
||||
@@ -70,7 +71,7 @@ export function registerEvents() {
|
||||
action: 'subscribe' | 'unsubscribe',
|
||||
channel: string
|
||||
) => {
|
||||
if (typeof channel !== 'string') return;
|
||||
if (!checkSource(event) || typeof channel !== 'string') return;
|
||||
if (action === 'subscribe') {
|
||||
addSubscription(event.sender, channel);
|
||||
if (channel === 'power:power-source') {
|
||||
|
||||
@@ -7,6 +7,7 @@ import { configStorageHandlers } from './config-storage';
|
||||
import { findInPageHandlers } from './find-in-page';
|
||||
import { getLogFilePath, logger, revealLogFile } from './logger';
|
||||
import { recordingHandlers } from './recording';
|
||||
import { checkSource } from './security-restrictions';
|
||||
import { sharedStorageHandlers } from './shared-storage';
|
||||
import { uiHandlers } from './ui/handlers';
|
||||
import { updaterHandlers } from './updater';
|
||||
@@ -49,7 +50,7 @@ export const registerHandlers = () => {
|
||||
...args: any[]
|
||||
) => {
|
||||
// args[0] is the `{namespace:key}`
|
||||
if (typeof args[0] !== 'string') {
|
||||
if (!checkSource(e) || typeof args[0] !== 'string') {
|
||||
logger.error('invalid ipc message', args);
|
||||
return;
|
||||
}
|
||||
@@ -97,6 +98,8 @@ export const registerHandlers = () => {
|
||||
});
|
||||
|
||||
ipcMain.on(AFFINE_API_CHANNEL_NAME, (e, ...args: any[]) => {
|
||||
if (!checkSource(e)) return;
|
||||
|
||||
handleIpcMessage(e, ...args)
|
||||
.then(ret => {
|
||||
e.returnValue = ret;
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import './security-restrictions';
|
||||
|
||||
import path from 'node:path';
|
||||
|
||||
import * as Sentry from '@sentry/electron/main';
|
||||
@@ -15,6 +13,7 @@ import { registerHandlers } from './handlers';
|
||||
import { logger } from './logger';
|
||||
import { registerProtocol } from './protocol';
|
||||
import { setupRecordingFeature } from './recording/feature';
|
||||
import { registerSecurityRestrictions } from './security-restrictions';
|
||||
import { setupTrayState } from './tray';
|
||||
import { registerUpdater } from './updater';
|
||||
import { launch } from './windows-manager/launcher';
|
||||
@@ -105,6 +104,7 @@ app.on('activate', () => {
|
||||
});
|
||||
|
||||
setupDeepLink(app);
|
||||
registerSecurityRestrictions();
|
||||
|
||||
/**
|
||||
* Create app window when background process will be ready
|
||||
|
||||
@@ -4,9 +4,9 @@ import { pathToFileURL } from 'node:url';
|
||||
import { app, net, protocol, session } from 'electron';
|
||||
import cookieParser from 'set-cookie-parser';
|
||||
|
||||
import { anotherHost, mainHost } from '../shared/internal-origin';
|
||||
import { isWindows, resourcesPath } from '../shared/utils';
|
||||
import { buildType, isDev } from './config';
|
||||
import { anotherHost, mainHost } from './constants';
|
||||
import { logger } from './logger';
|
||||
|
||||
const webStaticDir = join(resourcesPath, 'web-static');
|
||||
|
||||
@@ -1,71 +1,71 @@
|
||||
import { app } from 'electron';
|
||||
|
||||
import { anotherHost, mainHost } from './constants';
|
||||
import { isInternalUrl } from '../shared/internal-origin';
|
||||
import { logger } from './logger';
|
||||
import { openExternalSafely } from './security/open-external';
|
||||
import { validateRedirectProxyUrl } from './security/redirect-proxy';
|
||||
|
||||
app.on('web-contents-created', (_, contents) => {
|
||||
const isInternalUrl = (url: string) => {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (
|
||||
parsed.protocol === 'assets:' &&
|
||||
(parsed.hostname === mainHost || parsed.hostname === anotherHost)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
} catch {}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* Block navigation to origins not on the allowlist.
|
||||
*
|
||||
* Navigation is a common attack vector. If an attacker can convince the app to navigate away
|
||||
* from its current page, they can possibly force the app to open web sites on the Internet.
|
||||
*
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#13-disable-or-limit-navigation
|
||||
*/
|
||||
contents.on('will-navigate', (event, url) => {
|
||||
if (isInternalUrl(url)) {
|
||||
return;
|
||||
}
|
||||
// Prevent navigation
|
||||
event.preventDefault();
|
||||
openExternalSafely(url).catch(error => {
|
||||
console.error('[security] Failed to open external URL:', error);
|
||||
});
|
||||
});
|
||||
export const checkSource = (
|
||||
e: Electron.IpcMainInvokeEvent | Electron.IpcMainEvent
|
||||
) => {
|
||||
const url = e.senderFrame?.url || e.sender.getURL();
|
||||
const result = isInternalUrl(url);
|
||||
if (!result) logger.error('invalid source', url);
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hyperlinks to allowed sites open in the default browser.
|
||||
*
|
||||
* The creation of new `webContents` is a common attack vector. Attackers attempt to convince the app to create new windows,
|
||||
* frames, or other renderer processes with more privileges than they had before; or with pages opened that they couldn't open before.
|
||||
* You should deny any unexpected window creation.
|
||||
*
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#14-disable-or-limit-creation-of-new-windows
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#15-do-not-use-openexternal-with-untrusted-content
|
||||
*/
|
||||
contents.setWindowOpenHandler(({ url }) => {
|
||||
if (!isInternalUrl(url)) {
|
||||
export const registerSecurityRestrictions = () => {
|
||||
app.on('web-contents-created', (_, contents) => {
|
||||
/**
|
||||
* Block navigation to origins not on the allowlist.
|
||||
*
|
||||
* Navigation is a common attack vector. If an attacker can convince the app to navigate away
|
||||
* from its current page, they can possibly force the app to open web sites on the Internet.
|
||||
*
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#13-disable-or-limit-navigation
|
||||
*/
|
||||
contents.on('will-navigate', (event, url) => {
|
||||
if (isInternalUrl(url)) {
|
||||
return;
|
||||
}
|
||||
// Prevent navigation
|
||||
event.preventDefault();
|
||||
openExternalSafely(url).catch(error => {
|
||||
console.error('[security] Failed to open external URL:', error);
|
||||
});
|
||||
} else if (url.includes('/redirect-proxy')) {
|
||||
const result = validateRedirectProxyUrl(url);
|
||||
if (!result.allow) {
|
||||
console.warn(
|
||||
`[security] Blocked redirect proxy: ${result.reason}`,
|
||||
result.redirectTarget ?? url
|
||||
);
|
||||
return { action: 'deny' };
|
||||
}
|
||||
});
|
||||
|
||||
openExternalSafely(result.redirectTarget).catch(error => {
|
||||
console.error('[security] Failed to open external URL:', error);
|
||||
});
|
||||
}
|
||||
// Prevent creating new window in application
|
||||
return { action: 'deny' };
|
||||
/**
|
||||
* Hyperlinks to allowed sites open in the default browser.
|
||||
*
|
||||
* The creation of new `webContents` is a common attack vector. Attackers attempt to convince the app to create new windows,
|
||||
* frames, or other renderer processes with more privileges than they had before; or with pages opened that they couldn't open before.
|
||||
* You should deny any unexpected window creation.
|
||||
*
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#14-disable-or-limit-creation-of-new-windows
|
||||
* @see https://www.electronjs.org/docs/latest/tutorial/security#15-do-not-use-openexternal-with-untrusted-content
|
||||
*/
|
||||
contents.setWindowOpenHandler(({ url }) => {
|
||||
if (!isInternalUrl(url)) {
|
||||
openExternalSafely(url).catch(error => {
|
||||
console.error('[security] Failed to open external URL:', error);
|
||||
});
|
||||
} else if (url.includes('/redirect-proxy')) {
|
||||
const result = validateRedirectProxyUrl(url);
|
||||
if (!result.allow) {
|
||||
console.warn(
|
||||
`[security] Blocked redirect proxy: ${result.reason}`,
|
||||
result.redirectTarget ?? url
|
||||
);
|
||||
return { action: 'deny' };
|
||||
}
|
||||
|
||||
openExternalSafely(result.redirectTarget).catch(error => {
|
||||
console.error('[security] Failed to open external URL:', error);
|
||||
});
|
||||
}
|
||||
// Prevent creating new window in application
|
||||
return { action: 'deny' };
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
@@ -2,8 +2,8 @@ import { join } from 'node:path';
|
||||
|
||||
import { BrowserWindow, type Display, screen } from 'electron';
|
||||
|
||||
import { customThemeViewUrl } from '../../shared/internal-origin';
|
||||
import { isMacOS } from '../../shared/utils';
|
||||
import { customThemeViewUrl } from '../constants';
|
||||
import { logger } from '../logger';
|
||||
import { buildWebPreferences } from '../web-preferences';
|
||||
|
||||
|
||||
@@ -4,10 +4,10 @@ import { BrowserWindow, nativeTheme } from 'electron';
|
||||
import electronWindowState from 'electron-window-state';
|
||||
import { BehaviorSubject, map, shareReplay } from 'rxjs';
|
||||
|
||||
import { mainWindowOrigin } from '../../shared/internal-origin';
|
||||
import { isLinux, isMacOS, isWindows, resourcesPath } from '../../shared/utils';
|
||||
import { beforeAppQuit } from '../cleanup';
|
||||
import { buildType } from '../config';
|
||||
import { mainWindowOrigin } from '../constants';
|
||||
import { ensureHelperProcess } from '../helper-process';
|
||||
import { logger } from '../logger';
|
||||
import { MenubarStateKey, MenubarStateSchema } from '../shared-state-schema';
|
||||
|
||||
@@ -2,8 +2,8 @@ import { join } from 'node:path';
|
||||
|
||||
import { BrowserWindow, screen } from 'electron';
|
||||
|
||||
import { onboardingViewUrl } from '../../shared/internal-origin';
|
||||
import { isDev } from '../config';
|
||||
import { onboardingViewUrl } from '../constants';
|
||||
// import { getExposedMeta } from './exposed';
|
||||
import { logger } from '../logger';
|
||||
import { buildWebPreferences } from '../web-preferences';
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from 'electron';
|
||||
import { BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { popupViewUrl } from '../constants';
|
||||
import { popupViewUrl } from '../../shared/internal-origin';
|
||||
import { logger } from '../logger';
|
||||
import type { MainEventRegister, NamespaceHandlers } from '../type';
|
||||
import { buildWebPreferences } from '../web-preferences';
|
||||
|
||||
@@ -24,9 +24,9 @@ import {
|
||||
type Unsubscribable,
|
||||
} from 'rxjs';
|
||||
|
||||
import { mainWindowOrigin, shellViewUrl } from '../../shared/internal-origin';
|
||||
import { isMacOS } from '../../shared/utils';
|
||||
import { beforeAppQuit, onTabClose } from '../cleanup';
|
||||
import { mainWindowOrigin, shellViewUrl } from '../constants';
|
||||
import { ensureHelperProcess } from '../helper-process';
|
||||
import { logger } from '../logger';
|
||||
import {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { join } from 'node:path';
|
||||
|
||||
import { BrowserWindow, MessageChannelMain, type WebContents } from 'electron';
|
||||
|
||||
import { backgroundWorkerViewUrl } from '../constants';
|
||||
import { backgroundWorkerViewUrl } from '../../shared/internal-origin';
|
||||
import { ensureHelperProcess } from '../helper-process';
|
||||
import { logger } from '../logger';
|
||||
import { buildWebPreferences } from '../web-preferences';
|
||||
|
||||
@@ -2,13 +2,21 @@ import '@sentry/electron/preload';
|
||||
|
||||
import { contextBridge } from 'electron';
|
||||
|
||||
import { isInternalUrl } from '../shared/internal-origin';
|
||||
import { apis, appInfo, events } from './electron-api';
|
||||
import { sharedStorage } from './shared-storage';
|
||||
import { listenWorkerApis } from './worker';
|
||||
|
||||
contextBridge.exposeInMainWorld('__appInfo', appInfo);
|
||||
contextBridge.exposeInMainWorld('__apis', apis);
|
||||
contextBridge.exposeInMainWorld('__events', events);
|
||||
contextBridge.exposeInMainWorld('__sharedStorage', sharedStorage);
|
||||
const locationLike = (globalThis as { location?: { href?: unknown } }).location;
|
||||
|
||||
listenWorkerApis();
|
||||
const currentUrl =
|
||||
typeof locationLike?.href === 'string' ? locationLike.href : null;
|
||||
|
||||
if (currentUrl && isInternalUrl(currentUrl)) {
|
||||
contextBridge.exposeInMainWorld('__appInfo', appInfo);
|
||||
contextBridge.exposeInMainWorld('__apis', apis);
|
||||
contextBridge.exposeInMainWorld('__events', events);
|
||||
contextBridge.exposeInMainWorld('__sharedStorage', sharedStorage);
|
||||
|
||||
listenWorkerApis();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
export const mainHost = '.';
|
||||
export const anotherHost = 'another-host';
|
||||
export const internalHosts = new Set([mainHost, anotherHost]);
|
||||
|
||||
export const mainWindowOrigin = `assets://${mainHost}`;
|
||||
export const anotherOrigin = `assets://${anotherHost}`;
|
||||
@@ -13,3 +14,12 @@ export const customThemeViewUrl = `${mainWindowOrigin}/theme-editor`;
|
||||
// Notes from electron official docs:
|
||||
// "The zoom policy at the Chromium level is same-origin, meaning that the zoom level for a specific domain propagates across all instances of windows with the same domain. Differentiating the window URLs will make zoom work per-window."
|
||||
export const popupViewUrl = `${anotherOrigin}/popup.html`;
|
||||
|
||||
export const isInternalUrl = (url: string) => {
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
return parsed.protocol === 'assets:' && internalHosts.has(parsed.hostname);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
@@ -1,7 +1,12 @@
|
||||
import { test } from '@affine-test/kit/electron';
|
||||
import {
|
||||
ensureInEdgelessMode,
|
||||
ensureInPageMode,
|
||||
} from '@affine-test/kit/utils/editor';
|
||||
import {
|
||||
clickNewPageButton,
|
||||
getBlockSuiteEditorTitle,
|
||||
waitForEditorLoad,
|
||||
} from '@affine-test/kit/utils/page-logic';
|
||||
import { clickSideBarSettingButton } from '@affine-test/kit/utils/sidebar';
|
||||
import { createLocalWorkspace } from '@affine-test/kit/utils/workspace';
|
||||
@@ -14,12 +19,55 @@ const historyShortcut = async (page: Page, command: 'goBack' | 'goForward') => {
|
||||
);
|
||||
};
|
||||
|
||||
const setNewDocDefaultMode = async (
|
||||
page: Page,
|
||||
mode: 'page' | 'edgeless' | 'ask'
|
||||
) => {
|
||||
const modeTriggerByValue = {
|
||||
page: 'page-mode-trigger',
|
||||
edgeless: 'edgeless-mode-trigger',
|
||||
ask: 'ask-every-time-trigger',
|
||||
} as const;
|
||||
|
||||
await clickSideBarSettingButton(page);
|
||||
await page.getByTestId('editor-panel-trigger').click();
|
||||
await page.getByTestId('new-doc-default-mode-trigger').click();
|
||||
await page.getByTestId(modeTriggerByValue[mode]).click();
|
||||
await page.getByTestId('modal-close-button').click();
|
||||
};
|
||||
|
||||
test('new page', async ({ page, workspace }) => {
|
||||
await clickNewPageButton(page);
|
||||
const flavour = (await workspace.current()).meta.flavour;
|
||||
expect(flavour).toBe('local');
|
||||
});
|
||||
|
||||
test('application menu respects default new doc mode', async ({
|
||||
electronApp,
|
||||
page,
|
||||
}) => {
|
||||
await waitForEditorLoad(page);
|
||||
await ensureInPageMode(page);
|
||||
|
||||
await setNewDocDefaultMode(page, 'edgeless');
|
||||
await electronApp.evaluate(({ BrowserWindow, Menu }) => {
|
||||
const menuItem =
|
||||
Menu.getApplicationMenu()?.getMenuItemById('affine:new-page');
|
||||
const focusedWindow = BrowserWindow.getFocusedWindow();
|
||||
|
||||
if (!menuItem) {
|
||||
throw new Error('Missing application menu item: affine:new-page');
|
||||
}
|
||||
if (!focusedWindow) {
|
||||
throw new Error('Missing focused window for application menu dispatch');
|
||||
}
|
||||
|
||||
menuItem.click(undefined, focusedWindow, focusedWindow.webContents);
|
||||
});
|
||||
|
||||
await ensureInEdgelessMode(page);
|
||||
});
|
||||
|
||||
test('app sidebar router forward/back', async ({ page }) => {
|
||||
// create pages
|
||||
await page.waitForTimeout(500);
|
||||
|
||||
Reference in New Issue
Block a user