Skip to content
Snippets Groups Projects

Resolve #3479219 "Replace installercontrollers tempstore"

Files

+ 23
97
@@ -6,13 +6,12 @@ use Drupal\Component\Datetime\TimeInterface;
use Drupal\Component\Utility\DeprecationHelper;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Controller\ControllerBase;
use Drupal\Core\TempStore\SharedTempStore;
use Drupal\Core\TempStore\SharedTempStoreFactory;
use Drupal\Core\Url;
use Drupal\package_manager\Exception\StageException;
use Drupal\project_browser\ActivatorInterface;
use Drupal\project_browser\ComposerInstaller\Installer;
use Drupal\project_browser\EnabledSourceHandler;
use Drupal\project_browser\InstallState;
use Drupal\project_browser\ProjectBrowser\Project;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
@@ -53,39 +52,14 @@ class InstallerController extends ControllerBase {
*/
protected const STAGE_STATUS_OK = 0;
/**
* The Project Browser tempstore object.
*
* @var \Drupal\Core\TempStore\SharedTempStore
*/
protected SharedTempStore $projectBrowserTempStore;
/**
* Constructor for install controller.
*
* @param \Drupal\project_browser\ComposerInstaller\Installer $installer
* The installer service.
* @param \Drupal\Core\TempStore\SharedTempStoreFactory $shared_temp_store_factory
* The temporary storage factory.
* @param \Drupal\project_browser\EnabledSourceHandler $enabledSourceHandler
* The enabled project browser source.
* @param \Drupal\Component\Datetime\TimeInterface $time
* The system time.
* @param \Psr\Log\LoggerInterface $logger
* The logger instance.
* @param \Drupal\project_browser\ActivatorInterface $activator
* The project activator service.
*/
public function __construct(
private readonly Installer $installer,
SharedTempStoreFactory $shared_temp_store_factory,
private readonly EnabledSourceHandler $enabledSourceHandler,
private readonly TimeInterface $time,
private readonly LoggerInterface $logger,
private readonly ActivatorInterface $activator,
) {
$this->projectBrowserTempStore = $shared_temp_store_factory->get('project_browser');
}
private readonly InstallState $installState,
) {}
/**
* {@inheritdoc}
@@ -93,11 +67,11 @@ class InstallerController extends ControllerBase {
public static function create(ContainerInterface $container) {
return new static(
$container->get(Installer::class),
$container->get(SharedTempStoreFactory::class),
$container->get(EnabledSourceHandler::class),
$container->get(TimeInterface::class),
$container->get('logger.channel.project_browser'),
$container->get(ActivatorInterface::class),
$container->get(InstallState::class),
);
}
@@ -109,19 +83,11 @@ class InstallerController extends ControllerBase {
return AccessResult::allowedIf((bool) $ui_install);
}
/**
* Nulls the installing and core installing states.
*/
private function resetProgress(): void {
$this->projectBrowserTempStore->delete('requiring');
$this->projectBrowserTempStore->delete('installing');
}
/**
* Resets progress and destroys the stage.
*/
private function cancelRequire(): void {
$this->resetProgress();
$this->installState->deleteAll();
// Checking the for the presence of a lock in the package manager stage is
// necessary as this method can be called during create(), which includes
// both the PreCreate and PostCreate events. If an exception is caught
@@ -148,27 +114,17 @@ class InstallerController extends ControllerBase {
*
* @return \Symfony\Component\HttpFoundation\JsonResponse
* Information about the project's require/install status.
*
* If the project is being required, the response will include which require
* phase is currently occurring.
*
* When a project is required via the UI, the UI fetches this endpoint
* regularly so it can monitor the progress of the process and report which
* stage is taking place.
*/
public function inProgress(Project $project): JsonResponse {
$requiring = $this->projectBrowserTempStore->get('requiring');
$core_installing = $this->projectBrowserTempStore->get('installing');
$project_state = $this->installState->getStatus($project);
$return = ['status' => self::STATUS_IDLE];
if (isset($requiring['project_id']) && $requiring['project_id'] === $project->id) {
$return['status'] = self::STATUS_REQUIRING_PROJECT;
$return['phase'] = $requiring['phase'];
if ($project_state !== NULL) {
$return['status'] = ($project_state === 'requiring' || $project_state === 'applying')
? self::STATUS_REQUIRING_PROJECT
: self::STATUS_INSTALLING_PROJECT;
$return['phase'] = $project_state;
}
if ($core_installing === $project->id) {
$return['status'] = self::STATUS_INSTALLING_PROJECT;
}
return new JsonResponse($return);
}
@@ -242,24 +198,6 @@ class InstallerController extends ControllerBase {
], 418);
}
/**
* Updates the 'requiring' state in the temp store.
*
* @param string $id
* The ID of the project being required, as known to the enabled sources
* handler.
* @param string $phase
* The require phase in progress.
*/
private function setRequiringState(?string $id, string $phase): void {
$data = $this->projectBrowserTempStore->get('requiring') ?? [];
if ($id) {
$data['project_id'] = $id;
}
$data['phase'] = $phase;
$this->projectBrowserTempStore->set('requiring', $data);
}
/**
* Unlocks and destroys the stage.
*
@@ -291,7 +229,7 @@ class InstallerController extends ControllerBase {
catch (\Exception $e) {
return $this->errorResponse($e);
}
$this->projectBrowserTempStore->delete('requiring');
$this->installState->deleteAll();
$this->messenger()->addStatus($this->t('Install staging area unlocked.'));
return $this->redirect('project_browser.browse');
}
@@ -332,18 +270,18 @@ class InstallerController extends ControllerBase {
public function begin(): JsonResponse {
$stage_available = $this->installer->isAvailable();
if (!$stage_available) {
$requiring_metadata = $this->projectBrowserTempStore->getMetadata('requiring');
$updated_time = $this->installState->getFirstUpdatedTime();
if (!$this->installer->lockCameFromProjectBrowserInstaller()) {
return $this->lockedResponse($this->t('The installation stage is locked by a process outside of Project Browser'), '');
}
if (empty($requiring_metadata)) {
if (empty($updated_time)) {
$unlock_url = self::getUrlWithReplacedCsrfTokenPlaceholder(
Url::fromRoute('project_browser.install.unlock')
);
$message = t('An install staging area claimed by Project Browser exists but has expired. You may unlock the stage and try the install again.');
return $this->lockedResponse($message, $unlock_url);
}
$time_since_updated = $this->time->getRequestTime() - $requiring_metadata->getUpdated();
$time_since_updated = $this->time->getRequestTime() - $updated_time;
$hours = (int) gmdate("H", $time_since_updated);
$minutes = (int) gmdate("i", $time_since_updated);
$minutes = $time_since_updated > 60 ? $minutes : 'less than 1';
@@ -400,7 +338,7 @@ class InstallerController extends ControllerBase {
* Status message.
*/
public function require(Request $request, string $stage_id): JsonResponse {
$package_names = $package_ids = [];
$package_names = [];
foreach ($request->toArray() as $project) {
$project = $this->enabledSourceHandler->getStoredProject($project);
if ($project->source === 'project_browser_test_mock') {
@@ -412,23 +350,11 @@ class InstallerController extends ControllerBase {
return new JsonResponse(['message' => "$project->machineName is not safe to add because its security coverage has been revoked"], 500);
}
}
$this->installState->setState($project, 'requiring');
$package_names[] = $project->packageName;
$package_ids[] = $project->id;
}
$requiring = $this->projectBrowserTempStore->get('requiring');
$current_package_names = implode(', ', $package_names);
if (!empty($requiring['project_id']) && $requiring['project_id'] !== $current_package_names) {
$error_message = sprintf(
'Error: a request to install %s was ignored as an install for a different project is in progress.',
$current_package_names
);
return new JsonResponse(['message' => $error_message], 500);
}
$this->setRequiringState(implode(', ', $package_ids), 'requiring module');
try {
$this->installer->claim($stage_id)->require($package_names);
$this->setRequiringState(NULL, 'requiring module');
return $this->successResponse('require', $stage_id);
}
catch (\Exception $e) {
@@ -447,7 +373,9 @@ class InstallerController extends ControllerBase {
* Status message.
*/
public function apply(string $stage_id): JsonResponse {
$this->setRequiringState(NULL, 'applying');
foreach (array_keys($this->installState->toArray()) as $project_id) {
$this->installState->setState($this->enabledSourceHandler->getStoredProject($project_id), 'applying');
}
try {
$this->installer->claim($stage_id)->apply();
}
@@ -468,7 +396,6 @@ class InstallerController extends ControllerBase {
* Status message.
*/
public function postApply(string $stage_id): JsonResponse {
$this->setRequiringState(NULL, 'post apply');
try {
$this->installer->claim($stage_id)->postApply();
}
@@ -488,14 +415,12 @@ class InstallerController extends ControllerBase {
* Status message.
*/
public function destroy(string $stage_id): JsonResponse {
$this->setRequiringState(NULL, 'completing');
try {
$this->installer->claim($stage_id)->destroy();
}
catch (\Exception $e) {
return $this->errorResponse($e, 'destroy');
}
$this->projectBrowserTempStore->delete('requiring');
return new JsonResponse([
'phase' => 'destroy',
'status' => self::STAGE_STATUS_OK,
@@ -515,15 +440,16 @@ class InstallerController extends ControllerBase {
public function activate(Request $request): JsonResponse {
foreach ($request->toArray() as $project) {
$project = $this->enabledSourceHandler->getStoredProject($project);
$this->projectBrowserTempStore->set('installing', $project->id);
$this->installState->setState($project, 'activating');
try {
$this->activator->activate($project);
$this->installState->setState($project, 'installed');
}
catch (\Throwable $e) {
return $this->errorResponse($e, 'project install');
}
finally {
$this->resetProgress();
$this->installState->deleteAll();
}
}
return new JsonResponse(['status' => 0]);
Loading