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