Commit 339eba4a authored by catch's avatar catch
Browse files

Issue #3525642 by amateescu, smustgrave: The active workspace is not persisted...

Issue #3525642 by amateescu, smustgrave: The active workspace is not persisted for the entire lifecycle of a form
parent 50cbe65c
Loading
Loading
Loading
Loading
Loading
+1 −0
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@
%The "Drupal\\Core\\Database\\Query\\SelectExtender::hasAnyTag\(\)" method will require a new "string \.\.\. \$tags" argument in the next major version of its interface%
%The "Drupal\\Core\\Entity\\Query\\QueryBase::hasAllTags\(\)" method will require a new "string \.\.\. \$tags" argument in the next major version of its interface%
%The "Drupal\\Core\\Entity\\Query\\QueryBase::hasAnyTag\(\)" method will require a new "string \.\.\. \$tags" argument in the next major version of its interface%
%The "Drupal\\workspaces\\WorkspaceManager::setActiveWorkspace\(\)" method will require a new "bool \$persist" argument in the next major version of its interface%

# Symfony 7.3.
%Since symfony/validator 7.3: Passing an array of options to configure the "[^"]+" constraint is deprecated, use named arguments instead.%
+37 −0
Original line number Diff line number Diff line
@@ -44420,6 +44420,18 @@
	'count' => 1,
	'path' => __DIR__ . '/modules/workspaces/src/Form/WorkspaceSwitcherForm.php',
];
$ignoreErrors[] = [
	'message' => '#^Method Drupal\\\\workspaces\\\\Negotiator\\\\QueryParameterWorkspaceNegotiator\\:\\:setActiveWorkspace\\(\\) has no return type specified\\.$#',
	'identifier' => 'missingType.return',
	'count' => 1,
	'path' => __DIR__ . '/modules/workspaces/src/Negotiator/QueryParameterWorkspaceNegotiator.php',
];
$ignoreErrors[] = [
	'message' => '#^Method Drupal\\\\workspaces\\\\Negotiator\\\\QueryParameterWorkspaceNegotiator\\:\\:unsetActiveWorkspace\\(\\) has no return type specified\\.$#',
	'identifier' => 'missingType.return',
	'count' => 1,
	'path' => __DIR__ . '/modules/workspaces/src/Negotiator/QueryParameterWorkspaceNegotiator.php',
];
$ignoreErrors[] = [
	'message' => '#^Method Drupal\\\\workspaces\\\\Negotiator\\\\SessionWorkspaceNegotiator\\:\\:getActiveWorkspace\\(\\) has no return type specified\\.$#',
	'identifier' => 'missingType.return',
@@ -45116,6 +45128,31 @@
	'count' => 1,
	'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/EntityWorkspaceConflictConstraintValidatorTest.php',
];
$ignoreErrors[] = [
	'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\WorkspaceFormPersistenceTest\\:\\:initializeWorkspacesModule\\(\\) has no return type specified\\.$#',
	'identifier' => 'missingType.return',
	'count' => 1,
	'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceFormPersistenceTest.php',
];
$ignoreErrors[] = [
	'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\WorkspaceFormPersistenceTest\\:\\:switchToWorkspace\\(\\) has no return type specified\\.$#',
	'identifier' => 'missingType.return',
	'count' => 1,
	'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceFormPersistenceTest.php',
];
$ignoreErrors[] = [
	'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\WorkspaceFormPersistenceTest\\:\\:assertWorkspaceAssociation\\(\\) has no return type specified\\.$#',
	'identifier' => 'missingType.return',
	'count' => 1,
	'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceFormPersistenceTest.php',
];
$ignoreErrors[] = [
	'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\WorkspaceFormPersistenceTest\\:\\:createWorkspaceHierarchy\\(\\) has no return type specified\\.$#',
	'identifier' => 'missingType.return',
	'count' => 1,
	'path' => __DIR__ . '/modules/workspaces/tests/src/Kernel/WorkspaceFormPersistenceTest.php',
];
$ignoreErrors[] = [
	'message' => '#^Method Drupal\\\\Tests\\\\workspaces\\\\Kernel\\\\EntityWorkspaceConflictConstraintValidatorTest\\:\\:initializeWorkspacesModule\\(\\) has no return type specified\\.$#',
	'identifier' => 'missingType.return',
+55 −1
Original line number Diff line number Diff line
@@ -8,7 +8,10 @@
use Drupal\Core\Hook\Attribute\Hook;
use Drupal\Core\Render\Element;
use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\workspaces\Entity\Workspace;
use Drupal\workspaces\Negotiator\QueryParameterWorkspaceNegotiator;
use Drupal\workspaces\WorkspaceManagerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;

/**
 * Defines a class for reacting to form operations.
@@ -17,6 +20,8 @@ class FormOperations {

  public function __construct(
    protected WorkspaceManagerInterface $workspaceManager,
    #[Autowire('@workspaces.negotiator.query_parameter')]
    protected QueryParameterWorkspaceNegotiator $queryParameterNegotiator,
  ) {}

  /**
@@ -24,8 +29,21 @@ public function __construct(
   */
  #[Hook('form_alter')]
  public function formAlter(array &$form, FormStateInterface $form_state, $form_id): void {
    $active_workspace = $this->workspaceManager->getActiveWorkspace();

    // Ensure that the form's initial workspace (if any) is used for the current
    // request.
    $form_workspace_id = $form_state->getUserInput()['active_workspace_id'] ?? NULL;
    $form_workspace = $form_workspace_id
      ? Workspace::load($form_workspace_id)
      : NULL;
    if ($form_workspace && (!$active_workspace || $active_workspace->id() != $form_workspace->id())) {
      $this->workspaceManager->setActiveWorkspace($form_workspace, FALSE);
      $active_workspace = $form_workspace;
    }

    // No alterations are needed if we're not in a workspace context.
    if (!$this->workspaceManager->hasActiveWorkspace()) {
    if (!$active_workspace) {
      return;
    }

@@ -47,6 +65,17 @@ public function formAlter(array &$form, FormStateInterface $form_state, $form_id
      ];
      $this->addWorkspaceValidation($form);
    }
    else {
      // Persist the active workspace for the entire lifecycle of the form,
      // including AJAX requests.
      $form['active_workspace_id'] = [
        '#type' => 'hidden',
        '#value' => $active_workspace->id(),
      ];

      $url_query_options = $this->queryParameterNegotiator->getQueryOptions($active_workspace->id());
      $this->setAjaxWorkspace($form, $url_query_options + ['persist' => FALSE]);
    }
  }

  /**
@@ -83,4 +112,29 @@ public static function validateDefaultWorkspace(array &$form, FormStateInterface
    }
  }

  /**
   * Ensures that the current workspace is persisted across AJAX interactions.
   *
   * @param array &$element
   *   An associative array containing the structure of the form.
   * @param array $url_query_options
   *   An array of URL query options used by the query parameter workspace
   *   negotiator.
   */
  protected function setAjaxWorkspace(array &$element, array $url_query_options): void {
    // Recurse through all children if needed.
    foreach (Element::children($element) as $key) {
      if (isset($element[$key]) && $element[$key]) {
        $this->setAjaxWorkspace($element[$key], $url_query_options);
      }
    }

    if (isset($element['#ajax']) && !isset($element['#ajax']['options']['query']['workspace'])) {
      $element['#ajax']['options']['query'] = array_merge_recursive(
        $url_query_options,
        $element['#ajax']['options']['query'] ?? [],
      );
    }
  }

}
+42 −0
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@

use Drupal\Component\Utility\Crypt;
use Drupal\Core\Site\Settings;
use Drupal\workspaces\WorkspaceInterface;
use Symfony\Component\HttpFoundation\Request;

/**
@@ -11,6 +12,11 @@
 */
class QueryParameterWorkspaceNegotiator extends SessionWorkspaceNegotiator {

  /**
   * Whether the negotiated workspace should be persisted.
   */
  protected bool $persist = TRUE;

  /**
   * {@inheritdoc}
   */
@@ -24,6 +30,8 @@ public function applies(Request $request) {
   * {@inheritdoc}
   */
  public function getActiveWorkspaceId(Request $request): ?string {
    $this->persist = (bool) $request->query->get('persist', TRUE);

    $workspace_id = (string) $request->query->get('workspace');
    $token = (string) $request->query->get('token');
    $is_valid_token = hash_equals($this->getQueryToken($workspace_id), $token);
@@ -35,6 +43,40 @@ public function getActiveWorkspaceId(Request $request): ?string {
    return $is_valid_token ? $workspace_id : NULL;
  }

  /**
   * {@inheritdoc}
   */
  public function setActiveWorkspace(WorkspaceInterface $workspace) {
    if ($this->persist) {
      parent::setActiveWorkspace($workspace);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function unsetActiveWorkspace() {
    if ($this->persist) {
      parent::unsetActiveWorkspace();
    }
  }

  /**
   * Returns the query options used by this negotiator.
   *
   * @param string $workspace_id
   *   A workspace ID.
   *
   * @return array
   *   An array of query options that can be used for a \Drupal\Core\Url object.
   */
  public function getQueryOptions(string $workspace_id): array {
    return [
      'workspace' => $workspace_id,
      'token' => $this->getQueryToken($workspace_id),
    ];
  }

  /**
   * Calculates a token based on a workspace ID.
   *
+60 −18
Original line number Diff line number Diff line
@@ -9,24 +9,53 @@
use Drupal\Core\Site\Settings;
use Drupal\Core\State\StateInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\workspaces\Negotiator\WorkspaceNegotiatorInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\DependencyInjection\Attribute\AutowireIterator;
use Symfony\Component\HttpFoundation\RequestStack;

/**
 * Provides the workspace manager.
 *
 * @property iterable $negotiators
 */
class WorkspaceManager implements WorkspaceManagerInterface {

  use StringTranslationTrait;

  /**
   * The current active workspace or FALSE if there is no active workspace.
   * The current active workspace.
   *
   * @var \Drupal\workspaces\WorkspaceInterface|false
   * The value is either a workspace object, FALSE if there is no active
   * workspace, or NULL if the active workspace hasn't been determined yet.
   */
  protected $activeWorkspace;
  protected WorkspaceInterface|false|null $activeWorkspace = NULL;

  public function __construct(protected RequestStack $requestStack, protected EntityTypeManagerInterface $entityTypeManager, protected MemoryCacheInterface $entityMemoryCache, protected AccountProxyInterface $currentUser, protected StateInterface $state, protected LoggerInterface $logger, protected ClassResolverInterface $classResolver, protected WorkspaceAssociationInterface $workspaceAssociation, protected WorkspaceInformationInterface $workspaceInfo, protected array $negotiatorIds = []) {
  /**
   * An array of workspace negotiator services.
   *
   * @todo Remove in drupal:12.0.0.
   */
  private array $collectedNegotiators = [];

  public function __construct(
    protected RequestStack $requestStack,
    protected EntityTypeManagerInterface $entityTypeManager,
    protected MemoryCacheInterface $entityMemoryCache,
    protected AccountProxyInterface $currentUser,
    protected StateInterface $state,
    #[Autowire(service: 'logger.channel.workspaces')]
    protected LoggerInterface $logger,
    #[AutowireIterator(tag: 'workspace_negotiator')]
    protected $negotiators,
    protected WorkspaceAssociationInterface $workspaceAssociation,
    protected WorkspaceInformationInterface $workspaceInfo,
  ) {
    if ($negotiators instanceof ClassResolverInterface) {
      @trigger_error('Passing the \'class_resolver\' service as the 7th argument to ' . __METHOD__ . ' is deprecated in drupal:11.3.0 and is unsupported in drupal:12.0.0. Use autowiring for the \'workspaces.manager\' service instead. See https://www.drupal.org/node/3532939', E_USER_DEPRECATED);
      $this->negotiators = $this->collectedNegotiators;
    }
  }

  /**
@@ -43,10 +72,7 @@ public function getActiveWorkspace() {
    if (!isset($this->activeWorkspace)) {
      $request = $this->requestStack->getCurrentRequest();

      foreach ($this->negotiatorIds as $negotiator_id) {
        /** @var \Drupal\workspaces\Negotiator\WorkspaceIdNegotiatorInterface $negotiator */
        $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);

      foreach ($this->negotiators as $negotiator) {
        if ($negotiator->applies($request)) {
          if ($workspace_id = $negotiator->getActiveWorkspaceId($request)) {
            /** @var \Drupal\workspaces\WorkspaceInterface $negotiated_workspace */
@@ -79,18 +105,21 @@ public function getActiveWorkspace() {
  /**
   * {@inheritdoc}
   */
  public function setActiveWorkspace(WorkspaceInterface $workspace) {
  public function setActiveWorkspace(WorkspaceInterface $workspace, /* bool $persist = TRUE */) {
    $persist = func_num_args() < 2 || func_get_arg(1);

    $this->doSwitchWorkspace($workspace);

    // Set the workspace on the proper negotiator.
    // Set the workspace on the first applicable negotiator.
    if ($persist) {
      $request = $this->requestStack->getCurrentRequest();
    foreach ($this->negotiatorIds as $negotiator_id) {
      $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
      foreach ($this->negotiators as $negotiator) {
        if ($negotiator->applies($request)) {
          $negotiator->setActiveWorkspace($workspace);
          break;
        }
      }
    }

    return $this;
  }
@@ -102,8 +131,7 @@ public function switchToLive() {
    $this->doSwitchWorkspace(NULL);

    // Unset the active workspace on all negotiators.
    foreach ($this->negotiatorIds as $negotiator_id) {
      $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
    foreach ($this->negotiators as $negotiator) {
      $negotiator->unsetActiveWorkspace();
    }

@@ -253,4 +281,18 @@ public function purgeDeletedWorkspacesBatch() {
    }
  }

  /**
   * Adds a workspace negotiator service.
   *
   * @param \Drupal\workspaces\Negotiator\WorkspaceNegotiatorInterface $negotiator
   *   The negotiator to be added.
   *
   * @todo Remove in drupal:12.0.0.
   *
   * @internal
   */
  public function addNegotiator(WorkspaceNegotiatorInterface $negotiator): void {
    $this->collectedNegotiators[] = $negotiator;
  }

}
Loading