cms_content_sync-3.0.x-dev/src/Cli/CliService.php

src/Cli/CliService.php
<?php

namespace Drupal\cms_content_sync\Cli;

use Drupal\cms_content_sync\Controller\ContentSyncSettings;
use Drupal\cms_content_sync\Controller\FlowPull;
use Drupal\cms_content_sync\Controller\PoolExport;
use Drupal\cms_content_sync\Entity\EntityStatus;
use Drupal\cms_content_sync\Entity\Flow;
use Drupal\cms_content_sync\Entity\Pool;
use Drupal\cms_content_sync\PushIntent;
use Drupal\cms_content_sync\SyncCoreFlowExport;
use Drupal\cms_content_sync\SyncCoreInterface\DrupalApplication;
use Drupal\cms_content_sync\SyncCoreInterface\SyncCoreFactory;
use Drupal\cms_content_sync\SyncCorePoolExport;
use Drupal\cms_content_sync\SyncIntent;
use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Url;
use Drush\Exceptions\UserAbortException;
use EdgeBox\SyncCore\Exception\TimeoutException;
use EdgeBox\SyncCore\Exception\UnauthorizedException;
use Firebase\JWT\JWT;
use Drush\Style\DrushStyle;
use EdgeBox\SyncCore\V2\Raw\Model\RemoteSiteConfigRequestMode;
use EdgeBox\SyncCore\V2\Syndication\MassPush;

/**
 * The Drush CLI Service used by Drush 8 and 9.
 */
class CliService {

  /**
   * Export the configuration to the Sync Core.
   *
   * @param \Drush\Style\DrushStyle $io
   *   The CLI service which allows interoperability.
   * @param array $options
   *   An array containing the option parameters provided by Drush.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \EdgeBox\SyncCore\Exception\SyncCoreException
   * @throws \Exception
   */
  public function configurationExport(DrushStyle $io, array $options) {
    // Check if the site has already been registered.
    $settings = ContentSyncSettings::getInstance();
    if (!$settings->getSiteUuid()) {
      $io->error('The site needs to be registered first, before the configuration can be exported to the sync core.');

      return;
    }

    $io->text('Validating Pools...');
    foreach (Pool::getAll() as $pool) {
      if (!PoolExport::validateBaseUrl($pool)) {
        throw new \Exception('The site does not have a valid base url. The base url of the site can be configured at the CMS Content Sync settings page.');
      }
    }
    $io->text('Finished validating Pools.');

    $mode = $options['mode'] ?? 'all';
    if ($mode !== 'old') {
      $details_url = Url::fromRoute('cms_content_sync.syndication', ['type' => 'config'], ['absolute' => TRUE])->toString();
      $io->text("Starting config export using the faster, asynchronous update mode. If you face any issues, please look here for error details: " . $details_url);
      if (!SyncCoreFactory::export($mode, TRUE)) {
        $io->warning('Failed to use new mode. Your Sync Core is probably outdated. Please update.');
        $mode = 'old';
      }
      else {
        $io->text('Done.');
      }
    }
    if ($mode === 'old') {
      $io->text('Starting Flow export...');
      $count = 0;
      foreach (Flow::getAll() as $flow) {
        $io->text('> Updating Flow ' . $flow->label() . '...');
        $flow->getController()->updateEntityTypeVersions();
        $io->text('> Exporting Flow ' . $flow->label() . '...');
        $exporter = new SyncCoreFlowExport($flow);
        $batch = $exporter->prepareBatch($options['force']);
        $io->text('>> Executing ' . $batch->count() . ' operations...');
        $batch->executeAll();
        ++$count;
      }
      $io->text('Finished export of ' . $count . ' Flow(s).');

      $io->text('Deleting old configuration...');
      SyncCoreFlowExport::deleteUnusedFlows();
    }

    $io->success('Export completed.');
  }

  /**
   * Pull entities for a give flow.
   *
   * Kindly ask the Sync Core to pull all entities for a specific flow, or to
   * force pull one specific entity.
   *
   * @param \Drush\Style\DrushStyle $io
   *   The CLI service which allows interoperability.
   * @param string $flow_id
   *   The flow the entities should be pulled from.
   * @param array $options
   *   An array containing the option parameters provided by Drush.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \EdgeBox\SyncCore\Exception\SyncCoreException
   */
  public function pull(DrushStyle $io, $flow_id, array $options) {
    $force = $options['force'] ?? FALSE;
    $type = $options['type'] ?? NULL;

    // Hard deprecation: --force no longer has any effect for mass pull.
    if (!empty($force)) {
      $io->warning('--force is deprecated and has no effect. Mass pull uses migration types now.');
    }

    $entity_type = $options['entity_type'] ?? NULL;
    $bundle = $options['bundle'] ?? NULL;
    $entity_uuid = $options['entity_uuid'] ?? NULL;

    if (!empty($entity_uuid) && empty($entity_type)) {
      $io->error('If a specific --entity_uuid is provided, --entity_type must also be set.');
      return;
    }

    if (!empty($bundle) && empty($entity_type)) {
      $io->error('If a specific --bundle is provided, --entity_type must also be set.');
      return;
    }

    if (!empty($entity_uuid) && !empty($bundle)) {
      $io->error('The --entity_uuid and --bundle options cannot be used together.');
      return;
    }

    // Handle single-entity pull with the existing mechanism.
    if (!empty($entity_uuid)) {
      if (Uuid::isValid($entity_uuid)) {
        // @todo Allow pull for single entities which have not been pulled before.
        FlowPull::forcePullEntity($flow_id, $entity_type, $entity_uuid);
      }
      else {
        $io->error('The specified entity_uuid is invalid.');
      }
      return;
    }
    // Default migration type if not provided.
    if (empty($type)) {
      $type = 'pull-all';
    }
    // Trigger mass pull for selected flow and filters.
    $flows = Flow::getAll();
    $found_flow = FALSE;
    $results = [];
    foreach ($flows as $id => $flow) {
      if ($flow_id && $id != $flow_id) {
        continue;
      }
      $entity_typefound_flow = TRUE;
      $io->text('Preparing pull for Flow: ' . $flow->label());

      $entity_configs = $flow->getController()->getEntityTypeConfig(NULL, NULL, TRUE);
      foreach ($entity_configs as $entity_type_name => $bundles) {
        foreach ($bundles as $bundle_name => $config) {
          if (!empty($bundle) && $bundle !== $bundle_name) {
            continue;
          }

          $version = $config['version'] ?? NULL;
          if (!$version) {
            // Should not happen, but skip if no version is available.
            continue;
          }

          $operation = SyncCoreFactory::getSyncCoreV2()
            ->getSyndicationService()
            ->massPull()
            ->withFlow($flow->id)
            ->withNamespaceMachineName($entity_type_name)
            ->withEntityTypeMachineName($bundle_name)
            ->withEntityTypeVersion($version)
            ->executeWithType($type);

          if (!($goal = $operation->total())) {
            $io->text('> Nothing to do for: ' . $operation->getNamespaceMachineName() . '.' . $operation->getEntityTypeMachineName() . ' from ' . $operation->getFlow());
            continue;
          }

          $progress = 0;

          while ($progress < $operation->total()) {
            if ($progress > 0) {
              sleep(5);
            }

            try {
              $goal = $operation->total();
              $progress = $operation->progress(TRUE);
            }
            catch (TimeoutException $e) {
              $io->text('> Timeout when asking the Sync Core to report on the progress of pulling ' . $goal . ' ' . $operation->getNamespaceMachineName() . '.' . $operation->getEntityTypeMachineName() . ' from ' . $operation->getFlow() . '. Will try again in 15 seconds...');
              sleep(15);

              continue;
            }

            if ($progress == $goal) {
              $io->text('> Finished ' . $goal . ' operations for ' . $operation->getNamespaceMachineName() . '.' . $operation->getEntityTypeMachineName() . ' from ' . $operation->getFlow());
            }
            elseif (0 == $progress) {
              sleep(5);
            }
            else {
              $io->text('> Finished ' . $progress . ' of ' . $goal . ' operations for ' . $operation->getNamespaceMachineName() . '.' . $operation->getEntityTypeMachineName() . ' from ' . $operation->getFlow() . ': ' . floor($progress / $goal * 100) . '%');
            }
          }
        }
      }
    }
  }

  /**
   * Push all entities for a specific flow.
   *
   * @param \Drush\Style\DrushStyle $io
   *   The CLI service which allows interoperability.
   * @param string $flow_id
   *   The flow the entities should be pulled from.
   * @param array $options
   *   An array containing the option parameters provided by Drush.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \GuzzleHttp\Exception\GuzzleException
   */
  public function push(DrushStyle $io, $flow_id, array $options) {
    $push_mode = $options['push_mode'] ?? NULL;
    $type = $options['type'] ?? NULL;
    $filter_entity_type = $options['entity_type'] ?? NULL;
    $filter_bundle = $options['bundle'] ?? NULL;
    $flows = Flow::getAll();

    if (!is_null($push_mode)) {
      // Hard deprecation: --push_mode no longer has any effect for mass push.
      $io->warning('--push_mode is deprecated and has no effect. Use --type to control the migration behavior.');
      $push_mode = NULL;
    }

    if (!empty($filter_bundle) && empty($filter_entity_type)) {
      $io->error('If a specific --bundle is provided, --entity_type must also be set.');
      return;
    }

    // Default migration type if not provided.
    if (empty($type)) {
      $type = 'push-all';
    }

    foreach ($flows as $id => $flow) {
      if ($flow_id && $id != $flow_id) {
        continue;
      }
      foreach ($flow->getController()->getEntityTypeConfig(NULL, NULL, TRUE) as $entity_type_name => $bundles) {
        if (!empty($filter_entity_type) && $filter_entity_type !== $entity_type_name) {
          continue;
        }
        foreach ($bundles as $bundle_name => $config) {
          if (!empty($filter_bundle) && $filter_bundle !== $bundle_name) {
            continue;
          }

          // Include both automatic and manual export configurations by default.
          if (PushIntent::PUSH_AUTOMATICALLY != $config['export'] && PushIntent::PUSH_MANUALLY != $config['export']) {
            continue;
          }

          $version = $config['version'] ?? NULL;
          if (!$version) {
            continue;
          }

          $operation = SyncCoreFactory::getSyncCoreV2()->getSyndicationService()->massPush();
          $operation->withFlow($flow->id);
          $operation->withNamespaceMachineName($entity_type_name);
          $operation->withEntityTypeMachineName($bundle_name);
          $operation->withEntityTypeVersion($version);
          $operation->usingMigrationType($type);
          $operation->execute();

          $total = $operation->total();
          if (!$total) {
            $io->text('Skipping ' . $entity_type_name . '.' . $bundle_name . ' as no entities match.');
            continue;
          }
          $io->text('Starting to push ' . $operation->total() . ' ' . $entity_type_name . '.' . $bundle_name . ' entities.');
          $progress = 0;
          while ($operation->total() > $progress) {
            if ($progress > 0) {
              sleep(5);
            }

            try {
              // $operation->total() returns 1 at the beginning, so we update
              // the goal calculation as we go.
              $goal = $operation->total();
              $progress = $operation->progress(TRUE);
            }
            catch (TimeoutException $e) {
              $io->text('> Timeout when asking the Sync Core to report on the progress of pushing ' . $goal . ' ' . $operation->getNamespaceMachineName() . '.' . $operation->getEntityTypeMachineName() . ' from ' . $operation->getFlow() . '. Will try again in 15 seconds...');
              sleep(15);

              continue;
            }

            if ($progress == $goal) {
              $io->text('> Finished ' . $goal . ' operations for ' . $operation->getNamespaceMachineName() . '.' . $operation->getEntityTypeMachineName() . ' from ' . $operation->getFlow());
            }
            elseif (0 == $progress) {
              sleep(5);
            }
            else {
              $io->text('> Finished ' . $progress . ' of ' . $goal . ' operations for ' . $operation->getNamespaceMachineName() . '.' . $operation->getEntityTypeMachineName() . ' from ' . $operation->getFlow() . ': ' . floor($progress / $goal * 100) . '%');
            }
          }
        }
      }
    }
  }

  /**
   * Reset the status entities for a specific or all pool/s.
   *
   * @param \Drush\Style\DrushStyle $io
   *   The CLI service which allows interoperability.
   * @param array $options
   *   An array containing the option parameters provided by Drush.
   *
   * @throws \Drush\Exceptions\UserAbortException
   */
  public function resetStatusEntities(DrushStyle $io, array $options = ['pool_id' => NULL]) {
    $pool_id = empty($options['pool_id']) ? NULL : $options['pool_id'];

    if (empty($pool_id)) {
      $io->warning(dt('Are you sure you want to reset the status entities for all pools?'));
    }
    else {
      $io->warning(dt('Are you sure you want to reset the status entities for the pool: ' . $pool_id . '?'));
    }
    $io->warning(dt('By resetting the status of all entities, the date of the last pull and the date of the last push date will be reset. The dates will no longer be displayed until the content is pulled or pushed again and all entities will be pushed / pulled again at the next synchronization regardless of whether they have changed or not.'));

    if (!$io->confirm(dt('Do you want to continue?'))) {
      throw new UserAbortException();
    }

    empty($pool_id) ? Pool::resetStatusEntities() : Pool::resetStatusEntities($pool_id);
    $io->success('Status entities have been reset and entity caches are invalidated.');
  }

  /**
   * Check the flags for an entity.
   *
   * @param \Drush\Style\DrushStyle $io
   *   The CLI service which allows interoperability.
   * @param string $entity_uuid
   *   The uuid of the entity the flags should be checked for.
   * @param array $options
   *   An array containing the option parameters provided by Drush.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   */
  public function checkEntityFlags(DrushStyle $io, $entity_uuid, array $options = ['flag' => NULL]) {
    $flag = empty($options['flag']) ? NULL : $options['flag'];

    /**
     * @var \Drupal\cms_content_sync\Entity\EntityStatus[] $entity_status
     */
    $entity_status = \Drupal::entityTypeManager()
      ->getStorage('cms_content_sync_entity_status')
      ->loadByProperties(['entity_uuid' => $entity_uuid]);
    if (empty($entity_status)) {
      $io->text(dt('There is no status entity existent yet for this UUID.'));
    }
    else {
      foreach ($entity_status as $status) {
        $result = '';
        $io->text(dt('Flow: ' . $status->get('flow')->value));

        if (empty($flag)) {
          $result .= 'FLAG_IS_SOURCE_ENTITY: ' . ($status->isSourceEntity() ? 'TRUE' : 'FALSE') . PHP_EOL;
          $result .= 'FLAG_PUSH_ENABLED: ' . ($status->isPushEnabled() ? 'TRUE' : 'FALSE') . PHP_EOL;
          $result .= 'FLAG_PUSHED_AS_DEPENDENCY: ' . ($status->isPushedAsDependency() ? 'TRUE' : 'FALSE') . PHP_EOL;
          $result .= 'FLAG_EDIT_OVERRIDE: ' . ($status->isOverriddenLocally() ? 'TRUE' : 'FALSE') . PHP_EOL;
          $result .= 'FLAG_USER_ENABLED_PUSH: ' . ($status->didUserEnablePush() ? 'TRUE' : 'FALSE') . PHP_EOL;
          $result .= 'FLAG_DELETED: ' . ($status->isDeleted() ? 'TRUE' : 'FALSE') . PHP_EOL;
        }
        else {
          switch ($flag) {
            case 'FLAG_IS_SOURCE_ENTITY':
              $status->isSourceEntity() ? $result .= 'TRUE' : $result .= 'FALSE';

              break;

            case 'FLAG_PUSH_ENABLED':
              $status->isPushEnabled() ? $result .= 'TRUE' : $result .= 'FALSE';

              break;

            case 'FLAG_PUSHED_AS_DEPENDENCY':
              $status->isPushedAsDependency() ? $result .= 'TRUE' : $result .= 'FALSE';

              break;

            case 'FLAG_EDIT_OVERRIDE':
              $status->isOverriddenLocally() ? $result .= 'TRUE' : $result .= 'FALSE';

              break;

            case 'FLAG_USER_ENABLED_PUSH':
              $status->didUserEnablePush() ? $result .= 'TRUE' : $result .= 'FALSE';

              break;

            case 'FLAG_DELETED':
              $status->isDeleted() ? $result .= 'TRUE' : $result .= 'FALSE';

              break;
          }
        }
        $io->text(dt($result));
      }
    }
  }

  /**
   * Register this site to the Content Sync backend. Please visit https://app.content-sync.io/sites/register-multiple to get the required IDs and token.
   *
   * @param \Drush\Style\DrushStyle $io
   *   the CLI service which allows interoperability.
   * @param string $environment_type
   *   Either production, staging, testing or local.
   * @param string $contract
   *   The UUID of the contract. Please visit https://app.content-sync.io/sites/register-multiple to get the required IDs and token.
   * @param string $space
   *   The UUID of the space. Please visit https://app.content-sync.io/sites/register-multiple to get the required IDs and token.
   * @param string $token
   *   A JWT token to verify your request. Please visit https://app.content-sync.io/sites/register-multiple to get the required IDs and token.
   * @param array $options
   *   provide require_fixed_ip as "true" to use our proxy with a static outbound IP.
   */
  public function register(DrushStyle $io, string $environment_type, string $contract, string $space, string $token, array $options) {
    $require_fixed_ip = empty($options['require_fixed_ip']) ? NULL : 'true' === $options['require_fixed_ip'];

    $settings = ContentSyncSettings::getInstance();

    // Already registered if this exists.
    $uuid = $settings->getSiteUuid();

    $params = [
      'environmentType' => $environment_type,
      'useProxy' => $require_fixed_ip,
      'contractUuid' => $contract,
      'projectUuid' => $space,
      'uuid' => $uuid,
    ];

    $tks = explode('.', $token);
    [, $bodyb64] = $tks;
    $payload = JWT::jsonDecode(JWT::urlsafeB64Decode($bodyb64));
    if (!empty($payload->syncCoreBaseUrl)) {
      DrupalApplication::get()->setSyncCoreUrl($payload->syncCoreBaseUrl);
    }

    $core = SyncCoreFactory::getSyncCoreV2();

    try {
      $core->registerNewSiteWithToken($params, $token);

      // Clear feature flag cache.
      SyncCoreFactory::clearCache();

      $io->text(dt('Site was registered successfully.'));
    }
    // The site registration JWT expires after only 5 minutes, then the user must get a new one.
    catch (UnauthorizedException $e) {
      $io->error(dt('Your registration token expired. Please try again.'));
    }

    $details_url = Url::fromRoute('cms_content_sync.syndication', ['type' => 'config'], ['absolute' => TRUE])->toString();
    $io->text("Starting config export. If you face any issues, please look here for error details: " . $details_url);
    if (!SyncCoreFactory::export(RemoteSiteConfigRequestMode::ALL, TRUE)) {
      $io->warning('Failed to export Flows. Your Sync Core is probably outdated. Please update. To export the configuration to this Sync Core, please manually run: drush cse');
    }
    else {
      $io->text('Done.');
    }
  }

}

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

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