mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-06 08:50:50 +08:00
feat: bump electron (#14151)
This commit is contained in:
@@ -14,11 +14,34 @@ import { getCurrentWorkspace, isAiEnabled } from './utils';
|
||||
|
||||
const logger = new DebugLogger('electron-renderer:recording');
|
||||
|
||||
async function readRecordingFile(filepath: string) {
|
||||
if (apis?.recording?.readRecordingFile) {
|
||||
try {
|
||||
return await apis.recording.readRecordingFile(filepath);
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
'Failed to read recording file via IPC, fallback to fetch',
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const fileUrl = new URL(
|
||||
filepath,
|
||||
location.origin.replace(/\.$/, 'local-file')
|
||||
);
|
||||
const response = await fetch(fileUrl);
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`Failed to fetch recording file: ${response.status} ${response.statusText}`
|
||||
);
|
||||
}
|
||||
return response.arrayBuffer();
|
||||
}
|
||||
|
||||
async function saveRecordingBlob(blobEngine: BlobEngine, filepath: string) {
|
||||
logger.debug('Saving recording', filepath);
|
||||
const opusBuffer = await fetch(new URL(filepath, location.origin)).then(res =>
|
||||
res.arrayBuffer()
|
||||
);
|
||||
const opusBuffer = await readRecordingFile(filepath);
|
||||
const blob = new Blob([opusBuffer], {
|
||||
type: 'audio/mp4',
|
||||
});
|
||||
|
||||
@@ -111,57 +111,60 @@ const makers = [
|
||||
},
|
||||
},
|
||||
},
|
||||
!process.env.SKIP_BUNDLE &&
|
||||
false && {
|
||||
name: '@electron-forge/maker-flatpak',
|
||||
platforms: ['linux'],
|
||||
/** @type {import('@electron-forge/maker-flatpak').MakerFlatpakConfig} */
|
||||
config: {
|
||||
options: {
|
||||
mimeType: linuxMimeTypes,
|
||||
productName,
|
||||
bin: productName,
|
||||
id: fromBuildIdentifier(appIdMap),
|
||||
icon: iconPngPath, // not working yet
|
||||
branch: buildType,
|
||||
files: [
|
||||
[
|
||||
'./resources/affine.metainfo.xml',
|
||||
'/usr/share/metainfo/affine.metainfo.xml',
|
||||
!process.env.SKIP_BUNDLE && {
|
||||
name: '@electron-forge/maker-flatpak',
|
||||
platforms: ['linux'],
|
||||
/** @type {import('@electron-forge/maker-flatpak').MakerFlatpakConfig} */
|
||||
config: {
|
||||
options: {
|
||||
mimeType: linuxMimeTypes,
|
||||
productName,
|
||||
bin: productName,
|
||||
id: appIdMap[buildType],
|
||||
icon: iconPngPath, // not working yet
|
||||
branch: buildType,
|
||||
runtime: 'org.freedesktop.Platform',
|
||||
runtimeVersion: '25.08',
|
||||
sdk: 'org.freedesktop.Sdk',
|
||||
base: 'org.electronjs.Electron2.BaseApp',
|
||||
baseVersion: '25.08',
|
||||
files: [
|
||||
[
|
||||
'./resources/affine.metainfo.xml',
|
||||
'/usr/share/metainfo/affine.metainfo.xml',
|
||||
],
|
||||
],
|
||||
modules: [
|
||||
{
|
||||
name: 'zypak',
|
||||
sources: [
|
||||
{
|
||||
type: 'git',
|
||||
url: 'https://github.com/refi64/zypak',
|
||||
tag: 'v2025.09',
|
||||
},
|
||||
],
|
||||
],
|
||||
runtimeVersion: '25.08',
|
||||
modules: [
|
||||
{
|
||||
name: 'zypak',
|
||||
sources: [
|
||||
{
|
||||
type: 'git',
|
||||
url: 'https://github.com/refi64/zypak',
|
||||
tag: 'v2025.09',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
finishArgs: [
|
||||
// Wayland/X11 Rendering
|
||||
'--socket=wayland',
|
||||
'--socket=x11',
|
||||
'--share=ipc',
|
||||
// Open GL
|
||||
'--device=dri',
|
||||
// Audio output
|
||||
'--socket=pulseaudio',
|
||||
// Read/write home directory access
|
||||
'--filesystem=home',
|
||||
// Allow communication with network
|
||||
'--share=network',
|
||||
// System notifications with libnotify
|
||||
'--talk-name=org.freedesktop.Notifications',
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
finishArgs: [
|
||||
// Wayland/X11 Rendering
|
||||
'--socket=wayland',
|
||||
'--socket=x11',
|
||||
'--share=ipc',
|
||||
// Open GL
|
||||
'--device=dri',
|
||||
// Audio output
|
||||
'--socket=pulseaudio',
|
||||
// Read/write home directory access
|
||||
'--filesystem=home',
|
||||
// Allow communication with network
|
||||
'--share=network',
|
||||
// System notifications with libnotify
|
||||
'--talk-name=org.freedesktop.Notifications',
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
].filter(Boolean);
|
||||
|
||||
/**
|
||||
@@ -198,6 +201,7 @@ export default {
|
||||
},
|
||||
],
|
||||
executableName: productName,
|
||||
ignore: [/\.map$/],
|
||||
asar: true,
|
||||
extendInfo: {
|
||||
NSAudioCaptureUsageDescription:
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
"builder-util-runtime": "^9.5.0",
|
||||
"cross-env": "^10.1.0",
|
||||
"debug": "^4.4.0",
|
||||
"electron": "^35.0.0",
|
||||
"electron": "^36.0.0",
|
||||
"electron-log": "^5.4.3",
|
||||
"electron-squirrel-startup": "1.0.1",
|
||||
"electron-window-state": "^5.0.3",
|
||||
|
||||
@@ -63,6 +63,8 @@ const {
|
||||
default: process.platform,
|
||||
},
|
||||
},
|
||||
allowPositionals: true,
|
||||
strict: false,
|
||||
});
|
||||
|
||||
log(`parsed args: arch=${arch}, platform=${platform}`);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
export const mainHost = '.';
|
||||
export const anotherHost = 'another-host';
|
||||
|
||||
export const mainWindowOrigin = `file://${mainHost}`;
|
||||
export const anotherOrigin = `file://${anotherHost}`;
|
||||
export const mainWindowOrigin = `assets://${mainHost}`;
|
||||
export const anotherOrigin = `assets://${anotherHost}`;
|
||||
|
||||
export const onboardingViewUrl = `${mainWindowOrigin}/onboarding`;
|
||||
export const shellViewUrl = `${mainWindowOrigin}/shell.html`;
|
||||
|
||||
@@ -12,16 +12,15 @@ protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: 'assets',
|
||||
privileges: {
|
||||
secure: false,
|
||||
secure: true,
|
||||
allowServiceWorkers: true,
|
||||
corsEnabled: true,
|
||||
supportFetchAPI: true,
|
||||
standard: true,
|
||||
bypassCSP: true,
|
||||
stream: true,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
protocol.registerSchemesAsPrivileged([
|
||||
{
|
||||
scheme: 'file',
|
||||
privileges: {
|
||||
@@ -36,6 +35,17 @@ protocol.registerSchemesAsPrivileged([
|
||||
]);
|
||||
|
||||
const webStaticDir = join(resourcesPath, 'web-static');
|
||||
const localWhiteListDirs = [
|
||||
path.resolve(app.getPath('sessionData')).toLowerCase(),
|
||||
path.resolve(app.getPath('temp')).toLowerCase(),
|
||||
];
|
||||
|
||||
function isPathInWhiteList(filepath: string) {
|
||||
const lowerFilePath = filepath.toLowerCase();
|
||||
return localWhiteListDirs.some(whitelistDir =>
|
||||
lowerFilePath.startsWith(whitelistDir)
|
||||
);
|
||||
}
|
||||
|
||||
async function handleFileRequest(request: Request) {
|
||||
const urlObject = new URL(request.url);
|
||||
@@ -45,11 +55,14 @@ async function handleFileRequest(request: Request) {
|
||||
}
|
||||
|
||||
const isAbsolutePath = urlObject.host !== '.';
|
||||
const isFontRequest =
|
||||
urlObject.pathname &&
|
||||
/\.(woff2?|ttf|otf)$/i.test(urlObject.pathname.split('?')[0] ?? '');
|
||||
|
||||
// Redirect to webpack dev server if defined
|
||||
if (process.env.DEV_SERVER_URL && !isAbsolutePath) {
|
||||
if (process.env.DEV_SERVER_URL && !isAbsolutePath && !isFontRequest) {
|
||||
const devServerUrl = new URL(
|
||||
urlObject.pathname,
|
||||
`${urlObject.pathname}${urlObject.search}`,
|
||||
process.env.DEV_SERVER_URL
|
||||
);
|
||||
return net.fetch(devServerUrl.toString(), request);
|
||||
@@ -83,14 +96,7 @@ async function handleFileRequest(request: Request) {
|
||||
filepath = path.resolve(filepath.replace(/^\//, ''));
|
||||
}
|
||||
// security check if the filepath is within app.getPath('sessionData')
|
||||
const sessionDataPath = path
|
||||
.resolve(app.getPath('sessionData'))
|
||||
.toLowerCase();
|
||||
const tempPath = path.resolve(app.getPath('temp')).toLowerCase();
|
||||
if (
|
||||
!filepath.toLowerCase().startsWith(sessionDataPath) &&
|
||||
!filepath.toLowerCase().startsWith(tempPath)
|
||||
) {
|
||||
if (urlObject.host !== 'local-file' || !isPathInWhiteList(filepath)) {
|
||||
throw new Error('Invalid filepath');
|
||||
}
|
||||
}
|
||||
@@ -102,7 +108,20 @@ async function handleFileRequest(request: Request) {
|
||||
const corsWhitelist = [
|
||||
/^(?:[a-zA-Z0-9-]+\.)*googlevideo\.com$/,
|
||||
/^(?:[a-zA-Z0-9-]+\.)*youtube\.com$/,
|
||||
/^(?:[a-zA-Z0-9-]+\.)*youtube-nocookie\.com$/,
|
||||
/^(?:[a-zA-Z0-9-]+\.)*gstatic\.com$/,
|
||||
/^(?:[a-zA-Z0-9-]+\.)*googleapis\.com$/,
|
||||
/^localhost(?::\d+)?$/,
|
||||
/^127\.0\.0\.1(?::\d+)?$/,
|
||||
/^insider\.affine\.pro$/,
|
||||
/^app\.affine\.pro$/,
|
||||
];
|
||||
const needRefererDomains = [
|
||||
/^(?:[a-zA-Z0-9-]+\.)*youtube\.com$/,
|
||||
/^(?:[a-zA-Z0-9-]+\.)*youtube-nocookie\.com$/,
|
||||
/^(?:[a-zA-Z0-9-]+\.)*googlevideo\.com$/,
|
||||
];
|
||||
const defaultReferer = 'https://client.affine.local/';
|
||||
|
||||
export function registerProtocol() {
|
||||
protocol.handle('file', request => {
|
||||
@@ -159,6 +178,31 @@ export function registerProtocol() {
|
||||
delete responseHeaders['access-control-allow-headers'];
|
||||
delete responseHeaders['Access-Control-Allow-Origin'];
|
||||
delete responseHeaders['Access-Control-Allow-Headers'];
|
||||
} else if (
|
||||
!needRefererDomains.some(domainRegex => domainRegex.test(hostname))
|
||||
) {
|
||||
if (
|
||||
!responseHeaders['access-control-allow-origin'] &&
|
||||
!responseHeaders['Access-Control-Allow-Origin']
|
||||
) {
|
||||
responseHeaders['Access-Control-Allow-Origin'] = ['*'];
|
||||
}
|
||||
if (
|
||||
!responseHeaders['access-control-allow-headers'] &&
|
||||
!responseHeaders['Access-Control-Allow-Headers']
|
||||
) {
|
||||
responseHeaders['Access-Control-Allow-Headers'] = [
|
||||
'Origin, X-Requested-With, Content-Type, Accept, Authorization',
|
||||
];
|
||||
}
|
||||
if (
|
||||
!responseHeaders['access-control-allow-methods'] &&
|
||||
!responseHeaders['Access-Control-Allow-Methods']
|
||||
) {
|
||||
responseHeaders['Access-Control-Allow-Methods'] = [
|
||||
'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// to allow url embedding, remove "x-frame-options",
|
||||
@@ -217,7 +261,7 @@ export function registerProtocol() {
|
||||
const url = new URL(details.url);
|
||||
|
||||
(async () => {
|
||||
// session cookies are set to file:// on production
|
||||
// session cookies are set to assets:// on production
|
||||
// if sending request to the cloud, attach the session cookie (to affine cloud server)
|
||||
if (
|
||||
url.protocol === 'http:' ||
|
||||
@@ -235,6 +279,14 @@ export function registerProtocol() {
|
||||
delete details.requestHeaders['cookie'];
|
||||
details.requestHeaders['Cookie'] = cookieString;
|
||||
}
|
||||
|
||||
const hostname = url.hostname;
|
||||
const needReferer = needRefererDomains.some(regex =>
|
||||
regex.test(hostname)
|
||||
);
|
||||
if (needReferer && !details.requestHeaders['Referer']) {
|
||||
details.requestHeaders['Referer'] = defaultReferer;
|
||||
}
|
||||
})()
|
||||
.catch(err => {
|
||||
logger.error('error handling before send headers', err);
|
||||
|
||||
@@ -686,6 +686,22 @@ export async function getRawAudioBuffers(
|
||||
};
|
||||
}
|
||||
|
||||
function assertRecordingFilepath(filepath: string) {
|
||||
const normalizedPath = path.normalize(filepath);
|
||||
const normalizedBase = path.normalize(SAVED_RECORDINGS_DIR + path.sep);
|
||||
|
||||
if (!normalizedPath.toLowerCase().startsWith(normalizedBase.toLowerCase())) {
|
||||
throw new Error('Invalid recording filepath');
|
||||
}
|
||||
|
||||
return normalizedPath;
|
||||
}
|
||||
|
||||
export async function readRecordingFile(filepath: string) {
|
||||
const normalizedPath = assertRecordingFilepath(filepath);
|
||||
return fsp.readFile(normalizedPath);
|
||||
}
|
||||
|
||||
export async function readyRecording(id: number, buffer: Buffer) {
|
||||
logger.info('readyRecording', id);
|
||||
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
handleBlockCreationFailed,
|
||||
handleBlockCreationSuccess,
|
||||
pauseRecording,
|
||||
readRecordingFile,
|
||||
readyRecording,
|
||||
recordingStatus$,
|
||||
removeRecording,
|
||||
@@ -53,6 +54,9 @@ export const recordingHandlers = {
|
||||
getRawAudioBuffers: async (_, id: number, cursor?: number) => {
|
||||
return getRawAudioBuffers(id, cursor);
|
||||
},
|
||||
readRecordingFile: async (_, filepath: string) => {
|
||||
return readRecordingFile(filepath);
|
||||
},
|
||||
// save the encoded recording buffer to the file system
|
||||
readyRecording: async (_, id: number, buffer: Uint8Array) => {
|
||||
return readyRecording(id, Buffer.from(buffer));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { app } from 'electron';
|
||||
|
||||
import { anotherHost, mainHost } from './constants';
|
||||
import { openExternalSafely } from './security/open-external';
|
||||
|
||||
const extractRedirectTarget = (rawUrl: string) => {
|
||||
@@ -33,7 +34,20 @@ const extractRedirectTarget = (rawUrl: string) => {
|
||||
|
||||
app.on('web-contents-created', (_, contents) => {
|
||||
const isInternalUrl = (url: string) => {
|
||||
return url.startsWith('file://.');
|
||||
try {
|
||||
const parsed = new URL(url);
|
||||
if (
|
||||
parsed.protocol === 'assets:' &&
|
||||
(parsed.hostname === mainHost || parsed.hostname === anotherHost)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
if (parsed.protocol === 'file:' && parsed.hostname === mainHost) {
|
||||
// legacy allowance for older file:// loads
|
||||
return true;
|
||||
}
|
||||
} catch {}
|
||||
return false;
|
||||
};
|
||||
/**
|
||||
* Block navigation to origins not on the allowlist.
|
||||
|
||||
Reference in New Issue
Block a user