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