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);
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc