1
0
mirror of https://github.com/desktop/desktop synced 2024-06-30 22:54:41 +00:00

Compare commits

...

2 Commits

Author SHA1 Message Date
Sergio Padrino
4248e8d64e Fix accessibility of path errors 2024-06-27 17:51:35 +02:00
Sergio Padrino
26105fc89f On macOS, check allow selecting apps 2024-06-27 17:46:28 +02:00
2 changed files with 60 additions and 18 deletions

View File

@ -6,27 +6,22 @@ import { access, stat } from 'fs/promises'
import * as fs from 'fs'
import { InputError } from '../lib/input-description/input-error'
import { IAccessibleMessage } from '../../models/accessible-message'
import { promisify } from 'util'
import { exec } from 'child_process'
// Shells
// - macOS: path/bundleId + params
// - Windows: path + params
// - Linux: path + params
// Editors
// - macOS: path/bundleId + params
// - Windows: path + params + usesShell (if path ends with .cmd)
// - Linux: path + params
const execAsync = promisify(exec)
interface ICustomIntegrationFormProps {
readonly id: string
readonly path: string
readonly params: string
readonly arguments: string
readonly onPathChanged: (path: string) => void
readonly onParamsChanged: (params: string) => void
}
interface ICustomIntegrationFormState {
readonly path: string
readonly params: string
readonly arguments: string
readonly isValidPath: boolean
readonly showNonValidPathWarning: boolean
}
@ -40,7 +35,7 @@ export class CustomIntegrationForm extends React.Component<
this.state = {
path: props.path,
params: props.params,
arguments: props.arguments,
isValidPath: false,
showNonValidPathWarning: false,
}
@ -54,12 +49,13 @@ export class CustomIntegrationForm extends React.Component<
value={this.state.path}
onValueChanged={this.onPathChanged}
placeholder="Path to executable"
ariaDescribedBy={`${this.props.id}-custom-integration-path-error`}
/>
<Button onClick={this.onChoosePath}>Choose</Button>
</div>
{this.renderErrors()}
<TextBox
value={this.state.params}
value={this.state.arguments}
onValueChanged={this.onParamsChanged}
placeholder="Command line arguments"
/>
@ -87,7 +83,7 @@ export class CustomIntegrationForm extends React.Component<
return (
<div className="custom-integration-form-error">
<InputError
id="add-existing-repository-path-error"
id={`${this.props.id}-custom-integration-path-error`}
trackedUserInput={this.state.path}
ariaLiveMessage={msg.screenReaderMessage}
>
@ -118,6 +114,7 @@ export class CustomIntegrationForm extends React.Component<
if (path.length === 0) {
this.setState({
isValidPath: false,
showNonValidPathWarning: true,
})
return
}
@ -127,12 +124,27 @@ export class CustomIntegrationForm extends React.Component<
const canBeExecuted = await access(path, fs.constants.X_OK)
.then(() => true)
.catch(() => false)
const isExecutableFile = pathStat.isFile() && canBeExecuted
// On macOS, not only executable files are valid, but also apps (which are
// directories with a `.app` extension and from which we can retrieve
// the app bundle ID)
let bundleId = null
if (__DARWIN__ && !isExecutableFile && pathStat.isDirectory()) {
bundleId = await this.getBundleId(path)
}
const isValidPath = isExecutableFile || !!bundleId
this.setState({
isValidPath: pathStat.isFile() && canBeExecuted,
isValidPath,
showNonValidPathWarning: true,
})
} catch (e) {
this.setState({
isValidPath: false,
showNonValidPathWarning: true,
})
}
@ -143,8 +155,36 @@ export class CustomIntegrationForm extends React.Component<
this.updatePath(path)
}
// Function to retrieve, on macOS, the bundleId of an app given its path
private getBundleId = async (path: string) => {
try {
// Ensure the path ends with `.app` for applications
if (!path.endsWith('.app')) {
throw new Error(
'The provided path does not point to a macOS application.'
)
}
// Use mdls to query the kMDItemCFBundleIdentifier attribute
const { stdout } = await execAsync(
`mdls -name kMDItemCFBundleIdentifier -raw "${path}"`
)
const bundleId = stdout.trim()
// Check for valid output
if (!bundleId || bundleId === '(null)') {
return null
}
return bundleId
} catch (error) {
console.error('Failed to retrieve bundle ID:', error)
return null
}
}
private onParamsChanged = (params: string) => {
this.setState({ params })
this.setState({ arguments: params })
this.props.onParamsChanged(params)
}
}

View File

@ -126,8 +126,9 @@ export class Integrations extends React.Component<
return (
<Row>
<CustomIntegrationForm
id="custom-editor"
path=""
params=""
arguments=""
onPathChanged={this.onCustomEditorPathChanged}
onParamsChanged={this.onCustomEditorParamsChanged}
/>
@ -165,8 +166,9 @@ export class Integrations extends React.Component<
return (
<Row>
<CustomIntegrationForm
id="custom-shell"
path=""
params=""
arguments=""
onPathChanged={this.onCustomEditorPathChanged}
onParamsChanged={this.onCustomEditorParamsChanged}
/>