mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(browser): let pages use files picked, saved or dropped through File System Access (#32970)
This commit is contained in:
@@ -368,6 +368,88 @@ describe('partition permission policy', () => {
|
||||
expect(check()).toBe(true)
|
||||
})
|
||||
|
||||
it('gives browsing pages File System Access but keeps it from pages another machine can script', async () => {
|
||||
const install = await loadInstaller()
|
||||
const { installBrowserRoutePartitionPolicies } =
|
||||
await import('./browser-session-route-policies')
|
||||
const routed = `persist:orca-browser-v1-${'a'.repeat(64)}`
|
||||
install(profileFor('persist:browsing-5'))
|
||||
void installBrowserRoutePartitionPolicies(profileFor('persist:browsing-5'), routed)
|
||||
const decide = (partition: string) => {
|
||||
const sess = sessionsByPartition.get(partition)
|
||||
if (!sess) {
|
||||
throw new Error(`Expected the session for ${partition}`)
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the installer registered exactly this handler on the fake session.
|
||||
const requestHandler = sess.setPermissionRequestHandler.mock.calls[0]?.[0] as (
|
||||
webContents: { id: number; getURL: () => string },
|
||||
permission: string,
|
||||
callback: (allowed: boolean) => void,
|
||||
details: Partial<Electron.FilesystemPermissionRequest>
|
||||
) => void
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the installer registered exactly this handler on the fake session.
|
||||
const checkHandler = sess.setPermissionCheckHandler.mock.calls[0]?.[0] as (
|
||||
webContents: null,
|
||||
permission: string,
|
||||
requestingOrigin: string,
|
||||
details: Partial<Electron.PermissionCheckHandlerHandlerDetails>
|
||||
) => boolean
|
||||
let requested: boolean | null = null
|
||||
requestHandler(
|
||||
{ id: 11, getURL: () => 'https://app.test/' },
|
||||
'fileSystem',
|
||||
(allowed) => (requested = allowed),
|
||||
{ filePath: '/Users/me/project', isDirectory: true, fileAccessType: 'writable' }
|
||||
)
|
||||
const checked = checkHandler(null, 'fileSystem', 'https://app.test', {
|
||||
filePath: '/Users/me/notes.txt',
|
||||
isDirectory: false,
|
||||
fileAccessType: 'writable'
|
||||
})
|
||||
return { requested, checked }
|
||||
}
|
||||
|
||||
expect(decide('persist:browsing-5')).toEqual({ requested: true, checked: true })
|
||||
expect(mocks.notifyPermissionDenied).not.toHaveBeenCalled()
|
||||
expect(decide(routed)).toEqual({ requested: false, checked: false })
|
||||
expect(mocks.notifyPermissionDenied).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ guestWebContentsId: 11, permission: 'fileSystem' })
|
||||
)
|
||||
})
|
||||
|
||||
it('refuses a blocklisted system folder at once instead of leaving the page waiting', async () => {
|
||||
const install = await loadInstaller()
|
||||
install(profileFor('persist:browsing-6'))
|
||||
install(profileFor('orca-doc-preview'), { permissions: 'deny' })
|
||||
const answer = (partition: string) => {
|
||||
const sess = sessionsByPartition.get(partition)
|
||||
const listener = sess?.on.mock.calls.find(
|
||||
([event]) => event === 'file-system-access-restricted'
|
||||
)?.[1]
|
||||
const actions: string[] = []
|
||||
listener?.(
|
||||
{},
|
||||
{
|
||||
origin: 'https://app.test',
|
||||
isDirectory: true,
|
||||
path: '/Users/me',
|
||||
webContents: { id: 12 }
|
||||
},
|
||||
(action: string) => actions.push(action)
|
||||
)
|
||||
return actions
|
||||
}
|
||||
|
||||
expect(answer('orca-doc-preview')).toEqual(['deny'])
|
||||
expect(mocks.notifyPermissionDenied).not.toHaveBeenCalled()
|
||||
expect(answer('persist:browsing-6')).toEqual(['deny'])
|
||||
expect(mocks.notifyPermissionDenied).toHaveBeenCalledWith({
|
||||
guestWebContentsId: 12,
|
||||
permission: 'fileSystem',
|
||||
rawUrl: 'https://app.test'
|
||||
})
|
||||
})
|
||||
|
||||
it('hands a link to another app to the open-app prompt instead of denying it', async () => {
|
||||
const install = await loadInstaller()
|
||||
install(profileFor('persist:browsing-4'))
|
||||
|
||||
@@ -89,6 +89,34 @@ const handleDeniedWillDownload = (
|
||||
noticeDocPreviewDownloadBlocked(webContents)
|
||||
}
|
||||
|
||||
type RestrictedFileSystemAccessListener = (
|
||||
event: Electron.Event,
|
||||
details: Electron.FileSystemAccessRestrictedDetails,
|
||||
callback: (action: 'allow' | 'deny' | 'tryAgain') => void
|
||||
) => void
|
||||
|
||||
// Chromium waits for an answer on blocklisted system folders; without one the page hangs.
|
||||
const denyRestrictedFileSystemAccess: RestrictedFileSystemAccessListener = (
|
||||
_event,
|
||||
_details,
|
||||
callback
|
||||
) => callback('deny')
|
||||
|
||||
const handleRestrictedFileSystemAccess: RestrictedFileSystemAccessListener = (
|
||||
_event,
|
||||
details,
|
||||
callback
|
||||
) => {
|
||||
callback('deny')
|
||||
if (details.webContents) {
|
||||
browserManager.notifyPermissionDenied({
|
||||
guestWebContentsId: details.webContents.id,
|
||||
permission: 'fileSystem',
|
||||
rawUrl: details.origin
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function resolvePermissionNoticeUrl(
|
||||
webContents: Electron.WebContents,
|
||||
details: Electron.PermissionRequest | undefined
|
||||
@@ -114,6 +142,8 @@ export async function installBrowserSessionPartitionPolicies(
|
||||
downloads?: BrowserPartitionDownloadPolicy
|
||||
permissions?: BrowserPartitionPermissionPolicy
|
||||
applyAppWideProxy?: boolean
|
||||
/** Off where another machine can script pages and reuse their stored file handles. */
|
||||
fileSystemAccess?: boolean
|
||||
userAgentExceptions?: boolean
|
||||
/** Session-only answers are not persisted to a browser profile. */
|
||||
sitePermissions?: 'saved' | 'session'
|
||||
@@ -139,6 +169,12 @@ export async function installBrowserSessionPartitionPolicies(
|
||||
sess.setPermissionCheckHandler(() => false)
|
||||
clearBrowserWebAuthnAccessHandlers(sess)
|
||||
} else {
|
||||
// Electron answers every File System Access grant here, overriding Chromium's own;
|
||||
// the user's pick or drop is the consent, and handles a site stores keep their access.
|
||||
const grantsFileSystem = options.fileSystemAccess !== false
|
||||
const isAutoGranted = (permission: string): boolean =>
|
||||
isAutoGrantedBrowserSessionPermission(permission) ||
|
||||
(grantsFileSystem && permission === 'fileSystem')
|
||||
const siteMemory =
|
||||
options.sitePermissions === 'session'
|
||||
? sessionBrowserSitePermissions(partition)
|
||||
@@ -172,7 +208,7 @@ export async function installBrowserSessionPartitionPolicies(
|
||||
const mediaRequest = 'mediaTypes' in details ? details : undefined
|
||||
const sitePermissions = browserSitePermissionsFor(permission, mediaRequest?.mediaTypes)
|
||||
if (!sitePermissions) {
|
||||
const allowed = isAutoGrantedBrowserSessionPermission(permission)
|
||||
const allowed = isAutoGranted(permission)
|
||||
if (!allowed) {
|
||||
notifyDenied()
|
||||
}
|
||||
@@ -227,7 +263,7 @@ export async function installBrowserSessionPartitionPolicies(
|
||||
(permission !== 'media' || hasSystemMediaAccess(details?.mediaType))
|
||||
)
|
||||
}
|
||||
return isAutoGrantedBrowserSessionPermission(permission)
|
||||
return isAutoGranted(permission)
|
||||
})
|
||||
installBrowserWebAuthnAccessHandlers(sess)
|
||||
}
|
||||
@@ -240,6 +276,14 @@ export async function installBrowserSessionPartitionPolicies(
|
||||
'will-download',
|
||||
options?.downloads === 'deny' ? handleDeniedWillDownload : handleWillDownload
|
||||
)
|
||||
sess.removeListener('file-system-access-restricted', denyRestrictedFileSystemAccess)
|
||||
sess.removeListener('file-system-access-restricted', handleRestrictedFileSystemAccess)
|
||||
sess.on(
|
||||
'file-system-access-restricted',
|
||||
options?.permissions === 'deny'
|
||||
? denyRestrictedFileSystemAccess
|
||||
: handleRestrictedFileSystemAccess
|
||||
)
|
||||
configuredPartitions.add(partition)
|
||||
return proxyReady
|
||||
}
|
||||
@@ -252,6 +296,8 @@ export function clearBrowserSessionPartitionPolicies(partition: string, sess: Se
|
||||
browserManager.removeCertificateRequestGuard(sess)
|
||||
sess.removeListener('will-download', handleWillDownload)
|
||||
sess.removeListener('will-download', handleDeniedWillDownload)
|
||||
sess.removeListener('file-system-access-restricted', denyRestrictedFileSystemAccess)
|
||||
sess.removeListener('file-system-access-restricted', handleRestrictedFileSystemAccess)
|
||||
clearBrowserWebAuthnAccessHandlers(sess)
|
||||
sess.setPermissionRequestHandler(null)
|
||||
sess.setPermissionCheckHandler(null)
|
||||
|
||||
@@ -16,7 +16,7 @@ export function installBrowserRoutePartitionPolicies(
|
||||
}
|
||||
return installBrowserSessionPartitionPolicies(
|
||||
{ ...profile, partition },
|
||||
{ applyAppWideProxy: false, sitePermissions }
|
||||
{ applyAppWideProxy: false, fileSystemAccess: false, sitePermissions }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user