PhpTufValidator.php

Namespace

Drupal\package_manager\Validator

File

core/modules/package_manager/src/Validator/PhpTufValidator.php

View source
<?php

declare (strict_types=1);
namespace Drupal\package_manager\Validator;

use Drupal\Component\Assertion\Inspector;
use Drupal\Component\Serialization\Json;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Site\Settings;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\Url;
use Drupal\package_manager\ComposerInspector;
use Drupal\package_manager\Event\PreApplyEvent;
use Drupal\package_manager\Event\PreCreateEvent;
use Drupal\package_manager\Event\PreOperationStageEvent;
use Drupal\package_manager\Event\PreRequireEvent;
use Drupal\package_manager\Event\StatusCheckEvent;
use Drupal\package_manager\PathLocator;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

/**
 * Validates that PHP-TUF is installed and correctly configured.
 *
 * In both the active and stage directories, this checks for the following
 * conditions:
 * - The PHP-TUF plugin is installed.
 * - The plugin is not explicitly blocked by Composer's `allow-plugins`
 *   configuration.
 * - Composer is aware of at least one repository that has TUF support
 *   explicitly enabled.
 *
 * Until it's more real world-tested, TUF protection is bypassed by default.
 * Ultimately, though, Package Manager will not treat TUF as optional.
 *
 * @internal
 *   This is an internal part of Package Manager and may be changed or removed
 *   at any time without warning. External code should not interact with this
 *   class.
 */
final class PhpTufValidator implements EventSubscriberInterface {
    use StringTranslationTrait;
    
    /**
     * The name of the PHP-TUF Composer integration plugin.
     *
     * @var string
     */
    public const PLUGIN_NAME = 'php-tuf/composer-integration';
    public function __construct(PathLocator $pathLocator, ComposerInspector $composerInspector, ModuleHandlerInterface $moduleHandler, Settings $settings, array $repositories) {
        assert(Inspector::assertAllStrings($repositories));
    }
    
    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents() : array {
        return [
            StatusCheckEvent::class => 'validate',
            PreCreateEvent::class => 'validate',
            PreRequireEvent::class => 'validate',
            PreApplyEvent::class => 'validate',
        ];
    }
    
    /**
     * Reacts to a stage event by validating PHP-TUF configuration as needed.
     *
     * @param \Drupal\package_manager\Event\PreOperationStageEvent $event
     *   The event object.
     */
    public function validate(PreOperationStageEvent $event) : void {
        $messages = $this->validateTuf($this->pathLocator
            ->getProjectRoot());
        if ($messages) {
            $event->addError($messages, $this->t('The active directory is not protected by PHP-TUF, which is required to use Package Manager securely.'));
        }
        $stage = $event->stage;
        if ($stage->stageDirectoryExists()) {
            $messages = $this->validateTuf($stage->getStageDirectory());
            if ($messages) {
                $event->addError($messages, $this->t('The stage directory is not protected by PHP-TUF, which is required to use Package Manager securely.'));
            }
        }
    }
    
    /**
     * Flags messages if PHP-TUF is not installed and configured properly.
     *
     * @param string $dir
     *   The directory to examine.
     *
     * @return \Drupal\Core\StringTranslation\TranslatableMarkup[]
     *   The error messages, if any.
     */
    private function validateTuf(string $dir) : array {
        $messages = [];
        // This setting will be removed without warning when no longer need.
        if ($this->settings
            ->get('package_manager_bypass_tuf', TRUE)) {
            return $messages;
        }
        if ($this->moduleHandler
            ->moduleExists('help')) {
            $help_url = Url::fromRoute('help.page', [
                'name' => 'package_manager',
            ])->setOption('fragment', 'package-manager-tuf-info')
                ->toString();
        }
        // The Composer plugin must be installed.
        $installed_packages = $this->composerInspector
            ->getInstalledPackagesList($dir);
        if (!isset($installed_packages[static::PLUGIN_NAME])) {
            $message = $this->t('The <code>@plugin</code> plugin is not installed.', [
                '@plugin' => static::PLUGIN_NAME,
            ]);
            if (isset($help_url)) {
                $message = $this->t('@message See <a href=":url">the help page</a> for more information on how to install the plugin.', [
                    '@message' => $message,
                    ':url' => $help_url,
                ]);
            }
            $messages[] = $message;
        }
        // And it has to be explicitly enabled.
        $allowed_plugins = $this->composerInspector
            ->getAllowPluginsConfig($dir);
        if ($allowed_plugins !== TRUE && empty($allowed_plugins[static::PLUGIN_NAME])) {
            $message = $this->t('The <code>@plugin</code> plugin is not listed as an allowed plugin.', [
                '@plugin' => static::PLUGIN_NAME,
            ]);
            if (isset($help_url)) {
                $message = $this->t('@message See <a href=":url">the help page</a> for more information on how to configure the plugin.', [
                    '@message' => $message,
                    ':url' => $help_url,
                ]);
            }
            $messages[] = $message;
        }
        // Confirm that all repositories we're configured to look at have opted into
        // TUF protection.
        foreach ($this->getRepositoryStatus($dir) as $url => $is_protected) {
            if ($is_protected) {
                continue;
            }
            $message = $this->t('TUF is not enabled for the <code>@url</code> repository.', [
                '@url' => $url,
            ]);
            if (isset($help_url)) {
                $message = $this->t('@message See <a href=":url">the help page</a> for more information on how to set up this repository.', [
                    '@message' => $message,
                    ':url' => $help_url,
                ]);
            }
            $messages[] = $message;
        }
        return $messages;
    }
    
    /**
     * Gets the TUF protection status of Composer repositories.
     *
     * @param string $dir
     *   The directory in which to run Composer.
     *
     * @return bool[]
     *   An array of booleans, keyed by repository URL, indicating whether TUF
     *   protection is enabled for that repository.
     */
    private function getRepositoryStatus(string $dir) : array {
        $status = [];
        $repositories = $this->composerInspector
            ->getConfig('repositories', $dir);
        $repositories = Json::decode($repositories);
        foreach ($repositories as $repository) {
            // Only Composer repositories can have TUF protection.
            if ($repository['type'] === 'composer') {
                $url = $repository['url'];
                $status[$url] = !empty($repository['tuf']);
            }
        }
        return array_intersect_key($status, array_flip($this->repositories));
    }

}

Classes

Title Deprecated Summary
PhpTufValidator Validates that PHP-TUF is installed and correctly configured.

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.