Chris@17: 'workspace_association', Chris@17: 'workspace' => 'workspace', Chris@17: ]; Chris@17: Chris@17: /** Chris@17: * The request stack. Chris@17: * Chris@17: * @var \Symfony\Component\HttpFoundation\RequestStack Chris@17: */ Chris@17: protected $requestStack; Chris@17: Chris@17: /** Chris@17: * The entity type manager. Chris@17: * Chris@17: * @var \Drupal\Core\Entity\EntityTypeManagerInterface Chris@17: */ Chris@17: protected $entityTypeManager; Chris@17: Chris@17: /** Chris@17: * The entity memory cache service. Chris@17: * Chris@17: * @var \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface Chris@17: */ Chris@17: protected $entityMemoryCache; Chris@17: Chris@17: /** Chris@17: * The current user. Chris@17: * Chris@17: * @var \Drupal\Core\Session\AccountProxyInterface Chris@17: */ Chris@17: protected $currentUser; Chris@17: Chris@17: /** Chris@17: * The state service. Chris@17: * Chris@17: * @var \Drupal\Core\State\StateInterface Chris@17: */ Chris@17: protected $state; Chris@17: Chris@17: /** Chris@17: * A logger instance. Chris@17: * Chris@17: * @var \Psr\Log\LoggerInterface Chris@17: */ Chris@17: protected $logger; Chris@17: Chris@17: /** Chris@17: * The class resolver. Chris@17: * Chris@17: * @var \Drupal\Core\DependencyInjection\ClassResolverInterface Chris@17: */ Chris@17: protected $classResolver; Chris@17: Chris@17: /** Chris@17: * The workspace negotiator service IDs. Chris@17: * Chris@17: * @var array Chris@17: */ Chris@17: protected $negotiatorIds; Chris@17: Chris@17: /** Chris@17: * The current active workspace. Chris@17: * Chris@17: * @var \Drupal\workspaces\WorkspaceInterface Chris@17: */ Chris@17: protected $activeWorkspace; Chris@17: Chris@17: /** Chris@17: * Constructs a new WorkspaceManager. Chris@17: * Chris@17: * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack Chris@17: * The request stack. Chris@17: * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager Chris@17: * The entity type manager. Chris@17: * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $entity_memory_cache Chris@17: * The entity memory cache service. Chris@17: * @param \Drupal\Core\Session\AccountProxyInterface $current_user Chris@17: * The current user. Chris@17: * @param \Drupal\Core\State\StateInterface $state Chris@17: * The state service. Chris@17: * @param \Psr\Log\LoggerInterface $logger Chris@17: * A logger instance. Chris@17: * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver Chris@17: * The class resolver. Chris@17: * @param array $negotiator_ids Chris@17: * The workspace negotiator service IDs. Chris@17: */ Chris@17: public function __construct(RequestStack $request_stack, EntityTypeManagerInterface $entity_type_manager, MemoryCacheInterface $entity_memory_cache, AccountProxyInterface $current_user, StateInterface $state, LoggerInterface $logger, ClassResolverInterface $class_resolver, array $negotiator_ids) { Chris@17: $this->requestStack = $request_stack; Chris@17: $this->entityTypeManager = $entity_type_manager; Chris@17: $this->entityMemoryCache = $entity_memory_cache; Chris@17: $this->currentUser = $current_user; Chris@17: $this->state = $state; Chris@17: $this->logger = $logger; Chris@17: $this->classResolver = $class_resolver; Chris@17: $this->negotiatorIds = $negotiator_ids; Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: public function isEntityTypeSupported(EntityTypeInterface $entity_type) { Chris@17: // First, check if we already determined whether this entity type is Chris@17: // supported or not. Chris@17: if (isset($this->blacklist[$entity_type->id()])) { Chris@17: return FALSE; Chris@17: } Chris@17: Chris@17: if ($entity_type->entityClassImplements(EntityPublishedInterface::class) && $entity_type->isRevisionable()) { Chris@17: return TRUE; Chris@17: } Chris@17: Chris@17: // This entity type can not belong to a workspace, add it to the blacklist. Chris@17: $this->blacklist[$entity_type->id()] = $entity_type->id(); Chris@17: return FALSE; Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: public function getSupportedEntityTypes() { Chris@17: $entity_types = []; Chris@17: foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) { Chris@17: if ($this->isEntityTypeSupported($entity_type)) { Chris@17: $entity_types[$entity_type_id] = $entity_type; Chris@17: } Chris@17: } Chris@17: return $entity_types; Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: public function getActiveWorkspace() { Chris@17: if (!isset($this->activeWorkspace)) { Chris@17: $request = $this->requestStack->getCurrentRequest(); Chris@17: foreach ($this->negotiatorIds as $negotiator_id) { Chris@17: $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id); Chris@17: if ($negotiator->applies($request)) { Chris@17: if ($this->activeWorkspace = $negotiator->getActiveWorkspace($request)) { Chris@17: break; Chris@17: } Chris@17: } Chris@17: } Chris@17: } Chris@17: Chris@17: // The default workspace negotiator always returns a valid workspace. Chris@17: return $this->activeWorkspace; Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: public function setActiveWorkspace(WorkspaceInterface $workspace) { Chris@17: $this->doSwitchWorkspace($workspace); Chris@17: Chris@17: // Set the workspace on the proper negotiator. Chris@17: $request = $this->requestStack->getCurrentRequest(); Chris@17: foreach ($this->negotiatorIds as $negotiator_id) { Chris@17: $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id); Chris@17: if ($negotiator->applies($request)) { Chris@17: $negotiator->setActiveWorkspace($workspace); Chris@17: break; Chris@17: } Chris@17: } Chris@17: Chris@17: return $this; Chris@17: } Chris@17: Chris@17: /** Chris@17: * Switches the current workspace. Chris@17: * Chris@17: * @param \Drupal\workspaces\WorkspaceInterface $workspace Chris@17: * The workspace to set as active. Chris@17: * Chris@17: * @throws \Drupal\workspaces\WorkspaceAccessException Chris@17: * Thrown when the current user doesn't have access to view the workspace. Chris@17: */ Chris@17: protected function doSwitchWorkspace(WorkspaceInterface $workspace) { Chris@17: // If the current user doesn't have access to view the workspace, they Chris@17: // shouldn't be allowed to switch to it. Chris@17: if (!$workspace->access('view') && !$workspace->isDefaultWorkspace()) { Chris@17: $this->logger->error('Denied access to view workspace %workspace_label for user %uid', [ Chris@17: '%workspace_label' => $workspace->label(), Chris@17: '%uid' => $this->currentUser->id(), Chris@17: ]); Chris@17: throw new WorkspaceAccessException('The user does not have permission to view that workspace.'); Chris@17: } Chris@17: Chris@17: $this->activeWorkspace = $workspace; Chris@17: Chris@17: // Clear the static entity cache for the supported entity types. Chris@17: $cache_tags_to_invalidate = array_map(function ($entity_type_id) { Chris@17: return 'entity.memory_cache:' . $entity_type_id; Chris@17: }, array_keys($this->getSupportedEntityTypes())); Chris@17: $this->entityMemoryCache->invalidateTags($cache_tags_to_invalidate); Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: public function executeInWorkspace($workspace_id, callable $function) { Chris@17: /** @var \Drupal\workspaces\WorkspaceInterface $workspace */ Chris@17: $workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id); Chris@17: Chris@17: if (!$workspace) { Chris@17: throw new \InvalidArgumentException('The ' . $workspace_id . ' workspace does not exist.'); Chris@17: } Chris@17: Chris@17: $previous_active_workspace = $this->getActiveWorkspace(); Chris@17: $this->doSwitchWorkspace($workspace); Chris@17: $result = $function(); Chris@17: $this->doSwitchWorkspace($previous_active_workspace); Chris@17: Chris@17: return $result; Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: public function shouldAlterOperations(EntityTypeInterface $entity_type) { Chris@17: return $this->isEntityTypeSupported($entity_type) && !$this->getActiveWorkspace()->isDefaultWorkspace(); Chris@17: } Chris@17: Chris@17: /** Chris@17: * {@inheritdoc} Chris@17: */ Chris@17: public function purgeDeletedWorkspacesBatch() { Chris@17: $deleted_workspace_ids = $this->state->get('workspace.deleted', []); Chris@17: Chris@17: // Bail out early if there are no workspaces to purge. Chris@17: if (empty($deleted_workspace_ids)) { Chris@17: return; Chris@17: } Chris@17: Chris@17: $batch_size = Settings::get('entity_update_batch_size', 50); Chris@17: Chris@17: /** @var \Drupal\workspaces\WorkspaceAssociationStorageInterface $workspace_association_storage */ Chris@17: $workspace_association_storage = $this->entityTypeManager->getStorage('workspace_association'); Chris@17: Chris@17: // Get the first deleted workspace from the list and delete the revisions Chris@17: // associated with it, along with the workspace_association entries. Chris@17: $workspace_id = reset($deleted_workspace_ids); Chris@17: $workspace_association_ids = $this->getWorkspaceAssociationRevisionsToPurge($workspace_id, $batch_size); Chris@17: Chris@17: if ($workspace_association_ids) { Chris@17: $workspace_associations = $workspace_association_storage->loadMultipleRevisions(array_keys($workspace_association_ids)); Chris@17: foreach ($workspace_associations as $workspace_association) { Chris@17: $associated_entity_storage = $this->entityTypeManager->getStorage($workspace_association->target_entity_type_id->value); Chris@17: // Delete the associated entity revision. Chris@17: if ($entity = $associated_entity_storage->loadRevision($workspace_association->target_entity_revision_id->value)) { Chris@17: if ($entity->isDefaultRevision()) { Chris@17: $entity->delete(); Chris@17: } Chris@17: else { Chris@17: $associated_entity_storage->deleteRevision($workspace_association->target_entity_revision_id->value); Chris@17: } Chris@17: } Chris@17: Chris@17: // Delete the workspace_association revision. Chris@17: if ($workspace_association->isDefaultRevision()) { Chris@17: $workspace_association->delete(); Chris@17: } Chris@17: else { Chris@17: $workspace_association_storage->deleteRevision($workspace_association->getRevisionId()); Chris@17: } Chris@17: } Chris@17: } Chris@17: Chris@17: // The purging operation above might have taken a long time, so we need to Chris@17: // request a fresh list of workspace association IDs. If it is empty, we can Chris@17: // go ahead and remove the deleted workspace ID entry from state. Chris@17: if (!$this->getWorkspaceAssociationRevisionsToPurge($workspace_id, $batch_size)) { Chris@17: unset($deleted_workspace_ids[$workspace_id]); Chris@17: $this->state->set('workspace.deleted', $deleted_workspace_ids); Chris@17: } Chris@17: } Chris@17: Chris@17: /** Chris@17: * Gets a list of workspace association IDs to purge. Chris@17: * Chris@17: * @param string $workspace_id Chris@17: * The ID of the workspace. Chris@17: * @param int $batch_size Chris@17: * The maximum number of records that will be purged. Chris@17: * Chris@17: * @return array Chris@17: * An array of workspace association IDs, keyed by their revision IDs. Chris@17: */ Chris@17: protected function getWorkspaceAssociationRevisionsToPurge($workspace_id, $batch_size) { Chris@17: return $this->entityTypeManager->getStorage('workspace_association') Chris@17: ->getQuery() Chris@17: ->allRevisions() Chris@17: ->accessCheck(FALSE) Chris@17: ->condition('workspace', $workspace_id) Chris@17: ->sort('revision_id', 'ASC') Chris@17: ->range(0, $batch_size) Chris@17: ->execute(); Chris@17: } Chris@17: Chris@17: }