Skip to content

Commit

Permalink
Allow running builtin php validation with a relative path configured …
Browse files Browse the repository at this point in the history
…to the php binary.

This was previously allowed, but the fix to use 'which' was too strict
  • Loading branch information
roblourens committed Apr 21, 2021
1 parent 609e717 commit 6437567
Showing 1 changed file with 93 additions and 62 deletions.
155 changes: 93 additions & 62 deletions extensions/php-language-features/src/features/validationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,42 +84,31 @@ namespace RunTrigger {
};
}

async function getPhpPath(): Promise<string | undefined> {
try {
return await which('php');
} catch (e) {
return undefined;
}
}

export default class PHPValidationProvider {

private static MatchExpression: RegExp = /(?:(?:Parse|Fatal) error): (.*)(?: in )(.*?)(?: on line )(\d+)/;
private static BufferArgs: string[] = ['-l', '-n', '-d', 'display_errors=On', '-d', 'log_errors=Off'];
private static FileArgs: string[] = ['-l', '-n', '-d', 'display_errors=On', '-d', 'log_errors=Off', '-f'];

private validationEnabled: boolean;
private executableIsUserDefined: boolean | undefined;
private executable: string | undefined;
private trigger: RunTrigger;
private pauseValidation: boolean;
private config: IPhpConfig | undefined;
private loadConfigP: Promise<void>;

private documentListener: vscode.Disposable | null = null;
private diagnosticCollection?: vscode.DiagnosticCollection;
private delayers?: { [key: string]: ThrottledDelayer<void> };

constructor(private workspaceStore: vscode.Memento) {
this.executable = undefined;
this.validationEnabled = true;
this.trigger = RunTrigger.onSave;
this.pauseValidation = false;
this.loadConfigP = this.loadConfiguration();
}

public activate(subscriptions: vscode.Disposable[]) {
this.diagnosticCollection = vscode.languages.createDiagnosticCollection();
subscriptions.push(this);
vscode.workspace.onDidChangeConfiguration(this.loadConfiguration, this, subscriptions);
this.loadConfiguration();
subscriptions.push(vscode.workspace.onDidChangeConfiguration(() => this.loadConfigP = this.loadConfiguration()));

vscode.workspace.onDidOpenTextDocument(this.triggerValidate, this, subscriptions);
vscode.workspace.onDidCloseTextDocument((textDocument) => {
Expand All @@ -140,38 +129,28 @@ export default class PHPValidationProvider {
}
}

private loadConfiguration(): void {
let section = vscode.workspace.getConfiguration();
let oldExecutable = this.executable;
if (section) {
this.validationEnabled = section.get<boolean>(Setting.Enable, true);
let inspect = section.inspect<string>(Setting.ExecutablePath);
if (inspect && inspect.workspaceValue) {
this.executable = inspect.workspaceValue;
this.executableIsUserDefined = false;
} else if (inspect && inspect.globalValue) {
this.executable = inspect.globalValue;
this.executableIsUserDefined = true;
} else {
this.executable = undefined;
this.executableIsUserDefined = undefined;
}
this.trigger = RunTrigger.from(section.get<string>(Setting.Run, RunTrigger.strings.onSave));
}
if (this.executableIsUserDefined !== true && this.workspaceStore.get<string | undefined>(Setting.CheckedExecutablePath, undefined) !== undefined) {
private async loadConfiguration(): Promise<void> {
const section = vscode.workspace.getConfiguration();
const oldExecutable = this.config?.executable;
this.validationEnabled = section.get<boolean>(Setting.Enable, true);

this.config = await getConfig();

if (this.config.executableIsUserDefined !== true && this.workspaceStore.get<string | undefined>(Setting.CheckedExecutablePath, undefined) !== undefined) {
vscode.commands.executeCommand('setContext', 'php.untrustValidationExecutableContext', true);
}

this.delayers = Object.create(null);
if (this.pauseValidation) {
this.pauseValidation = oldExecutable === this.executable;
this.pauseValidation = oldExecutable === this.config.executable;
}
if (this.documentListener) {
this.documentListener.dispose();
this.documentListener = null;
}
this.diagnosticCollection!.clear();
if (this.validationEnabled) {
if (this.trigger === RunTrigger.onType) {
if (this.config.trigger === RunTrigger.onType) {
this.documentListener = vscode.workspace.onDidChangeTextDocument((e) => {
this.triggerValidate(e.document);
});
Expand All @@ -188,7 +167,8 @@ export default class PHPValidationProvider {
vscode.commands.executeCommand('setContext', 'php.untrustValidationExecutableContext', false);
}

private triggerValidate(textDocument: vscode.TextDocument): void {
private async triggerValidate(textDocument: vscode.TextDocument): Promise<void> {
await this.loadConfigP;
if (textDocument.languageId !== 'php' || this.pauseValidation || !this.validationEnabled) {
return;
}
Expand All @@ -201,17 +181,17 @@ export default class PHPValidationProvider {
let key = textDocument.uri.toString();
let delayer = this.delayers![key];
if (!delayer) {
delayer = new ThrottledDelayer<void>(this.trigger === RunTrigger.onType ? 250 : 0);
delayer = new ThrottledDelayer<void>(this.config?.trigger === RunTrigger.onType ? 250 : 0);
this.delayers![key] = delayer;
}
delayer.trigger(() => this.doValidate(textDocument));
};

if (this.executableIsUserDefined !== undefined && !this.executableIsUserDefined) {
let checkedExecutablePath = this.workspaceStore.get<string | undefined>(Setting.CheckedExecutablePath, undefined);
if (!checkedExecutablePath || checkedExecutablePath !== this.executable) {
vscode.window.showInformationMessage<MessageItem>(
localize('php.useExecutablePath', 'Do you allow {0} (defined as a workspace setting) to be executed to lint PHP files?', this.executable),
if (this.config!.executableIsUserDefined !== undefined && !this.config!.executableIsUserDefined) {
const checkedExecutablePath = this.workspaceStore.get<string | undefined>(Setting.CheckedExecutablePath, undefined);
if (!checkedExecutablePath || checkedExecutablePath !== this.config!.executable) {
const selected = await vscode.window.showInformationMessage<MessageItem>(
localize('php.useExecutablePath', 'Do you allow {0} (defined as a workspace setting) to be executed to lint PHP files?', this.config!.executable),
{
title: localize('php.yes', 'Allow'),
id: 'yes'
Expand All @@ -221,35 +201,36 @@ export default class PHPValidationProvider {
isCloseAffordance: true,
id: 'no'
}
).then(selected => {
if (!selected || selected.id === 'no') {
this.pauseValidation = true;
} else if (selected.id === 'yes') {
this.workspaceStore.update(Setting.CheckedExecutablePath, this.executable);
vscode.commands.executeCommand('setContext', 'php.untrustValidationExecutableContext', true);
trigger();
}
});
);

if (!selected || selected.id === 'no') {
this.pauseValidation = true;
} else if (selected.id === 'yes') {
this.workspaceStore.update(Setting.CheckedExecutablePath, this.config!.executable);
vscode.commands.executeCommand('setContext', 'php.untrustValidationExecutableContext', true);
trigger();
}

return;
}
}

trigger();
}

private doValidate(textDocument: vscode.TextDocument): Promise<void> {
return new Promise<void>(async (resolve) => {
if (this.executable && !path.isAbsolute(this.executable)) {
this.showErrorMessage(localize('phpExecutableNotAbsolute', 'Cannot validate since the setting \'php.validate.executablePath\' must be an absolute path.'));
const executable = this.config!.executable;
if (!executable) {
this.showErrorMessage(localize('noPhp', 'Cannot validate since a PHP installation could not be found. Use the setting \'php.validate.executablePath\' to configure the PHP executable.'));
this.pauseValidation = true;
resolve();
return;
}

const executable = this.executable || await getPhpPath();
if (!executable) {
this.showErrorMessage(localize('noPhp', 'Cannot validate since a PHP installation could not be found. Use the setting \'php.validate.executablePath\' to configure the PHP executable.'));
this.pauseValidation = true;
resolve();
if (!path.isAbsolute(executable)) {
// executable should either be resolved to an absolute path or undefined.
// This is just to be sure.
return;
}

Expand All @@ -270,7 +251,7 @@ export default class PHPValidationProvider {

let options = (vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0]) ? { cwd: vscode.workspace.workspaceFolders[0].uri.fsPath } : undefined;
let args: string[];
if (this.trigger === RunTrigger.onSave) {
if (this.config!.trigger === RunTrigger.onSave) {
args = PHPValidationProvider.FileArgs.slice(0);
args.push(textDocument.fileName);
} else {
Expand All @@ -288,7 +269,7 @@ export default class PHPValidationProvider {
resolve();
});
if (childProcess.pid) {
if (this.trigger === RunTrigger.onType) {
if (this.config!.trigger === RunTrigger.onType) {
childProcess.stdin.write(textDocument.getText());
childProcess.stdin.end();
}
Expand All @@ -315,7 +296,7 @@ export default class PHPValidationProvider {
private async showError(error: any, executable: string): Promise<void> {
let message: string | null = null;
if (error.code === 'ENOENT') {
if (this.executable) {
if (this.config!.executable) {
message = localize('wrongExecutable', 'Cannot validate since {0} is not a valid php executable. Use the setting \'php.validate.executablePath\' to configure the PHP executable.', executable);
} else {
message = localize('noExecutable', 'Cannot validate since no PHP executable is set. Use the setting \'php.validate.executablePath\' to configure the PHP executable.');
Expand All @@ -337,3 +318,53 @@ export default class PHPValidationProvider {
}
}
}

interface IPhpConfig {
readonly executable: string | undefined;
readonly executableIsUserDefined: boolean | undefined;
readonly trigger: RunTrigger;
}

async function getConfig(): Promise<IPhpConfig> {
const section = vscode.workspace.getConfiguration();

let executable: string | undefined;
let executableIsUserDefined: boolean | undefined;
const inspect = section.inspect<string>(Setting.ExecutablePath);
if (inspect && inspect.workspaceValue) {
executable = inspect.workspaceValue;
executableIsUserDefined = false;
} else if (inspect && inspect.globalValue) {
executable = inspect.globalValue;
executableIsUserDefined = true;
} else {
executable = undefined;
executableIsUserDefined = undefined;
}

if (executable && !path.isAbsolute(executable)) {
const first = vscode.workspace.workspaceFolders && vscode.workspace.workspaceFolders[0];
if (first) {
executable = vscode.Uri.joinPath(first.uri, executable).fsPath;
} else {
executable = undefined;
}
} else if (!executable) {
executable = await getPhpPath();
}

const trigger = RunTrigger.from(section.get<string>(Setting.Run, RunTrigger.strings.onSave));
return {
executable,
executableIsUserDefined,
trigger
};
}

async function getPhpPath(): Promise<string | undefined> {
try {
return await which('php');
} catch (e) {
return undefined;
}
}

0 comments on commit 6437567

Please sign in to comment.