ai_upgrade_assistant-0.2.0-alpha2/src/Service/PatchValidator.php
src/Service/PatchValidator.php
<?php
namespace Drupal\ai_upgrade_assistant\Service;
use Drupal\Core\File\FileSystemInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Symfony\Component\Process\Process;
use Drupal\Core\Config\ConfigFactoryInterface;
/**
* Service for validating patches before applying them.
*/
class PatchValidator {
/**
* The file system service.
*
* @var \Drupal\Core\File\FileSystemInterface
*/
protected $fileSystem;
/**
* The logger factory.
*
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
*/
protected $loggerFactory;
/**
* The config factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The PHP parser service.
*
* @var \Drupal\ai_upgrade_assistant\Service\PhpParserService
*/
protected $phpParser;
/**
* Constructs a new PatchValidator.
*
* @param \Drupal\Core\File\FileSystemInterface $file_system
* The file system service.
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
* The logger factory service.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The config factory.
* @param \Drupal\ai_upgrade_assistant\Service\PhpParserService $php_parser
* The PHP parser service.
*/
public function __construct(
FileSystemInterface $file_system,
LoggerChannelFactoryInterface $logger_factory,
ConfigFactoryInterface $config_factory,
PhpParserService $php_parser
) {
$this->fileSystem = $file_system;
$this->loggerFactory = $logger_factory;
$this->configFactory = $config_factory;
$this->phpParser = $php_parser;
}
/**
* Validates a patch file.
*
* @param string $patch_file
* Path to the patch file.
* @param string $module_path
* Path to the module directory.
*
* @return array
* Validation results containing success status and any issues found.
*/
public function validatePatch($patch_file, $module_path) {
$results = [
'status' => TRUE,
'messages' => [],
'errors' => [],
'warnings' => [],
];
// Check if patch file exists
if (!file_exists($patch_file)) {
$results['status'] = FALSE;
$results['errors'][] = "Patch file not found: $patch_file";
return $results;
}
// Validate patch format
if (!$this->validatePatchFormat($patch_file)) {
$results['status'] = FALSE;
$results['errors'][] = 'Invalid patch format';
return $results;
}
// Check if patch can be applied
$dry_run_result = $this->dryRunPatch($patch_file, $module_path);
if (!$dry_run_result['status']) {
$results['status'] = FALSE;
$results['errors'] = array_merge($results['errors'], $dry_run_result['errors']);
return $results;
}
// Validate PHP syntax of modified files
$syntax_results = $this->validatePhpSyntax($patch_file, $module_path);
if (!$syntax_results['status']) {
$results['status'] = FALSE;
$results['errors'] = array_merge($results['errors'], $syntax_results['errors']);
}
// Check for potential security issues
$security_results = $this->validateSecurity($patch_file);
if (!$security_results['status']) {
$results['status'] = FALSE;
$results['errors'] = array_merge($results['errors'], $security_results['errors']);
}
$results['warnings'] = array_merge($results['warnings'], $security_results['warnings']);
// Check Drupal coding standards
$coding_standards_results = $this->validateCodingStandards($patch_file);
if (!$coding_standards_results['status']) {
$results['warnings'] = array_merge($results['warnings'], $coding_standards_results['warnings']);
}
return $results;
}
/**
* Validates the format of a patch file.
*
* @param string $patch_file
* Path to the patch file.
*
* @return bool
* TRUE if the patch format is valid, FALSE otherwise.
*/
protected function validatePatchFormat($patch_file) {
$content = file_get_contents($patch_file);
if ($content === FALSE) {
return FALSE;
}
// Check for unified diff format
if (!preg_match('/^--- .*$/m', $content) || !preg_match('/^\+\+\+ .*$/m', $content)) {
return FALSE;
}
// Check for chunk headers
if (!preg_match('/^@@ -\d+,\d+ \+\d+,\d+ @@/m', $content)) {
return FALSE;
}
return TRUE;
}
/**
* Performs a dry run of patch application.
*
* @param string $patch_file
* Path to the patch file.
* @param string $module_path
* Path to the module directory.
*
* @return array
* Results of the dry run.
*/
protected function dryRunPatch($patch_file, $module_path) {
$results = [
'status' => TRUE,
'errors' => [],
];
$process = new Process(['git', 'apply', '--check', $patch_file], $module_path);
$process->run();
if (!$process->isSuccessful()) {
$results['status'] = FALSE;
$results['errors'][] = 'Patch cannot be applied cleanly: ' . $process->getErrorOutput();
}
return $results;
}
/**
* Validates PHP syntax of modified files.
*
* @param string $patch_file
* Path to the patch file.
* @param string $module_path
* Path to the module directory.
*
* @return array
* Results of the syntax validation.
*/
protected function validatePhpSyntax($patch_file, $module_path) {
$results = [
'status' => TRUE,
'errors' => [],
];
// Create a temporary directory for validation
$temp_dir = $this->fileSystem->getTempDirectory() . '/patch_validation_' . uniqid();
$this->fileSystem->mkdir($temp_dir);
try {
// Copy module files to temp directory
$this->fileSystem->copyDirectory($module_path, $temp_dir);
// Apply patch in temp directory
$process = new Process(['git', 'apply', $patch_file], $temp_dir);
$process->run();
if (!$process->isSuccessful()) {
throw new \Exception('Failed to apply patch in temporary directory');
}
// Find modified PHP files
$modified_files = $this->getModifiedPhpFiles($patch_file);
// Check syntax of each modified file
foreach ($modified_files as $file) {
$temp_file = $temp_dir . '/' . $file;
if (file_exists($temp_file)) {
// Use PHP Parser to validate syntax
$ast = $this->phpParser->parseFile($temp_file);
if ($ast === NULL) {
$results['status'] = FALSE;
$results['errors'][] = "PHP syntax error in $file";
}
}
}
}
catch (\Exception $e) {
$results['status'] = FALSE;
$results['errors'][] = $e->getMessage();
}
finally {
// Clean up temporary directory
$this->fileSystem->deleteRecursive($temp_dir);
}
return $results;
}
/**
* Validates security implications of the patch.
*
* @param string $patch_file
* Path to the patch file.
*
* @return array
* Results of the security validation.
*/
protected function validateSecurity($patch_file) {
$results = [
'status' => TRUE,
'errors' => [],
'warnings' => [],
];
$content = file_get_contents($patch_file);
if ($content === FALSE) {
$results['status'] = FALSE;
$results['errors'][] = 'Could not read patch file';
return $results;
}
// Check for potentially dangerous patterns
$patterns = [
'eval\s*\(' => 'Use of eval() is discouraged for security reasons',
'exec\s*\(' => 'Use of exec() is discouraged for security reasons',
'shell_exec\s*\(' => 'Use of shell_exec() is discouraged for security reasons',
'system\s*\(' => 'Use of system() is discouraged for security reasons',
'passthru\s*\(' => 'Use of passthru() is discouraged for security reasons',
'\$_GET\[' => 'Direct use of $_GET without proper sanitization',
'\$_POST\[' => 'Direct use of $_POST without proper sanitization',
'\$_REQUEST\[' => 'Direct use of $_REQUEST without proper sanitization',
'mysql_' => 'Use of deprecated mysql_* functions',
'mysqli_' => 'Direct database access bypassing Drupal database API',
'pg_' => 'Direct database access bypassing Drupal database API',
'file_get_contents\s*\(' => 'Ensure proper validation of file paths',
'file_put_contents\s*\(' => 'Ensure proper validation of file paths',
'unserialize\s*\(' => 'Use of unserialize() may be dangerous',
];
foreach ($patterns as $pattern => $warning) {
if (preg_match('/' . $pattern . '/i', $content)) {
$results['warnings'][] = $warning;
}
}
return $results;
}
/**
* Validates Drupal coding standards compliance.
*
* @param string $patch_file
* Path to the patch file.
*
* @return array
* Results of the coding standards validation.
*/
protected function validateCodingStandards($patch_file) {
$results = [
'status' => TRUE,
'warnings' => [],
];
$content = file_get_contents($patch_file);
if ($content === FALSE) {
$results['status'] = FALSE;
return $results;
}
// Check for common coding standards issues
$patterns = [
'if\s*\([^\s]' => 'Space required after if keyword',
'function\s*[a-zA-Z_]+\s*\([^\s]' => 'Space required after function name',
'\){' => 'Space required between ) and {',
'[\t]' => 'Tab character found, use spaces instead',
'\s+$' => 'Trailing whitespace found',
'{\s*\n\s*\n' => 'Extra blank line after {',
'\n\s*\n\s*}' => 'Extra blank line before }',
];
foreach ($patterns as $pattern => $warning) {
if (preg_match('/' . $pattern . '/m', $content)) {
$results['warnings'][] = $warning;
}
}
return $results;
}
/**
* Gets the list of modified PHP files from a patch.
*
* @param string $patch_file
* Path to the patch file.
*
* @return array
* List of modified PHP files.
*/
protected function getModifiedPhpFiles($patch_file) {
$files = [];
$content = file_get_contents($patch_file);
if ($content === FALSE) {
return $files;
}
// Extract filenames from patch headers
preg_match_all('/^--- (?:a\/)?(.+?)(?:\t|\s)/m', $content, $matches);
foreach ($matches[1] as $file) {
if (pathinfo($file, PATHINFO_EXTENSION) === 'php') {
$files[] = $file;
}
}
return array_unique($files);
}
}
