Skip to content
Snippets Groups Projects
Commit c84365f7 authored by catch's avatar catch
Browse files

Issue #3443761 by amateescu: Workspace negotiators shouldn't be responsible...

Issue #3443761 by amateescu: Workspace negotiators shouldn't be responsible for loading the negotiated workspace entity
parent 091483c1
No related branches found
No related tags found
26 merge requests!11131[10.4.x-only-DO-NOT-MERGE]: Issue ##2842525 Ajax attached to Views exposed filter form does not trigger callbacks,!9470[10.3.x-only-DO-NOT-MERGE]: #3331771 Fix file_get_contents(): Passing null to parameter,!8540Issue #3457061: Bootstrap Modal dialog Not closing after 10.3.0 Update,!8528Issue #3456871 by Tim Bozeman: Support NULL services,!8373Issue #3427374 by danflanagan8, Vighneshh: taxonomy_tid ViewsArgumentDefault...,!3878Removed unused condition head title for views,!3818Issue #2140179: $entity->original gets stale between updates,!3742Issue #3328429: Create item list field formatter for displaying ordered and unordered lists,!3731Claro: role=button on status report items,!3651Issue #3347736: Create new SDC component for Olivero (header-search),!3531Issue #3336994: StringFormatter always displays links to entity even if the user in context does not have access,!3355Issue #3209129: Scrolling problems when adding a block via layout builder,!3154Fixes #2987987 - CSRF token validation broken on routes with optional parameters.,!3133core/modules/system/css/components/hidden.module.css,!2964Issue #2865710 : Dependencies from only one instance of a widget are used in display modes,!2812Issue #3312049: [Followup] Fix Drupal.Commenting.FunctionComment.MissingReturnType returns for NULL,!2378Issue #2875033: Optimize joins and table selection in SQL entity query implementation,!2062Issue #3246454: Add weekly granularity to views date sort,!1105Issue #3025039: New non translatable field on translatable content throws error,!1073issue #3191727: Focus states on mobile second level navigation items fixed,!10223132456: Fix issue where views instances are emptied before an ajax request is complete,!877Issue #2708101: Default value for link text is not saved,!617Issue #3043725: Provide a Entity Handler for user cancelation,!579Issue #2230909: Simple decimals fail to pass validation,!560Move callback classRemove outside of the loop,!555Issue #3202493
Pipeline #163442 canceled
Pipeline: drupal

#163443

    ......@@ -2,6 +2,8 @@
    namespace Drupal\workspaces\Negotiator;
    use Drupal\Component\Utility\Crypt;
    use Drupal\Core\Site\Settings;
    use Symfony\Component\HttpFoundation\Request;
    /**
    ......@@ -13,21 +15,38 @@ class QueryParameterWorkspaceNegotiator extends SessionWorkspaceNegotiator {
    * {@inheritdoc}
    */
    public function applies(Request $request) {
    return is_string($request->query->get('workspace')) && parent::applies($request);
    return is_string($request->query->get('workspace'))
    && is_string($request->query->get('token'))
    && parent::applies($request);
    }
    /**
    * {@inheritdoc}
    */
    public function getActiveWorkspace(Request $request) {
    $workspace_id = $request->query->get('workspace');
    if ($workspace_id && ($workspace = $this->workspaceStorage->load($workspace_id))) {
    $this->setActiveWorkspace($workspace);
    return $workspace;
    }
    public function getActiveWorkspaceId(Request $request): ?string {
    $workspace_id = (string) $request->query->get('workspace');
    $token = (string) $request->query->get('token');
    $is_valid_token = hash_equals($this->getQueryToken($workspace_id), $token);
    // This negotiator receives a workspace ID from user input, so a minimal
    // validation is needed to ensure that we protect against fake input before
    // the workspace manager fully validates the negotiated workspace ID.
    // @see \Drupal\workspaces\WorkspaceManager::getActiveWorkspace()
    return $is_valid_token ? $workspace_id : NULL;
    }
    return NULL;
    /**
    * Calculates a token based on a workspace ID.
    *
    * @param string $workspace_id
    * The workspace ID.
    *
    * @return string
    * An 8 char token based on the given workspace ID.
    */
    protected function getQueryToken(string $workspace_id): string {
    // Return the first 8 characters.
    return substr(Crypt::hmacBase64($workspace_id, Settings::getHashSalt()), 0, 8);
    }
    }
    ......@@ -11,60 +11,36 @@
    /**
    * Defines the session workspace negotiator.
    */
    class SessionWorkspaceNegotiator implements WorkspaceNegotiatorInterface {
    class SessionWorkspaceNegotiator implements WorkspaceNegotiatorInterface, WorkspaceIdNegotiatorInterface {
    /**
    * The current user.
    *
    * @var \Drupal\Core\Session\AccountInterface
    */
    protected $currentUser;
    /**
    * The session.
    *
    * @var \Symfony\Component\HttpFoundation\Session\Session
    */
    protected $session;
    /**
    * The workspace storage handler.
    *
    * @var \Drupal\Core\Entity\EntityStorageInterface
    */
    protected $workspaceStorage;
    public function __construct(
    protected readonly AccountInterface $currentUser,
    protected readonly Session $session,
    protected readonly EntityTypeManagerInterface $entityTypeManager
    ) {}
    /**
    * Constructor.
    *
    * @param \Drupal\Core\Session\AccountInterface $current_user
    * The current user.
    * @param \Symfony\Component\HttpFoundation\Session\Session $session
    * The session.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
    * The entity type manager.
    * {@inheritdoc}
    */
    public function __construct(AccountInterface $current_user, Session $session, EntityTypeManagerInterface $entity_type_manager) {
    $this->currentUser = $current_user;
    $this->session = $session;
    $this->workspaceStorage = $entity_type_manager->getStorage('workspace');
    public function applies(Request $request) {
    // This negotiator only applies if the current user is authenticated.
    return $this->currentUser->isAuthenticated();
    }
    /**
    * {@inheritdoc}
    */
    public function applies(Request $request) {
    // This negotiator only applies if the current user is authenticated.
    return $this->currentUser->isAuthenticated();
    public function getActiveWorkspaceId(Request $request): ?string {
    return $this->session->get('active_workspace_id');
    }
    /**
    * {@inheritdoc}
    */
    public function getActiveWorkspace(Request $request) {
    $workspace_id = $this->session->get('active_workspace_id');
    $workspace_id = $this->getActiveWorkspaceId($request);
    if ($workspace_id && ($workspace = $this->workspaceStorage->load($workspace_id))) {
    if ($workspace_id && ($workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id))) {
    return $workspace;
    }
    ......
    <?php
    namespace Drupal\workspaces\Negotiator;
    use Symfony\Component\HttpFoundation\Request;
    /**
    * Interface for workspace negotiators that return only the negotiated ID.
    */
    interface WorkspaceIdNegotiatorInterface {
    /**
    * Performs workspace negotiation.
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
    * The HTTP request.
    *
    * @return string|null
    * A valid workspace ID if the negotiation was successful, NULL otherwise.
    */
    public function getActiveWorkspaceId(Request $request): ?string;
    }
    ......@@ -25,25 +25,10 @@ interface WorkspaceNegotiatorInterface {
    public function applies(Request $request);
    /**
    * Gets the negotiated workspace, if any.
    *
    * Note that it is the responsibility of each implementation to check whether
    * the negotiated workspace actually exists in the storage.
    *
    * @param \Symfony\Component\HttpFoundation\Request $request
    * The HTTP request.
    *
    * @return \Drupal\workspaces\WorkspaceInterface|null
    * The negotiated workspace or NULL if the negotiator could not determine a
    * valid workspace.
    */
    public function getActiveWorkspace(Request $request);
    /**
    * Sets the negotiated workspace.
    * Notifies the negotiator that the workspace ID returned has been accepted.
    *
    * @param \Drupal\workspaces\WorkspaceInterface $workspace
    * The workspace entity.
    * The negotiated workspace entity.
    */
    public function setActiveWorkspace(WorkspaceInterface $workspace);
    ......
    ......@@ -42,21 +42,34 @@ public function hasActiveWorkspace() {
    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);
    if ($negotiator->applies($request)) {
    if ($workspace_id = $negotiator->getActiveWorkspaceId($request)) {
    /** @var \Drupal\workspaces\WorkspaceInterface $negotiated_workspace */
    $negotiated_workspace = $this->entityTypeManager
    ->getStorage('workspace')
    ->load($workspace_id);
    }
    // By default, 'view' access is checked when a workspace is activated,
    // but it should also be checked when retrieving the currently active
    // workspace.
    if (($negotiated_workspace = $negotiator->getActiveWorkspace($request)) && $negotiated_workspace->access('view')) {
    if (isset($negotiated_workspace) && $negotiated_workspace->access('view')) {
    // Notify the negotiator that its workspace has been selected.
    $negotiator->setActiveWorkspace($negotiated_workspace);
    $active_workspace = $negotiated_workspace;
    break;
    }
    }
    }
    // If no negotiator was able to determine the active workspace, default to
    // the live version of the site.
    // If no negotiator was able to provide a valid workspace, default to the
    // live version of the site.
    $this->activeWorkspace = $active_workspace ?? FALSE;
    }
    ......
    ......@@ -3,6 +3,7 @@
    namespace Drupal\workspace_update_test\Negotiator;
    use Drupal\workspaces\Entity\Workspace;
    use Drupal\workspaces\Negotiator\WorkspaceIdNegotiatorInterface;
    use Drupal\workspaces\Negotiator\WorkspaceNegotiatorInterface;
    use Drupal\workspaces\WorkspaceInterface;
    use Symfony\Component\HttpFoundation\Request;
    ......@@ -10,7 +11,7 @@
    /**
    * Defines a workspace negotiator used for testing.
    */
    class TestWorkspaceNegotiator implements WorkspaceNegotiatorInterface {
    class TestWorkspaceNegotiator implements WorkspaceNegotiatorInterface, WorkspaceIdNegotiatorInterface {
    /**
    * {@inheritdoc}
    ......@@ -19,11 +20,18 @@ public function applies(Request $request) {
    return TRUE;
    }
    /**
    * {@inheritdoc}
    */
    public function getActiveWorkspaceId(Request $request): ?string {
    return 'test';
    }
    /**
    * {@inheritdoc}
    */
    public function getActiveWorkspace(Request $request) {
    return Workspace::create(['id' => 'test', 'label' => 'Test']);
    return Workspace::load($this->getActiveWorkspaceId($request));
    }
    /**
    ......
    ......@@ -7,6 +7,7 @@
    use Drupal\Tests\BrowserTestBase;
    use Drupal\Tests\UpdatePathTestTrait;
    use Drupal\Tests\user\Traits\UserCreationTrait;
    use Drupal\workspaces\Entity\Workspace;
    /**
    * Tests that there is no active workspace during database updates.
    ......@@ -45,6 +46,9 @@ protected function setUp(): void {
    $index = array_search('workspace_update_test_post_update_check_active_workspace', $existing_updates);
    unset($existing_updates[$index]);
    \Drupal::keyValue('post_update')->set('existing_updates', $existing_updates);
    // Create a valid workspace that can be used for testing.
    Workspace::create(['id' => 'test', 'label' => 'Test'])->save();
    }
    /**
    ......
    <?php
    declare(strict_types=1);
    namespace Drupal\Tests\workspaces\Kernel;
    use Drupal\Component\Utility\Crypt;
    use Drupal\KernelTests\KernelTestBase;
    use Drupal\Tests\user\Traits\UserCreationTrait;
    use Drupal\workspaces\Entity\Workspace;
    /**
    * Tests the query parameter workspace negotiator.
    *
    * @coversDefaultClass \Drupal\workspaces\Negotiator\QueryParameterWorkspaceNegotiator
    * @group workspaces
    */
    class WorkspaceQueryParameterNegotiatorTest extends KernelTestBase {
    use UserCreationTrait;
    /**
    * {@inheritdoc}
    */
    protected static $modules = [
    'user',
    'system',
    'workspaces',
    ];
    /**
    * {@inheritdoc}
    */
    protected function setUp(): void {
    parent::setUp();
    $this->installEntitySchema('user');
    $this->installEntitySchema('workspace');
    $this->installSchema('workspaces', ['workspace_association']);
    // Create a new workspace for testing.
    Workspace::create(['id' => 'stage', 'label' => 'Stage'])->save();
    $this->setCurrentUser($this->createUser(['administer workspaces']));
    // Reset the internal state of the workspace manager so that checking for an
    // active workspace in the test is not influenced by previous actions.
    \Drupal::getContainer()->set('workspaces.manager', NULL);
    }
    /**
    * @covers ::getActiveWorkspaceId
    * @dataProvider providerTestWorkspaceQueryParameter
    */
    public function testWorkspaceQueryParameter(?string $workspace, ?string $token, ?string $negotiated_workspace, bool $has_active_workspace): void {
    // We can't access the settings service in the data provider method, so we
    // generate a good token here.
    if ($token === 'good_token') {
    $hash_salt = $this->container->get('settings')->get('hash_salt');
    $token = substr(Crypt::hmacBase64($workspace, $hash_salt), 0, 8);
    }
    $request = \Drupal::request();
    $request->query->set('workspace', $workspace);
    $request->query->set('token', $token);
    /** @var \Drupal\workspaces\Negotiator\QueryParameterWorkspaceNegotiator $negotiator */
    $negotiator = $this->container->get('workspaces.negotiator.query_parameter');
    $this->assertSame($negotiated_workspace, $negotiator->getActiveWorkspaceId($request));
    $this->assertSame($has_active_workspace, \Drupal::service('workspaces.manager')->hasActiveWorkspace());
    }
    /**
    * Data provider for testWorkspaceQueryParameter.
    */
    public static function providerTestWorkspaceQueryParameter(): array {
    return [
    'no workspace, no token' => [
    'workspace' => NULL,
    'token' => NULL,
    'negotiated_workspace' => NULL,
    'has_active_workspace' => FALSE,
    ],
    'fake workspace, no token' => [
    'workspace' => 'fake_id',
    'token' => NULL,
    'negotiated_workspace' => NULL,
    'has_active_workspace' => FALSE,
    ],
    'fake workspace, fake token' => [
    'workspace' => 'fake_id',
    'token' => 'fake_token',
    'negotiated_workspace' => NULL,
    'has_active_workspace' => FALSE,
    ],
    'good workspace, fake token' => [
    'workspace' => 'stage',
    'token' => 'fake_token',
    'negotiated_workspace' => NULL,
    'has_active_workspace' => FALSE,
    ],
    // The fake workspace will be accepted by the negotiator in this case, but
    // the workspace manager will try to load and check access for it, and
    // won't set it as the active workspace. Note that "fake" can also mean a
    // workspace that existed at some point, then it was deleted and the user
    // is just accessing a stale link.
    'fake workspace, good token' => [
    'workspace' => 'fake_id',
    'token' => 'good_token',
    'negotiated_workspace' => 'fake_id',
    'has_active_workspace' => FALSE,
    ],
    'good workspace, good token' => [
    'workspace' => 'stage',
    'token' => 'good_token',
    'negotiated_workspace' => 'stage',
    'has_active_workspace' => TRUE,
    ],
    ];
    }
    }
    0% Loading or .
    You are about to add 0 people to the discussion. Proceed with caution.
    Finish editing this message first!
    Please register or to comment