Pull merge.
[yaffs-website] / web / core / modules / workspaces / src / WorkspaceManager.php
1 <?php
2
3 namespace Drupal\workspaces;
4
5 use Drupal\Core\Cache\MemoryCache\MemoryCacheInterface;
6 use Drupal\Core\DependencyInjection\ClassResolverInterface;
7 use Drupal\Core\Entity\EntityPublishedInterface;
8 use Drupal\Core\Entity\EntityTypeInterface;
9 use Drupal\Core\Entity\EntityTypeManagerInterface;
10 use Drupal\Core\Session\AccountProxyInterface;
11 use Drupal\Core\Site\Settings;
12 use Drupal\Core\State\StateInterface;
13 use Drupal\Core\StringTranslation\StringTranslationTrait;
14 use Psr\Log\LoggerInterface;
15 use Symfony\Component\HttpFoundation\RequestStack;
16
17 /**
18  * Provides the workspace manager.
19  */
20 class WorkspaceManager implements WorkspaceManagerInterface {
21
22   use StringTranslationTrait;
23
24   /**
25    * An array of entity type IDs that can not belong to a workspace.
26    *
27    * By default, only entity types which are revisionable and publishable can
28    * belong to a workspace.
29    *
30    * @var string[]
31    */
32   protected $blacklist = [
33     'workspace_association',
34     'workspace',
35   ];
36
37   /**
38    * The request stack.
39    *
40    * @var \Symfony\Component\HttpFoundation\RequestStack
41    */
42   protected $requestStack;
43
44   /**
45    * The entity type manager.
46    *
47    * @var \Drupal\Core\Entity\EntityTypeManagerInterface
48    */
49   protected $entityTypeManager;
50
51   /**
52    * The entity memory cache service.
53    *
54    * @var \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface
55    */
56   protected $entityMemoryCache;
57
58   /**
59    * The current user.
60    *
61    * @var \Drupal\Core\Session\AccountProxyInterface
62    */
63   protected $currentUser;
64
65   /**
66    * The state service.
67    *
68    * @var \Drupal\Core\State\StateInterface
69    */
70   protected $state;
71
72   /**
73    * A logger instance.
74    *
75    * @var \Psr\Log\LoggerInterface
76    */
77   protected $logger;
78
79   /**
80    * The class resolver.
81    *
82    * @var \Drupal\Core\DependencyInjection\ClassResolverInterface
83    */
84   protected $classResolver;
85
86   /**
87    * The workspace negotiator service IDs.
88    *
89    * @var array
90    */
91   protected $negotiatorIds;
92
93   /**
94    * The current active workspace.
95    *
96    * @var \Drupal\workspaces\WorkspaceInterface
97    */
98   protected $activeWorkspace;
99
100   /**
101    * Constructs a new WorkspaceManager.
102    *
103    * @param \Symfony\Component\HttpFoundation\RequestStack $request_stack
104    *   The request stack.
105    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
106    *   The entity type manager.
107    * @param \Drupal\Core\Cache\MemoryCache\MemoryCacheInterface $entity_memory_cache
108    *   The entity memory cache service.
109    * @param \Drupal\Core\Session\AccountProxyInterface $current_user
110    *   The current user.
111    * @param \Drupal\Core\State\StateInterface $state
112    *   The state service.
113    * @param \Psr\Log\LoggerInterface $logger
114    *   A logger instance.
115    * @param \Drupal\Core\DependencyInjection\ClassResolverInterface $class_resolver
116    *   The class resolver.
117    * @param array $negotiator_ids
118    *   The workspace negotiator service IDs.
119    */
120   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) {
121     $this->requestStack = $request_stack;
122     $this->entityTypeManager = $entity_type_manager;
123     $this->entityMemoryCache = $entity_memory_cache;
124     $this->currentUser = $current_user;
125     $this->state = $state;
126     $this->logger = $logger;
127     $this->classResolver = $class_resolver;
128     $this->negotiatorIds = $negotiator_ids;
129   }
130
131   /**
132    * {@inheritdoc}
133    */
134   public function isEntityTypeSupported(EntityTypeInterface $entity_type) {
135     if (!isset($this->blacklist[$entity_type->id()])
136       && $entity_type->entityClassImplements(EntityPublishedInterface::class)
137       && $entity_type->isRevisionable()) {
138       return TRUE;
139     }
140     $this->blacklist[$entity_type->id()] = $entity_type->id();
141     return FALSE;
142   }
143
144   /**
145    * {@inheritdoc}
146    */
147   public function getSupportedEntityTypes() {
148     $entity_types = [];
149     foreach ($this->entityTypeManager->getDefinitions() as $entity_type_id => $entity_type) {
150       if ($this->isEntityTypeSupported($entity_type)) {
151         $entity_types[$entity_type_id] = $entity_type;
152       }
153     }
154     return $entity_types;
155   }
156
157   /**
158    * {@inheritdoc}
159    */
160   public function getActiveWorkspace() {
161     if (!isset($this->activeWorkspace)) {
162       $request = $this->requestStack->getCurrentRequest();
163       foreach ($this->negotiatorIds as $negotiator_id) {
164         $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
165         if ($negotiator->applies($request)) {
166           if ($this->activeWorkspace = $negotiator->getActiveWorkspace($request)) {
167             break;
168           }
169         }
170       }
171     }
172
173     // The default workspace negotiator always returns a valid workspace.
174     return $this->activeWorkspace;
175   }
176
177   /**
178    * {@inheritdoc}
179    */
180   public function setActiveWorkspace(WorkspaceInterface $workspace) {
181     $this->doSwitchWorkspace($workspace);
182
183     // Set the workspace on the proper negotiator.
184     $request = $this->requestStack->getCurrentRequest();
185     foreach ($this->negotiatorIds as $negotiator_id) {
186       $negotiator = $this->classResolver->getInstanceFromDefinition($negotiator_id);
187       if ($negotiator->applies($request)) {
188         $negotiator->setActiveWorkspace($workspace);
189         break;
190       }
191     }
192
193     return $this;
194   }
195
196   /**
197    * Switches the current workspace.
198    *
199    * @param \Drupal\workspaces\WorkspaceInterface $workspace
200    *   The workspace to set as active.
201    *
202    * @throws \Drupal\workspaces\WorkspaceAccessException
203    *   Thrown when the current user doesn't have access to view the workspace.
204    */
205   protected function doSwitchWorkspace(WorkspaceInterface $workspace) {
206     // If the current user doesn't have access to view the workspace, they
207     // shouldn't be allowed to switch to it.
208     if (!$workspace->access('view') && !$workspace->isDefaultWorkspace()) {
209       $this->logger->error('Denied access to view workspace %workspace_label for user %uid', [
210         '%workspace_label' => $workspace->label(),
211         '%uid' => $this->currentUser->id(),
212       ]);
213       throw new WorkspaceAccessException('The user does not have permission to view that workspace.');
214     }
215
216     $this->activeWorkspace = $workspace;
217
218     // Clear the static entity cache for the supported entity types.
219     $cache_tags_to_invalidate = array_map(function ($entity_type_id) {
220       return 'entity.memory_cache:' . $entity_type_id;
221     }, array_keys($this->getSupportedEntityTypes()));
222     $this->entityMemoryCache->invalidateTags($cache_tags_to_invalidate);
223   }
224
225   /**
226    * {@inheritdoc}
227    */
228   public function executeInWorkspace($workspace_id, callable $function) {
229     /** @var \Drupal\workspaces\WorkspaceInterface $workspace */
230     $workspace = $this->entityTypeManager->getStorage('workspace')->load($workspace_id);
231
232     if (!$workspace) {
233       throw new \InvalidArgumentException('The ' . $workspace_id . ' workspace does not exist.');
234     }
235
236     $previous_active_workspace = $this->getActiveWorkspace();
237     $this->doSwitchWorkspace($workspace);
238     $result = $function();
239     $this->doSwitchWorkspace($previous_active_workspace);
240
241     return $result;
242   }
243
244   /**
245    * {@inheritdoc}
246    */
247   public function shouldAlterOperations(EntityTypeInterface $entity_type) {
248     return $this->isEntityTypeSupported($entity_type) && !$this->getActiveWorkspace()->isDefaultWorkspace();
249   }
250
251   /**
252    * {@inheritdoc}
253    */
254   public function purgeDeletedWorkspacesBatch() {
255     $deleted_workspace_ids = $this->state->get('workspace.deleted', []);
256
257     // Bail out early if there are no workspaces to purge.
258     if (empty($deleted_workspace_ids)) {
259       return;
260     }
261
262     $batch_size = Settings::get('entity_update_batch_size', 50);
263
264     /** @var \Drupal\workspaces\WorkspaceAssociationStorageInterface $workspace_association_storage */
265     $workspace_association_storage = $this->entityTypeManager->getStorage('workspace_association');
266
267     // Get the first deleted workspace from the list and delete the revisions
268     // associated with it, along with the workspace_association entries.
269     $workspace_id = reset($deleted_workspace_ids);
270     $workspace_association_ids = $this->getWorkspaceAssociationRevisionsToPurge($workspace_id, $batch_size);
271
272     if ($workspace_association_ids) {
273       $workspace_associations = $workspace_association_storage->loadMultipleRevisions(array_keys($workspace_association_ids));
274       foreach ($workspace_associations as $workspace_association) {
275         $associated_entity_storage = $this->entityTypeManager->getStorage($workspace_association->target_entity_type_id->value);
276         // Delete the associated entity revision.
277         if ($entity = $associated_entity_storage->loadRevision($workspace_association->target_entity_revision_id->value)) {
278           if ($entity->isDefaultRevision()) {
279             $entity->delete();
280           }
281           else {
282             $associated_entity_storage->deleteRevision($workspace_association->target_entity_revision_id->value);
283           }
284         }
285
286         // Delete the workspace_association revision.
287         if ($workspace_association->isDefaultRevision()) {
288           $workspace_association->delete();
289         }
290         else {
291           $workspace_association_storage->deleteRevision($workspace_association->getRevisionId());
292         }
293       }
294     }
295
296     // The purging operation above might have taken a long time, so we need to
297     // request a fresh list of workspace association IDs. If it is empty, we can
298     // go ahead and remove the deleted workspace ID entry from state.
299     if (!$this->getWorkspaceAssociationRevisionsToPurge($workspace_id, $batch_size)) {
300       unset($deleted_workspace_ids[$workspace_id]);
301       $this->state->set('workspace.deleted', $deleted_workspace_ids);
302     }
303   }
304
305   /**
306    * Gets a list of workspace association IDs to purge.
307    *
308    * @param string $workspace_id
309    *   The ID of the workspace.
310    * @param int $batch_size
311    *   The maximum number of records that will be purged.
312    *
313    * @return array
314    *   An array of workspace association IDs, keyed by their revision IDs.
315    */
316   protected function getWorkspaceAssociationRevisionsToPurge($workspace_id, $batch_size) {
317     return $this->entityTypeManager->getStorage('workspace_association')
318       ->getQuery()
319       ->allRevisions()
320       ->accessCheck(FALSE)
321       ->condition('workspace', $workspace_id)
322       ->sort('revision_id', 'ASC')
323       ->range(0, $batch_size)
324       ->execute();
325   }
326
327 }