config_preview_deploy-1.0.0-alpha3/src/ConfigVerifier.php

src/ConfigVerifier.php
<?php

declare(strict_types=1);

namespace Drupal\config_preview_deploy;

use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Datetime\DateFormatterInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\TempStore\PrivateTempStoreFactory;
use GuzzleHttp\ClientInterface;

/**
 * Verifies configuration state between environments.
 */
class ConfigVerifier {

  use StringTranslationTrait;

  /**
   * The HTTP client.
   */
  protected ClientInterface $httpClient;

  /**
   * The state service.
   */
  protected StateInterface $state;

  /**
   * Logger channel.
   */
  protected LoggerChannelInterface $logger;

  /**
   * The configuration factory.
   */
  protected ConfigFactoryInterface $configFactory;

  /**
   * The date formatter service.
   */
  protected DateFormatterInterface $dateFormatter;


  /**
   * The private tempstore factory.
   */
  protected PrivateTempStoreFactory $privateTempStoreFactory;

  /**
   * The hash verification service.
   *
   * @var \Drupal\config_preview_deploy\HashVerification
   */
  protected HashVerification $hashVerification;

  /**
   * Constructs a ConfigVerifier object.
   */
  public function __construct(
    ClientInterface $httpClient,
    StateInterface $state,
    LoggerChannelFactoryInterface $loggerFactory,
    ConfigFactoryInterface $configFactory,
    DateFormatterInterface $dateFormatter,
    PrivateTempStoreFactory $privateTempStoreFactory,
    HashVerification $hashVerification,
  ) {
    $this->httpClient = $httpClient;
    $this->state = $state;
    $this->configFactory = $configFactory;
    $this->logger = $loggerFactory->get('config_preview_deploy');
    $this->dateFormatter = $dateFormatter;
    $this->privateTempStoreFactory = $privateTempStoreFactory;
    $this->hashVerification = $hashVerification;
  }

  /**
   * Validates incoming verification request on production.
   *
   * This method is called on the production environment to validate
   * verification requests from preview environments.
   *
   * @param array $request
   *   The verification request data.
   *
   * @return array
   *   Validation result with verification data.
   *
   * @throws \RuntimeException
   *   When authentication fails.
   */
  public function validateVerificationRequest(array $request): array {
    // Verify authentication hash including timestamp.
    $baseTimestamp = $request['base_timestamp'] ?? 0;

    if (!isset($request['auth_hash']) || !$this->hashVerification->verifyHash($request['auth_hash'], $baseTimestamp)) {
      $this->logger->warning('Invalid authentication in verification request from @env', [
        '@env' => $request['environment'] ?? 'unknown',
      ]);
      throw new \RuntimeException($this->t('Invalid authentication')->render());
    }

    $lastChange = $this->state->get('config_preview_deploy.last_change', 0);
    $environment = $request['environment'] ?? 'unknown';

    // Deployment is valid if production hasn't changed since the base
    // timestamp.
    $isValid = ($lastChange <= $baseTimestamp);

    $this->logger->debug('Verification request from @env: @status (prod: @prod_time, base: @base_time)', [
      '@env' => $environment,
      '@status' => $isValid ? 'valid' : 'invalid',
      '@prod_time' => date('Y-m-d H:i:s', $lastChange),
      '@base_time' => date('Y-m-d H:i:s', $baseTimestamp),
    ]);

    return [
      'current_timestamp' => $lastChange,
      'last_deployed_from' => $this->state->get('config_preview_deploy.last_deployed_from', ''),
      'valid' => $isValid,
    ];
  }

  /**
   * Updates production's last change timestamp.
   *
   * This should be called after successful configuration deployment.
   *
   * @param string $environment
   *   The environment that deployed the configuration.
   */
  public function updateLastChange(string $environment): void {
    $timestamp = time();
    $this->state->set('config_preview_deploy.last_change', $timestamp);
    $this->state->set('config_preview_deploy.last_deployed_from', $environment);
  }

  /**
   * Gets the current environment name.
   *
   * @return string
   *   The environment name.
   */
  public function getEnvironmentName(): string {
    // Try common environment variables used by hosting platforms.
    $envVars = [
      'PHAPP_ENV',
      'LAGOON_ENVIRONMENT',
      'PLATFORM_BRANCH',
      'PANTHEON_ENVIRONMENT',
      'ACQUIA_ENVIRONMENT',
      'CI_ENVIRONMENT_SLUG',
    ];

    foreach ($envVars as $var) {
      $value = getenv($var);
      if ($value !== FALSE && !empty($value)) {
        return $value;
      }
    }

    // Fallback to hostname or 'local'.
    return gethostname() ?: 'local';
  }

  /**
   * Deploys configuration diff to production.
   *
   * @param string $diff
   *   The configuration diff to deploy.
   *
   * @return array
   *   Response data from production deployment.
   *
   * @throws \RuntimeException
   *   When deployment fails or configuration is invalid.
   */
  public function deployToProduction(string $diff): array {
    $config = $this->configFactory->get('config_preview_deploy.settings');
    $productionUrl = $config->get('production_url');

    if (!$productionUrl) {
      throw new \RuntimeException($this->t('Production URL not configured.')->render());
    }

    // Get OAuth access token from tempstore.
    $tempstore = $this->privateTempStoreFactory->get('config_preview_deploy');
    $access_token = $tempstore->get('access_token');
    $token_type = $tempstore->get('token_type') ?? 'Bearer';

    if (!$access_token) {
      throw new \RuntimeException($this->t('No valid OAuth token found. Please authorize with production first.')->render());
    }

    // Create authentication hash including timestamp.
    $productionHost = parse_url($productionUrl, PHP_URL_HOST);
    $timestamp = time();
    $authHash = $this->hashVerification->generateVerificationHash($productionHost, $timestamp);

    $environment = $this->getEnvironmentName();
    $deployUrl = rtrim($productionUrl, '/') . '/admin/config/development/config-preview-deploy/deploy-endpoint';

    try {
      $response = $this->httpClient->post($deployUrl, [
        'json' => [
          'diff' => $diff,
          'environment' => $environment,
          'auth_hash' => $authHash,
          'timestamp' => $timestamp,
        ],
        'timeout' => 60,
        'headers' => [
          'Content-Type' => 'application/json',
          'Accept' => 'application/json',
          'Authorization' => $token_type . ' ' . $access_token,
        ],
      ]);

      $data = json_decode($response->getBody()->getContents(), TRUE);

      if (!($data['success'] ?? FALSE)) {
        throw new \RuntimeException($this->t('Deployment failed: @error', [
          '@error' => $data['error'] ?? 'Unknown deployment error',
        ])->render());
      }

      // Update local state to reflect successful deployment.
      $this->updateLastChange($environment);

      return $data;

    }
    catch (\Exception $e) {
      $this->logger->error('Failed to deploy to production: @message', [
        '@message' => $e->getMessage(),
      ]);
      throw new \RuntimeException($this->t('Failed to deploy to production: @message', ['@message' => $e->getMessage()])->render());
    }
  }

}

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

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