fc5c2b27308ac300647531e5c4a4cdc2f20daadb
[Packages/TYPO3.CMS.git] / typo3 / sysext / version / Classes / DataHandler / CommandMap.php
1 <?php
2 namespace TYPO3\CMS\Version\DataHandler;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2010-2013 Oliver Hader <oliver@typo3.org>
8 * All rights reserved
9 *
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
15 *
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 * A copy is found in the text file GPL.txt and important notices to the license
19 * from the author is found in LICENSE.txt distributed with these scripts.
20 *
21 *
22 * This script is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
26 *
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
29
30 use TYPO3\CMS\Backend\Utility\BackendUtility;
31 use TYPO3\CMS\Core\Versioning\VersionState;
32
33 /**
34 * Handles the \TYPO3\CMS\Core\DataHandling\DataHandler command map and is
35 * only used in combination with \TYPO3\CMS\Core\DataHandling\DataHandler
36 */
37 class CommandMap {
38
39 const SCOPE_WorkspacesSwap = 'SCOPE_WorkspacesSwap';
40 const SCOPE_WorkspacesSetStage = 'SCOPE_WorkspacesSetStage';
41 const SCOPE_WorkspacesClear = 'SCOPE_WorkspacesClear';
42 const KEY_ScopeErrorMessage = 'KEY_ScopeErrorMessage';
43 const KEY_ScopeErrorCode = 'KEY_ScopeErrorCode';
44 const KEY_GetElementPropertiesCallback = 'KEY_GetElementPropertiesCallback';
45 const KEY_GetCommonPropertiesCallback = 'KEY_GetCommonPropertiesCallback';
46 const KEY_ElementConstructCallback = 'KEY_EventConstructCallback';
47 const KEY_ElementCreateChildReferenceCallback = 'KEY_ElementCreateChildReferenceCallback';
48 const KEY_ElementCreateParentReferenceCallback = 'KEY_ElementCreateParentReferenceCallback';
49 const KEY_PurgeWithErrorMessageGetIdCallback = 'KEY_PurgeWithErrorMessageGetIdCallback';
50 const KEY_UpdateGetIdCallback = 'KEY_UpdateGetIdCallback';
51 const KEY_TransformDependentElementsToUseLiveId = 'KEY_TransformDependentElementsToUseLiveId';
52 /**
53 * @var \TYPO3\CMS\Version\Hook\DataHandlerHook
54 */
55 protected $parent;
56
57 /**
58 * @var \TYPO3\CMS\Core\DataHandling\DataHandler
59 */
60 protected $tceMain;
61
62 /**
63 * @var array
64 */
65 protected $commandMap = array();
66
67 /**
68 * @var int
69 */
70 protected $workspace;
71
72 /**
73 * @var string
74 */
75 protected $workspacesSwapMode;
76
77 /**
78 * @var string
79 */
80 protected $workspacesChangeStageMode;
81
82 /**
83 * @var boolean
84 */
85 protected $workspacesConsiderReferences;
86
87 /**
88 * @var array
89 */
90 protected $scopes;
91
92 /**
93 * Creates this object.
94 *
95 * @param \TYPO3\CMS\Version\Hook\DataHandlerHook $parent
96 * @param \TYPO3\CMS\Core\DataHandling\DataHandler $tceMain
97 * @param array $commandMap
98 */
99 public function __construct(\TYPO3\CMS\Version\Hook\DataHandlerHook $parent, \TYPO3\CMS\Core\DataHandling\DataHandler $tceMain, array $commandMap, $workspace) {
100 $this->setParent($parent);
101 $this->setTceMain($tceMain);
102 $this->set($commandMap);
103 $this->setWorkspace($workspace);
104 $this->setWorkspacesSwapMode($this->getTceMain()->BE_USER->getTSConfigVal('options.workspaces.swapMode'));
105 $this->setWorkspacesChangeStageMode($this->getTceMain()->BE_USER->getTSConfigVal('options.workspaces.changeStageMode'));
106 $this->setWorkspacesConsiderReferences($this->getTceMain()->BE_USER->getTSConfigVal('options.workspaces.considerReferences'));
107 $this->constructScopes();
108 }
109
110 /**
111 * Gets the command map.
112 *
113 * @return array
114 */
115 public function get() {
116 return $this->commandMap;
117 }
118
119 /**
120 * Sets the command map.
121 *
122 * @param array $commandMap
123 * @return \TYPO3\CMS\Version\DataHandler\CommandMap
124 */
125 public function set(array $commandMap) {
126 $this->commandMap = $commandMap;
127 return $this;
128 }
129
130 /**
131 * Gets the parent object.
132 *
133 * @return \TYPO3\CMS\Version\Hook\DataHandlerHook
134 */
135 public function getParent() {
136 return $this->parent;
137 }
138
139 /**
140 * Sets the parent object.
141 *
142 * @param \TYPO3\CMS\Version\Hook\DataHandlerHook $parent
143 * @return \TYPO3\CMS\Version\DataHandler\CommandMap
144 */
145 public function setParent(\TYPO3\CMS\Version\Hook\DataHandlerHook $parent) {
146 $this->parent = $parent;
147 return $this;
148 }
149
150 /**
151 * Gets the parent object.
152 *
153 * @return \TYPO3\CMS\Core\DataHandling\DataHandler
154 */
155 public function getTceMain() {
156 return $this->tceMain;
157 }
158
159 /**
160 * Sets the parent object.
161 *
162 * @param \TYPO3\CMS\Core\DataHandling\DataHandler $tceMain
163 * @return \TYPO3\CMS\Version\DataHandler\CommandMap
164 */
165 public function setTceMain(\TYPO3\CMS\Core\DataHandling\DataHandler $tceMain) {
166 $this->tceMain = $tceMain;
167 return $this;
168 }
169
170 /**
171 * Sets the current workspace.
172 *
173 * @param int $workspace
174 */
175 public function setWorkspace($workspace) {
176 $this->workspace = (int)$workspace;
177 }
178
179 /**
180 * Gets the current workspace.
181 *
182 * @return int
183 */
184 public function getWorkspace() {
185 return $this->workspace;
186 }
187
188 /**
189 * Sets the workspaces swap mode
190 * (see options.workspaces.swapMode).
191 *
192 * @param string $workspacesSwapMode
193 * @return \TYPO3\CMS\Version\DataHandler\CommandMap
194 */
195 public function setWorkspacesSwapMode($workspacesSwapMode) {
196 $this->workspacesSwapMode = (string) $workspacesSwapMode;
197 return $this;
198 }
199
200 /**
201 * Sets the workspaces change stage mode
202 * see options.workspaces.changeStageMode)
203 *
204 * @param string $workspacesChangeStageMode
205 * @return \TYPO3\CMS\Version\DataHandler\CommandMap
206 */
207 public function setWorkspacesChangeStageMode($workspacesChangeStageMode) {
208 $this->workspacesChangeStageMode = (string) $workspacesChangeStageMode;
209 return $this;
210 }
211
212 /**
213 * Sets the workspace behaviour to automatically consider references
214 * (see options.workspaces.considerReferences)
215 *
216 * @param boolean $workspacesConsiderReferences
217 * @return \TYPO3\CMS\Version\DataHandler\CommandMap
218 */
219 public function setWorkspacesConsiderReferences($workspacesConsiderReferences) {
220 $this->workspacesConsiderReferences = (bool) $workspacesConsiderReferences;
221 return $this;
222 }
223
224 /**
225 * Processes the command map.
226 *
227 * @return \TYPO3\CMS\Version\DataHandler\CommandMap
228 */
229 public function process() {
230 $this->resolveWorkspacesSwapDependencies();
231 $this->resolveWorkspacesSetStageDependencies();
232 $this->resolveWorkspacesClearDependencies();
233 return $this;
234 }
235
236 /**
237 * Invokes all items for swapping/publishing with a callback method.
238 *
239 * @param string $callbackMethod
240 * @param array $arguments Optional leading arguments for the callback method
241 * @return void
242 */
243 protected function invokeWorkspacesSwapItems($callbackMethod, array $arguments = array()) {
244 // Traverses the cmd[] array and fetches the accordant actions:
245 foreach ($this->commandMap as $table => $liveIdCollection) {
246 foreach ($liveIdCollection as $liveId => $commandCollection) {
247 foreach ($commandCollection as $command => $properties) {
248 if ($command === 'version' && isset($properties['action']) && $properties['action'] === 'swap') {
249 if (isset($properties['swapWith']) && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($properties['swapWith'])) {
250 call_user_func_array(array($this, $callbackMethod), array_merge($arguments, array($table, $liveId, $properties)));
251 }
252 }
253 }
254 }
255 }
256 }
257
258 /**
259 * Resolves workspaces related dependencies for swapping/publishing of the command map.
260 * Workspaces records that have children or (relative) parents which are versionized
261 * but not published with this request, are removed from the command map. Otherwise
262 * this would produce hanging record sets and lost references.
263 *
264 * @return void
265 */
266 protected function resolveWorkspacesSwapDependencies() {
267 $scope = self::SCOPE_WorkspacesSwap;
268 $dependency = $this->getDependencyUtility($scope);
269 if (\TYPO3\CMS\Core\Utility\GeneralUtility::inList('any,pages', $this->workspacesSwapMode)) {
270 $this->invokeWorkspacesSwapItems('applyWorkspacesSwapBehaviour');
271 }
272 $this->invokeWorkspacesSwapItems('addWorkspacesSwapElements', array($dependency));
273 $this->applyWorkspacesDependencies($dependency, $scope);
274 }
275
276 /**
277 * Applies workspaces behaviour for swapping/publishing and takes care of the swapMode.
278 *
279 * @param string $table
280 * @param integer $liveId
281 * @param array $properties
282 * @return void
283 */
284 protected function applyWorkspacesSwapBehaviour($table, $liveId, array $properties) {
285 $extendedCommandMap = array();
286 $elementList = array();
287 // Fetch accordant elements if the swapMode is 'any' or 'pages':
288 if ($this->workspacesSwapMode === 'any' || $this->workspacesSwapMode === 'pages' && $table === 'pages') {
289 $elementList = $this->getParent()->findPageElementsForVersionSwap($table, $liveId, $properties['swapWith']);
290 }
291 foreach ($elementList as $elementTable => $elementIdArray) {
292 foreach ($elementIdArray as $elementIds) {
293 $extendedCommandMap[$elementTable][$elementIds[0]]['version'] = array_merge($properties, array('swapWith' => $elementIds[1]));
294 }
295 }
296 if (count($elementList) > 0) {
297 $this->remove($table, $liveId, 'version');
298 $this->mergeToBottom($extendedCommandMap);
299 }
300 }
301
302 /**
303 * Adds workspaces elements for swapping/publishing.
304 *
305 * @param \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency
306 * @param string $table
307 * @param integer $liveId
308 * @param array $properties
309 * @return void
310 */
311 protected function addWorkspacesSwapElements(\TYPO3\CMS\Version\Dependency\DependencyResolver $dependency, $table, $liveId, array $properties) {
312 $elementList = array();
313 // Fetch accordant elements if the swapMode is 'any' or 'pages':
314 if ($this->workspacesSwapMode === 'any' || $this->workspacesSwapMode === 'pages' && $table === 'pages') {
315 $elementList = $this->getParent()->findPageElementsForVersionSwap($table, $liveId, $properties['swapWith']);
316 }
317 foreach ($elementList as $elementTable => $elementIdArray) {
318 foreach ($elementIdArray as $elementIds) {
319 $dependency->addElement($elementTable, $elementIds[1], array('liveId' => $elementIds[0], 'properties' => array_merge($properties, array('swapWith' => $elementIds[1]))));
320 }
321 }
322 if (count($elementList) === 0) {
323 $dependency->addElement($table, $properties['swapWith'], array('liveId' => $liveId, 'properties' => $properties));
324 }
325 }
326
327 /**
328 * Invokes all items for staging with a callback method.
329 *
330 * @param string $callbackMethod
331 * @param array $arguments Optional leading arguments for the callback method
332 * @return void
333 */
334 protected function invokeWorkspacesSetStageItems($callbackMethod, array $arguments = array()) {
335 // Traverses the cmd[] array and fetches the accordant actions:
336 foreach ($this->commandMap as $table => $versionIdCollection) {
337 foreach ($versionIdCollection as $versionIdList => $commandCollection) {
338 foreach ($commandCollection as $command => $properties) {
339 if ($command === 'version' && isset($properties['action']) && $properties['action'] === 'setStage') {
340 if (isset($properties['stageId']) && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($properties['stageId'])) {
341 call_user_func_array(array($this, $callbackMethod), array_merge($arguments, array($table, $versionIdList, $properties)));
342 }
343 }
344 }
345 }
346 }
347 }
348
349 /**
350 * Resolves workspaces related dependencies for staging of the command map.
351 * Workspaces records that have children or (relative) parents which are versionized
352 * but not staged with this request, are removed from the command map.
353 *
354 * @return void
355 */
356 protected function resolveWorkspacesSetStageDependencies() {
357 $scope = self::SCOPE_WorkspacesSetStage;
358 $dependency = $this->getDependencyUtility($scope);
359 if (\TYPO3\CMS\Core\Utility\GeneralUtility::inList('any,pages', $this->workspacesChangeStageMode)) {
360 $this->invokeWorkspacesSetStageItems('applyWorkspacesSetStageBehaviour');
361 }
362 $this->invokeWorkspacesSetStageItems('explodeSetStage');
363 $this->invokeWorkspacesSetStageItems('addWorkspacesSetStageElements', array($dependency));
364 $this->applyWorkspacesDependencies($dependency, $scope);
365 }
366
367 /**
368 * Applies workspaces behaviour for staging and takes care of the changeStageMode.
369 *
370 * @param string $table
371 * @param string $versionIdList
372 * @param array $properties
373 * @return void
374 */
375 protected function applyWorkspacesSetStageBehaviour($table, $versionIdList, array $properties) {
376 $extendedCommandMap = array();
377 $versionIds = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $versionIdList, TRUE);
378 $elementList = array($table => $versionIds);
379 if (\TYPO3\CMS\Core\Utility\GeneralUtility::inList('any,pages', $this->workspacesChangeStageMode)) {
380 if (count($versionIds) === 1) {
381 $workspaceRecord = BackendUtility::getRecord($table, $versionIds[0], 't3ver_wsid');
382 $workspaceId = $workspaceRecord['t3ver_wsid'];
383 } else {
384 $workspaceId = $this->getWorkspace();
385 }
386 if ($table === 'pages') {
387 // Find all elements from the same ws to change stage
388 $livePageIds = $versionIds;
389 $this->getParent()->findRealPageIds($livePageIds);
390 $this->getParent()->findPageElementsForVersionStageChange($livePageIds, $workspaceId, $elementList);
391 } elseif ($this->workspacesChangeStageMode === 'any') {
392 // Find page to change stage:
393 $pageIdList = array();
394 $this->getParent()->findPageIdsForVersionStateChange($table, $versionIds, $workspaceId, $pageIdList, $elementList);
395 // Find other elements from the same ws to change stage:
396 $this->getParent()->findPageElementsForVersionStageChange($pageIdList, $workspaceId, $elementList);
397 }
398 }
399 foreach ($elementList as $elementTable => $elementIds) {
400 foreach ($elementIds as $elementId) {
401 $extendedCommandMap[$elementTable][$elementId]['version'] = $properties;
402 }
403 }
404 $this->remove($table, $versionIds, 'version');
405 $this->mergeToBottom($extendedCommandMap);
406 }
407
408 /**
409 * Adds workspaces elements for staging.
410 *
411 * @param \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency
412 * @param string $table
413 * @param string $versionId
414 * @param array $properties
415 * @return void
416 */
417 protected function addWorkspacesSetStageElements(\TYPO3\CMS\Version\Dependency\DependencyResolver $dependency, $table, $versionId, array $properties) {
418 $dependency->addElement($table, $versionId, array('versionId' => $versionId, 'properties' => $properties));
419 }
420
421 /**
422 * Resolves workspaces related dependencies for clearing/flushing of the command map.
423 * Workspaces records that have children or (relative) parents which are versionized
424 * but not cleared/flushed with this request, are removed from the command map.
425 *
426 * @return void
427 */
428 protected function resolveWorkspacesClearDependencies() {
429 $scope = self::SCOPE_WorkspacesClear;
430 $dependency = $this->getDependencyUtility($scope);
431 // Traverses the cmd[] array and fetches the accordant actions:
432 foreach ($this->commandMap as $table => $versionIdCollection) {
433 foreach ($versionIdCollection as $versionId => $commandCollection) {
434 foreach ($commandCollection as $command => $properties) {
435 if ($command === 'version' && isset($properties['action']) && ($properties['action'] === 'clearWSID' || $properties['action'] === 'flush')) {
436 $dependency->addElement($table, $versionId, array('versionId' => $versionId, 'properties' => $properties));
437 }
438 }
439 }
440 }
441 $this->applyWorkspacesDependencies($dependency, $scope);
442 }
443
444 /**
445 * Explodes id-lists in the command map for staging actions.
446 *
447 * @throws \RuntimeException
448 * @param string $table
449 * @param string $versionIdList
450 * @param array $properties
451 * @return void
452 */
453 protected function explodeSetStage($table, $versionIdList, array $properties) {
454 $extractedCommandMap = array();
455 $versionIds = \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $versionIdList, TRUE);
456 if (count($versionIds) > 1) {
457 foreach ($versionIds as $versionId) {
458 if (isset($this->commandMap[$table][$versionId]['version'])) {
459 throw new \RuntimeException('Command map for [' . $table . '][' . $versionId . '][version] was already set.', 1289391048);
460 }
461 $extractedCommandMap[$table][$versionId]['version'] = $properties;
462 }
463 $this->remove($table, $versionIdList, 'version');
464 $this->mergeToBottom($extractedCommandMap);
465 }
466 }
467
468 /**
469 * Applies the workspaces dependencies and removes incomplete structures or automatically
470 * completes them, depending on the options.workspaces.considerReferences setting
471 *
472 * @param \TYPO3\CMS\Version\Dependency\DependencyResolver $dependency
473 * @param string $scope
474 * @return void
475 */
476 protected function applyWorkspacesDependencies(\TYPO3\CMS\Version\Dependency\DependencyResolver $dependency, $scope) {
477 $transformDependentElementsToUseLiveId = $this->getScopeData($scope, self::KEY_TransformDependentElementsToUseLiveId);
478 $elementsToBeVersionized = $dependency->getElements();
479 // Use the uid of the live record instead of the workspace record:
480 if ($transformDependentElementsToUseLiveId) {
481 $elementsToBeVersionized = $this->transformDependentElementsToUseLiveId($elementsToBeVersionized);
482 }
483 $outerMostParents = $dependency->getOuterMostParents();
484 /** @var $outerMostParent \TYPO3\CMS\Version\Dependency\ElementEntity */
485 foreach ($outerMostParents as $outerMostParent) {
486 $dependentElements = $dependency->getNestedElements($outerMostParent);
487 if ($transformDependentElementsToUseLiveId) {
488 $dependentElements = $this->transformDependentElementsToUseLiveId($dependentElements);
489 }
490 // Gets the difference (intersection) between elements that were submitted by the user
491 // and the evaluation of all dependent records that should be used for this action instead:
492 $intersectingElements = array_intersect_key($dependentElements, $elementsToBeVersionized);
493 if (count($intersectingElements) > 0) {
494 // If at least one element intersects but not all, throw away all elements of the depdendent structure:
495 if (count($intersectingElements) !== count($dependentElements) && $this->workspacesConsiderReferences === FALSE) {
496 $this->purgeWithErrorMessage($intersectingElements, $scope);
497 } else {
498 $this->update(current($intersectingElements), $dependentElements, $scope);
499 }
500 }
501 }
502 }
503
504 /**
505 * Purges incomplete structures from the command map and triggers an error message.
506 *
507 * @param array $elements
508 * @param string $scope
509 * @return void
510 */
511 protected function purgeWithErrorMessage(array $elements, $scope) {
512 /** @var $element \TYPO3\CMS\Version\Dependency\ElementEntity */
513 foreach ($elements as $element) {
514 $table = $element->getTable();
515 $id = $this->processCallback($this->getScopeData($scope, self::KEY_PurgeWithErrorMessageGetIdCallback), array($element));
516 $this->remove($table, $id, 'version');
517 $this->getTceMain()->log($table, $id, 5, 0, 1, $this->getScopeData($scope, self::KEY_ScopeErrorMessage), $this->getScopeData($scope, self::KEY_ScopeErrorCode), array(
518 BackendUtility::getRecordTitle($table, BackendUtility::getRecord($table, $id)),
519 $table,
520 $id
521 ));
522 }
523 }
524
525 /**
526 * Updates the command map accordant to valid structures and takes care of the correct order.
527 *
528 * @param \TYPO3\CMS\Version\Dependency\ElementEntity $intersectingElement
529 * @param array $elements
530 * @param string $scope
531 * @return void
532 */
533 protected function update(\TYPO3\CMS\Version\Dependency\ElementEntity $intersectingElement, array $elements, $scope) {
534 $orderedCommandMap = array();
535 $commonProperties = array();
536 if ($this->getScopeData($scope, self::KEY_GetCommonPropertiesCallback)) {
537 $commonProperties = $this->processCallback($this->getScopeData($scope, self::KEY_GetCommonPropertiesCallback), array($intersectingElement));
538 }
539 /** @var $element \TYPO3\CMS\Version\Dependency\ElementEntity */
540 foreach ($elements as $element) {
541 $table = $element->getTable();
542 $id = $this->processCallback($this->getScopeData($scope, self::KEY_UpdateGetIdCallback), array($element));
543 $this->remove($table, $id, 'version');
544 if ($element->isInvalid()) {
545 continue;
546 }
547 $orderedCommandMap[$table][$id]['version'] = $commonProperties;
548 if ($this->getScopeData($scope, self::KEY_GetElementPropertiesCallback)) {
549 $orderedCommandMap[$table][$id]['version'] = array_merge($commonProperties, $this->processCallback($this->getScopeData($scope, self::KEY_GetElementPropertiesCallback), array($element)));
550 }
551 }
552 // Ensure that ordered command map is on top of the command map:
553 $this->mergeToTop($orderedCommandMap);
554 }
555
556 /**
557 * Merges command map elements to the top of the current command map..
558 *
559 * @param array $commandMap
560 * @return void
561 */
562 protected function mergeToTop(array $commandMap) {
563 \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($commandMap, $this->commandMap);
564 $this->commandMap = $commandMap;
565 }
566
567 /**
568 * Merges command map elements to the bottom of the current command map.
569 *
570 * @param array $commandMap
571 * @return void
572 */
573 protected function mergeToBottom(array $commandMap) {
574 \TYPO3\CMS\Core\Utility\ArrayUtility::mergeRecursiveWithOverrule($this->commandMap, $commandMap);
575 }
576
577 /**
578 * Removes an element from the command map.
579 *
580 * @param string $table
581 * @param string $id
582 * @param string $command (optional)
583 * @return void
584 */
585 protected function remove($table, $id, $command = NULL) {
586 if (is_string($command)) {
587 unset($this->commandMap[$table][$id][$command]);
588 } else {
589 unset($this->commandMap[$table][$id]);
590 }
591 }
592
593 /**
594 * Callback to get the liveId of an dependent element.
595 *
596 * @param \TYPO3\CMS\Version\Dependency\ElementEntity $element
597 * @return integer
598 */
599 protected function getElementLiveIdCallback(\TYPO3\CMS\Version\Dependency\ElementEntity $element) {
600 return $element->getDataValue('liveId');
601 }
602
603 /**
604 * Callback to get the real id of an dependent element.
605 *
606 * @param \TYPO3\CMS\Version\Dependency\ElementEntity $element
607 * @return integer
608 */
609 protected function getElementIdCallback(\TYPO3\CMS\Version\Dependency\ElementEntity $element) {
610 return $element->getId();
611 }
612
613 /**
614 * Callback to get the specific properties of a dependent element for swapping/publishing.
615 *
616 * @param \TYPO3\CMS\Version\Dependency\ElementEntity $element
617 * @return array
618 */
619 protected function getElementSwapPropertiesCallback(\TYPO3\CMS\Version\Dependency\ElementEntity $element) {
620 return array(
621 'swapWith' => $element->getId()
622 );
623 }
624
625 /**
626 * Callback to get common properties of dependent elements for clearing.
627 *
628 * @param \TYPO3\CMS\Version\Dependency\ElementEntity $element
629 * @return array
630 */
631 protected function getCommonClearPropertiesCallback(\TYPO3\CMS\Version\Dependency\ElementEntity $element) {
632 $commonSwapProperties = array();
633 $elementProperties = $element->getDataValue('properties');
634 if (isset($elementProperties['action'])) {
635 $commonSwapProperties['action'] = $elementProperties['action'];
636 }
637 return $commonSwapProperties;
638 }
639
640 /**
641 * Callback to get common properties of dependent elements for swapping/publishing.
642 *
643 * @param \TYPO3\CMS\Version\Dependency\ElementEntity $element
644 * @return array
645 */
646 protected function getCommonSwapPropertiesCallback(\TYPO3\CMS\Version\Dependency\ElementEntity $element) {
647 $commonSwapProperties = array();
648 $elementProperties = $element->getDataValue('properties');
649 if (isset($elementProperties['action'])) {
650 $commonSwapProperties['action'] = $elementProperties['action'];
651 }
652 if (isset($elementProperties['swapIntoWS'])) {
653 $commonSwapProperties['swapIntoWS'] = $elementProperties['swapIntoWS'];
654 }
655 if (isset($elementProperties['comment'])) {
656 $commonSwapProperties['comment'] = $elementProperties['comment'];
657 }
658 if (isset($elementProperties['notificationAlternativeRecipients'])) {
659 $commonSwapProperties['notificationAlternativeRecipients'] = $elementProperties['notificationAlternativeRecipients'];
660 }
661
662 return $commonSwapProperties;
663 }
664
665 /**
666 * Callback to get the specific properties of a dependent element for staging.
667 *
668 * @param \TYPO3\CMS\Version\Dependency\ElementEntity $element
669 * @return array
670 */
671 protected function getElementSetStagePropertiesCallback(\TYPO3\CMS\Version\Dependency\ElementEntity $element) {
672 return $this->getCommonSetStagePropertiesCallback($element);
673 }
674
675 /**
676 * Callback to get common properties of dependent elements for staging.
677 *
678 * @param \TYPO3\CMS\Version\Dependency\ElementEntity $element
679 * @return array
680 */
681 protected function getCommonSetStagePropertiesCallback(\TYPO3\CMS\Version\Dependency\ElementEntity $element) {
682 $commonSetStageProperties = array();
683 $elementProperties = $element->getDataValue('properties');
684 if (isset($elementProperties['stageId'])) {
685 $commonSetStageProperties['stageId'] = $elementProperties['stageId'];
686 }
687 if (isset($elementProperties['comment'])) {
688 $commonSetStageProperties['comment'] = $elementProperties['comment'];
689 }
690 if (isset($elementProperties['action'])) {
691 $commonSetStageProperties['action'] = $elementProperties['action'];
692 }
693 if (isset($elementProperties['notificationAlternativeRecipients'])) {
694 $commonSetStageProperties['notificationAlternativeRecipients'] = $elementProperties['notificationAlternativeRecipients'];
695 }
696 return $commonSetStageProperties;
697 }
698
699 /**
700 * Gets an instance of the depency resolver utility.
701 *
702 * @param string $scope Scope identifier
703 * @return \TYPO3\CMS\Version\Dependency\DependencyResolver
704 */
705 protected function getDependencyUtility($scope) {
706 /** @var $dependency \TYPO3\CMS\Version\Dependency\DependencyResolver */
707 $dependency = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Version\\Dependency\\DependencyResolver');
708 $dependency->setWorkspace($this->getWorkspace());
709 $dependency->setOuterMostParentsRequireReferences(TRUE);
710 if ($this->getScopeData($scope, self::KEY_ElementConstructCallback)) {
711 $dependency->setEventCallback(\TYPO3\CMS\Version\Dependency\ElementEntity::EVENT_Construct, $this->getDependencyCallback($this->getScopeData($scope, self::KEY_ElementConstructCallback)));
712 }
713 if ($this->getScopeData($scope, self::KEY_ElementCreateChildReferenceCallback)) {
714 $dependency->setEventCallback(\TYPO3\CMS\Version\Dependency\ElementEntity::EVENT_CreateChildReference, $this->getDependencyCallback($this->getScopeData($scope, self::KEY_ElementCreateChildReferenceCallback)));
715 }
716 if ($this->getScopeData($scope, self::KEY_ElementCreateParentReferenceCallback)) {
717 $dependency->setEventCallback(\TYPO3\CMS\Version\Dependency\ElementEntity::EVENT_CreateParentReference, $this->getDependencyCallback($this->getScopeData($scope, self::KEY_ElementCreateParentReferenceCallback)));
718 }
719 return $dependency;
720 }
721
722 /**
723 * Callback to determine whether a new child reference shall be considered in the dependency resolver utility.
724 *
725 * @param array $callerArguments
726 * @param array $targetArgument
727 * @param \TYPO3\CMS\Version\Dependency\ElementEntity $caller
728 * @param string $eventName
729 * @return string Skip response (if required)
730 */
731 public function createNewDependentElementChildReferenceCallback(array $callerArguments, array $targetArgument, \TYPO3\CMS\Version\Dependency\ElementEntity $caller, $eventName) {
732 $fieldConfiguration = BackendUtility::getTcaFieldConfiguration($caller->getTable(), $callerArguments['field']);
733 if (!$fieldConfiguration || !\TYPO3\CMS\Core\Utility\GeneralUtility::inList('field,list', $this->getTceMain()->getInlineFieldType($fieldConfiguration))) {
734 return \TYPO3\CMS\Version\Dependency\ElementEntity::RESPONSE_Skip;
735 }
736 }
737
738 /**
739 * Callback to determine whether a new parent reference shall be considered in the dependency resolver utility.
740 *
741 * @param array $callerArguments
742 * @param array $targetArgument
743 * @param \TYPO3\CMS\Version\Dependency\ElementEntity $caller
744 * @param string $eventName
745 * @return string Skip response (if required)
746 */
747 public function createNewDependentElementParentReferenceCallback(array $callerArguments, array $targetArgument, \TYPO3\CMS\Version\Dependency\ElementEntity $caller, $eventName) {
748 $fieldConfiguration = BackendUtility::getTcaFieldConfiguration($callerArguments['table'], $callerArguments['field']);
749 if (!$fieldConfiguration || !\TYPO3\CMS\Core\Utility\GeneralUtility::inList('field,list', $this->getTceMain()->getInlineFieldType($fieldConfiguration))) {
750 return \TYPO3\CMS\Version\Dependency\ElementEntity::RESPONSE_Skip;
751 }
752 }
753
754 /**
755 * Callback to determine whether a new child reference shall be considered in the dependency resolver utility.
756 * Only elements that are a delete placeholder are considered.
757 *
758 * @param array $callerArguments
759 * @param array $targetArgument
760 * @param \TYPO3\CMS\Version\Dependency\ElementEntity $caller
761 * @param string $eventName
762 * @return string Skip response (if required)
763 */
764 public function createClearDependentElementChildReferenceCallback(array $callerArguments, array $targetArgument, \TYPO3\CMS\Version\Dependency\ElementEntity $caller, $eventName) {
765 $response = $this->createNewDependentElementChildReferenceCallback($callerArguments, $targetArgument, $caller, $eventName);
766 if (empty($response)) {
767 $record = BackendUtility::getRecord($callerArguments['table'], $callerArguments['id']);
768 if (!VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
769 $response = \TYPO3\CMS\Version\Dependency\ElementEntity::RESPONSE_Skip;
770 }
771 }
772 return $response;
773 }
774
775 /**
776 * Callback to determine whether a new parent reference shall be considered in the dependency resolver utility.
777 * Only elements that are a delete placeholder are considered.
778 *
779 * @param array $callerArguments
780 * @param array $targetArgument
781 * @param \TYPO3\CMS\Version\Dependency\ElementEntity $caller
782 * @param string $eventName
783 * @return string Skip response (if required)
784 */
785 public function createClearDependentElementParentReferenceCallback(array $callerArguments, array $targetArgument, \TYPO3\CMS\Version\Dependency\ElementEntity $caller, $eventName) {
786 $response = $this->createNewDependentElementParentReferenceCallback($callerArguments, $targetArgument, $caller, $eventName);
787 if (empty($response)) {
788 $record = BackendUtility::getRecord($callerArguments['table'], $callerArguments['id']);
789 if (!VersionState::cast($record['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) {
790 $response = \TYPO3\CMS\Version\Dependency\ElementEntity::RESPONSE_Skip;
791 }
792 }
793 return $response;
794 }
795
796 /**
797 * Callback to add additional data to new elements created in the dependency resolver utility.
798 *
799 * @throws \RuntimeException
800 * @param \TYPO3\CMS\Version\Dependency\ElementEntity $caller
801 * @param array $callerArguments
802 * @param array $targetArgument
803 * @return void
804 */
805 public function createNewDependentElementCallback(array $callerArguments, array $targetArgument, \TYPO3\CMS\Version\Dependency\ElementEntity $caller) {
806 $versionRecord = $caller->getRecord();
807 // If version record does not exist, it probably has been deleted (cleared from workspace), this means,
808 // that the reference index still has an old reference pointer, which is "fine" for deleted parents
809 if (empty($versionRecord)) {
810 throw new \RuntimeException(
811 'Element "' . $caller::getIdentifier($caller->getTable(), $caller->getId()) . '" does not exist',
812 1393960943
813 );
814 }
815 // If version is on live workspace, but the pid is negative, mark the record as invalid.
816 // This happens if a change has been discarded (clearWSID) - it will be removed from the command map.
817 if ((int)$versionRecord['t3ver_wsid'] === 0 && (int)$versionRecord['pid'] === -1) {
818 $caller->setDataValue('liveId', $caller->getId());
819 $caller->setInvalid(TRUE);
820 return;
821 }
822 if ($caller->hasDataValue('liveId') === FALSE) {
823 // Set the original uid from the version record
824 if (!empty($versionRecord['t3ver_oid']) && (int)$versionRecord['pid'] === -1 && (int)$versionRecord['t3ver_wsid'] === $this->getWorkspace()) {
825 $caller->setDataValue('liveId', $versionRecord['t3ver_oid']);
826 // The current version record is actually a live record or an accordant placeholder for live
827 } elseif ((int)$versionRecord['t3ver_wsid'] === 0 || (int)$versionRecord['pid'] !== -1) {
828 $caller->setDataValue('liveId', $caller->getId());
829 $versionRecord = BackendUtility::getWorkspaceVersionOfRecord(
830 $this->getWorkspace(),
831 $caller->getTable(),
832 $caller->getId(),
833 'uid,t3ver_state'
834 );
835 // Set version uid to caller, most likely it's a delete placeholder
836 // for a child record that is not recognized in the reference index
837 if (!empty($versionRecord['uid'])) {
838 $caller->setId($versionRecord['uid']);
839 // If no version could be determined, mark record as invalid
840 // (thus, it will be removed from the command map)
841 } else {
842 $caller->setInvalid(TRUE);
843 }
844 // In case of an unexpected record state, mark the record as invalid
845 } else {
846 $caller->setInvalid(TRUE);
847 }
848 }
849 }
850
851 /**
852 * Transforms dependent elements to use the liveId as array key.
853 *
854 * @param $elements array<\TYPO3\CMS\Version\Dependency\ElementEntity>
855 * @return array
856 */
857 protected function transformDependentElementsToUseLiveId(array $elements) {
858 $transformedElements = array();
859 /** @var $element \TYPO3\CMS\Version\Dependency\ElementEntity */
860 foreach ($elements as $element) {
861 $elementName = \TYPO3\CMS\Version\Dependency\ElementEntity::getIdentifier($element->getTable(), $element->getDataValue('liveId'));
862 $transformedElements[$elementName] = $element;
863 }
864 return $transformedElements;
865 }
866
867 /**
868 * Constructs the scope settings.
869 * Currently the scopes for swapping/publishing and staging are available.
870 *
871 * @return void
872 */
873 protected function constructScopes() {
874 $this->scopes = array(
875 // settings for publishing and swapping:
876 self::SCOPE_WorkspacesSwap => array(
877 // error message and error code
878 self::KEY_ScopeErrorMessage => 'Record "%s" (%s:%s) cannot be swapped or published independently, because it is related to other new or modified records.',
879 self::KEY_ScopeErrorCode => 1288283630,
880 // callback functons used to modify the commandMap
881 // + element properties are specific for each element
882 // + common properties are the same for all elements
883 self::KEY_GetElementPropertiesCallback => 'getElementSwapPropertiesCallback',
884 self::KEY_GetCommonPropertiesCallback => 'getCommonSwapPropertiesCallback',
885 // callback function used, when a new element to be checked is added
886 self::KEY_ElementConstructCallback => 'createNewDependentElementCallback',
887 // callback function used to determine whether an element is a valid child or parent reference (e.g. IRRE)
888 self::KEY_ElementCreateChildReferenceCallback => 'createNewDependentElementChildReferenceCallback',
889 self::KEY_ElementCreateParentReferenceCallback => 'createNewDependentElementParentReferenceCallback',
890 // callback function used to get the correct record uid to be used in the error message
891 self::KEY_PurgeWithErrorMessageGetIdCallback => 'getElementLiveIdCallback',
892 // callback function used to fetch the correct record uid on modifying the commandMap
893 self::KEY_UpdateGetIdCallback => 'getElementLiveIdCallback',
894 // setting whether to use the uid of the live record instead of the workspace record
895 self::KEY_TransformDependentElementsToUseLiveId => TRUE
896 ),
897 // settings for modifying the stage:
898 self::SCOPE_WorkspacesSetStage => array(
899 // error message and error code
900 self::KEY_ScopeErrorMessage => 'Record "%s" (%s:%s) cannot be sent to another stage independently, because it is related to other new or modified records.',
901 self::KEY_ScopeErrorCode => 1289342524,
902 // callback functons used to modify the commandMap
903 // + element properties are specific for each element
904 // + common properties are the same for all elements
905 self::KEY_GetElementPropertiesCallback => 'getElementSetStagePropertiesCallback',
906 self::KEY_GetCommonPropertiesCallback => 'getCommonSetStagePropertiesCallback',
907 // callback function used, when a new element to be checked is added
908 self::KEY_ElementConstructCallback => NULL,
909 // callback function used to determine whether an element is a valid child or parent reference (e.g. IRRE)
910 self::KEY_ElementCreateChildReferenceCallback => 'createNewDependentElementChildReferenceCallback',
911 self::KEY_ElementCreateParentReferenceCallback => 'createNewDependentElementParentReferenceCallback',
912 // callback function used to get the correct record uid to be used in the error message
913 self::KEY_PurgeWithErrorMessageGetIdCallback => 'getElementIdCallback',
914 // callback function used to fetch the correct record uid on modifying the commandMap
915 self::KEY_UpdateGetIdCallback => 'getElementIdCallback',
916 // setting whether to use the uid of the live record instead of the workspace record
917 self::KEY_TransformDependentElementsToUseLiveId => FALSE
918 ),
919 // settings for clearing and flushing:
920 self::SCOPE_WorkspacesClear => array(
921 // error message and error code
922 self::KEY_ScopeErrorMessage => 'Record "%s" (%s:%s) cannot be flushed independently, because it is related to other new or modified records.',
923 self::KEY_ScopeErrorCode => 1300467990,
924 // callback functons used to modify the commandMap
925 // + element properties are specific for each element
926 // + common properties are the same for all elements
927 self::KEY_GetElementPropertiesCallback => NULL,
928 self::KEY_GetCommonPropertiesCallback => 'getCommonClearPropertiesCallback',
929 // callback function used, when a new element to be checked is added
930 self::KEY_ElementConstructCallback => NULL,
931 // callback function used to determine whether an element is a valid child or parent reference (e.g. IRRE)
932 self::KEY_ElementCreateChildReferenceCallback => 'createClearDependentElementChildReferenceCallback',
933 self::KEY_ElementCreateParentReferenceCallback => 'createClearDependentElementParentReferenceCallback',
934 // callback function used to get the correct record uid to be used in the error message
935 self::KEY_PurgeWithErrorMessageGetIdCallback => 'getElementIdCallback',
936 // callback function used to fetch the correct record uid on modifying the commandMap
937 self::KEY_UpdateGetIdCallback => 'getElementIdCallback',
938 // setting whether to use the uid of the live record instead of the workspace record
939 self::KEY_TransformDependentElementsToUseLiveId => FALSE
940 )
941 );
942 }
943
944 /**
945 * Gets data for a particular scope.
946 *
947 * @throws RuntimeException
948 * @param string $scope Scope identifier
949 * @param string $key
950 * @return string
951 */
952 protected function getScopeData($scope, $key) {
953 if (!isset($this->scopes[$scope])) {
954 throw new \RuntimeException('Scope "' . $scope . '" is not defined.', 1289342187);
955 }
956 return $this->scopes[$scope][$key];
957 }
958
959 /**
960 * Gets a new callback to be used in the dependency resolver utility.
961 *
962 * @param string $method
963 * @param array $targetArguments
964 * @return \TYPO3\CMS\Version\Dependency\EventCallback
965 */
966 protected function getDependencyCallback($method, array $targetArguments = array()) {
967 return \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Version\\Dependency\\EventCallback', $this, $method, $targetArguments);
968 }
969
970 /**
971 * Processes a local callback inside this object.
972 *
973 * @param string $method
974 * @param array $callbackArguments
975 * @return mixed
976 */
977 protected function processCallback($method, array $callbackArguments) {
978 return call_user_func_array(array($this, $method), $callbackArguments);
979 }
980
981 }