Total Complexity | 209 |
Total Lines | 1494 |
Duplicated Lines | 0 % |
Changes | 0 |
Complex classes like DataHandlerHook often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use DataHandlerHook, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
47 | class DataHandlerHook |
||
48 | { |
||
49 | /** |
||
50 | * For accumulating information about workspace stages raised |
||
51 | * on elements so a single mail is sent as notification. |
||
52 | * |
||
53 | * @var array |
||
54 | */ |
||
55 | protected $notificationEmailInfo = []; |
||
56 | |||
57 | /** |
||
58 | * Contains remapped IDs. |
||
59 | * |
||
60 | * @var array |
||
61 | */ |
||
62 | protected $remappedIds = []; |
||
63 | |||
64 | /**************************** |
||
65 | ***** Cmdmap Hooks ****** |
||
66 | ****************************/ |
||
67 | /** |
||
68 | * hook that is called before any cmd of the commandmap is executed |
||
69 | * |
||
70 | * @param DataHandler $dataHandler reference to the main DataHandler object |
||
71 | */ |
||
72 | public function processCmdmap_beforeStart(DataHandler $dataHandler) |
||
73 | { |
||
74 | // Reset notification array |
||
75 | $this->notificationEmailInfo = []; |
||
76 | // Resolve dependencies of version/workspaces actions: |
||
77 | $dataHandler->cmdmap = $this->getCommandMap($dataHandler)->process()->get(); |
||
78 | } |
||
79 | |||
80 | /** |
||
81 | * hook that is called when no prepared command was found |
||
82 | * |
||
83 | * @param string $command the command to be executed |
||
84 | * @param string $table the table of the record |
||
85 | * @param int $id the ID of the record |
||
86 | * @param mixed $value the value containing the data |
||
87 | * @param bool $commandIsProcessed can be set so that other hooks or |
||
88 | * @param DataHandler $dataHandler reference to the main DataHandler object |
||
89 | */ |
||
90 | public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $dataHandler) |
||
91 | { |
||
92 | // custom command "version" |
||
93 | if ($command !== 'version') { |
||
94 | return; |
||
95 | } |
||
96 | $commandIsProcessed = true; |
||
97 | $action = (string)$value['action']; |
||
98 | $comment = $value['comment'] ?? ''; |
||
99 | $notificationAlternativeRecipients = $value['notificationAlternativeRecipients'] ?? []; |
||
100 | switch ($action) { |
||
101 | case 'new': |
||
102 | $dataHandler->versionizeRecord($table, $id, $value['label']); |
||
103 | break; |
||
104 | case 'swap': |
||
105 | case 'publish': |
||
106 | $this->version_swap( |
||
107 | $table, |
||
108 | $id, |
||
109 | $value['swapWith'], |
||
110 | $dataHandler, |
||
111 | $comment, |
||
112 | $notificationAlternativeRecipients |
||
113 | ); |
||
114 | break; |
||
115 | case 'clearWSID': |
||
116 | case 'flush': |
||
117 | $dataHandler->discard($table, (int)$id); |
||
118 | break; |
||
119 | case 'setStage': |
||
120 | $elementIds = GeneralUtility::intExplode(',', (string)$id, true); |
||
121 | foreach ($elementIds as $elementId) { |
||
122 | $this->version_setStage( |
||
123 | $table, |
||
124 | $elementId, |
||
125 | $value['stageId'], |
||
126 | $comment, |
||
127 | $dataHandler, |
||
128 | $notificationAlternativeRecipients |
||
129 | ); |
||
130 | } |
||
131 | break; |
||
132 | default: |
||
133 | // Do nothing |
||
134 | } |
||
135 | } |
||
136 | |||
137 | /** |
||
138 | * hook that is called AFTER all commands of the commandmap was |
||
139 | * executed |
||
140 | * |
||
141 | * @param DataHandler $dataHandler reference to the main DataHandler object |
||
142 | */ |
||
143 | public function processCmdmap_afterFinish(DataHandler $dataHandler) |
||
144 | { |
||
145 | // Empty accumulation array |
||
146 | $emailNotificationService = GeneralUtility::makeInstance(StageChangeNotification::class); |
||
147 | $this->sendStageChangeNotification( |
||
148 | $this->notificationEmailInfo, |
||
149 | $emailNotificationService, |
||
150 | $dataHandler |
||
151 | ); |
||
152 | |||
153 | // Reset notification array |
||
154 | $this->notificationEmailInfo = []; |
||
155 | // Reset remapped IDs |
||
156 | $this->remappedIds = []; |
||
157 | |||
158 | $this->flushWorkspaceCacheEntriesByWorkspaceId((int)$dataHandler->BE_USER->workspace); |
||
159 | } |
||
160 | |||
161 | protected function sendStageChangeNotification( |
||
162 | array $accumulatedNotificationInformation, |
||
163 | StageChangeNotification $notificationService, |
||
164 | DataHandler $dataHandler |
||
165 | ): void { |
||
166 | foreach ($accumulatedNotificationInformation as $groupedNotificationInformation) { |
||
167 | $emails = (array)$groupedNotificationInformation['recipients']; |
||
168 | if (empty($emails)) { |
||
169 | continue; |
||
170 | } |
||
171 | $workspaceRec = $groupedNotificationInformation['shared'][0]; |
||
172 | if (!is_array($workspaceRec)) { |
||
173 | continue; |
||
174 | } |
||
175 | $notificationService->notifyStageChange( |
||
176 | $workspaceRec, |
||
177 | (int)$groupedNotificationInformation['shared'][1], |
||
178 | $groupedNotificationInformation['elements'], |
||
179 | $groupedNotificationInformation['shared'][2], |
||
180 | $emails, |
||
181 | $dataHandler->BE_USER |
||
182 | ); |
||
183 | |||
184 | if ($dataHandler->enableLogging) { |
||
185 | [$elementTable, $elementUid] = reset($groupedNotificationInformation['elements']); |
||
186 | $propertyArray = $dataHandler->getRecordProperties($elementTable, $elementUid); |
||
187 | $pid = $propertyArray['pid']; |
||
188 | $dataHandler->log($elementTable, $elementUid, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Notification email for stage change was sent to "' . implode('", "', $emails) . '"', -1, [], $dataHandler->eventPid($elementTable, $elementUid, $pid)); |
||
189 | } |
||
190 | } |
||
191 | } |
||
192 | |||
193 | /** |
||
194 | * hook that is called when an element shall get deleted |
||
195 | * |
||
196 | * @param string $table the table of the record |
||
197 | * @param int $id the ID of the record |
||
198 | * @param array $record The accordant database record |
||
199 | * @param bool $recordWasDeleted can be set so that other hooks or |
||
200 | * @param DataHandler $dataHandler reference to the main DataHandler object |
||
201 | */ |
||
202 | public function processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $dataHandler) |
||
203 | { |
||
204 | // only process the hook if it wasn't processed |
||
205 | // by someone else before |
||
206 | if ($recordWasDeleted) { |
||
207 | return; |
||
208 | } |
||
209 | $recordWasDeleted = true; |
||
210 | // For Live version, try if there is a workspace version because if so, rather "delete" that instead |
||
211 | // Look, if record is an offline version, then delete directly: |
||
212 | if ((int)($record['t3ver_oid'] ?? 0) === 0) { |
||
213 | if ($wsVersion = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $id)) { |
||
214 | $record = $wsVersion; |
||
215 | $id = $record['uid']; |
||
216 | } |
||
217 | } |
||
218 | $recordVersionState = VersionState::cast($record['t3ver_state'] ?? 0); |
||
219 | // Look, if record is an offline version, then delete directly: |
||
220 | if ((int)($record['t3ver_oid'] ?? 0) > 0) { |
||
221 | if (BackendUtility::isTableWorkspaceEnabled($table)) { |
||
222 | // In Live workspace, delete any. In other workspaces there must be match. |
||
223 | if ($dataHandler->BE_USER->workspace == 0 || (int)$record['t3ver_wsid'] == $dataHandler->BE_USER->workspace) { |
||
224 | $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state'); |
||
225 | // Processing can be skipped if a delete placeholder shall be published |
||
226 | // during the current request. Thus it will be deleted later on... |
||
227 | $liveRecordVersionState = VersionState::cast($liveRec['t3ver_state']); |
||
228 | if ($recordVersionState->equals(VersionState::DELETE_PLACEHOLDER) && !empty($liveRec['uid']) |
||
229 | && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action']) |
||
230 | && !empty($dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith']) |
||
231 | && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['action'] === 'swap' |
||
232 | && $dataHandler->cmdmap[$table][$liveRec['uid']]['version']['swapWith'] == $id |
||
233 | ) { |
||
234 | return null; |
||
235 | } |
||
236 | |||
237 | if ($record['t3ver_wsid'] > 0 && $recordVersionState->equals(VersionState::DEFAULT_STATE)) { |
||
238 | // Change normal versioned record to delete placeholder |
||
239 | // Happens when an edited record is deleted |
||
240 | GeneralUtility::makeInstance(ConnectionPool::class) |
||
241 | ->getConnectionForTable($table) |
||
242 | ->update( |
||
243 | $table, |
||
244 | ['t3ver_state' => VersionState::DELETE_PLACEHOLDER], |
||
245 | ['uid' => $id] |
||
246 | ); |
||
247 | |||
248 | // Delete localization overlays: |
||
249 | $dataHandler->deleteL10nOverlayRecords($table, $id); |
||
250 | } elseif ($record['t3ver_wsid'] == 0 || !$liveRecordVersionState->indicatesPlaceholder()) { |
||
251 | // Delete those in WS 0 + if their live records state was not "Placeholder". |
||
252 | $dataHandler->deleteEl($table, $id); |
||
253 | } elseif ($recordVersionState->equals(VersionState::NEW_PLACEHOLDER)) { |
||
254 | $placeholderRecord = BackendUtility::getLiveVersionOfRecord($table, (int)$id); |
||
255 | $dataHandler->deleteEl($table, (int)$id); |
||
256 | if (is_array($placeholderRecord)) { |
||
257 | $this->softOrHardDeleteSingleRecord($table, (int)$placeholderRecord['uid']); |
||
258 | } |
||
259 | } |
||
260 | } else { |
||
261 | $dataHandler->newlog('Tried to delete record from another workspace', SystemLogErrorClassification::USER_ERROR); |
||
262 | } |
||
263 | } else { |
||
264 | $dataHandler->newlog('Versioning not enabled for record with an online ID (t3ver_oid) given', SystemLogErrorClassification::SYSTEM_ERROR); |
||
265 | } |
||
266 | } elseif ($recordVersionState->equals(VersionState::NEW_PLACEHOLDER)) { |
||
267 | // If it is a new versioned record, delete it directly. |
||
268 | $dataHandler->deleteEl($table, $id); |
||
269 | } elseif ($dataHandler->BE_USER->workspaceAllowsLiveEditingInTable($table)) { |
||
270 | // Look, if record is "online" then delete directly. |
||
271 | $dataHandler->deleteEl($table, $id); |
||
272 | } else { |
||
273 | // Otherwise, try to delete by versioning: |
||
274 | $copyMappingArray = $dataHandler->copyMappingArray; |
||
275 | $dataHandler->versionizeRecord($table, $id, 'DELETED!', true); |
||
276 | // Determine newly created versions: |
||
277 | // (remove placeholders are copied and modified, thus they appear in the copyMappingArray) |
||
278 | $versionizedElements = ArrayUtility::arrayDiffKeyRecursive($dataHandler->copyMappingArray, $copyMappingArray); |
||
279 | // Delete localization overlays: |
||
280 | foreach ($versionizedElements as $versionizedTableName => $versionizedOriginalIds) { |
||
281 | foreach ($versionizedOriginalIds as $versionizedOriginalId => $_) { |
||
282 | $dataHandler->deleteL10nOverlayRecords($versionizedTableName, $versionizedOriginalId); |
||
283 | } |
||
284 | } |
||
285 | } |
||
286 | } |
||
287 | |||
288 | /** |
||
289 | * In case a sys_workspace_stage record is deleted we do a hard reset |
||
290 | * for all existing records in that stage to avoid that any of these end up |
||
291 | * as orphan records. |
||
292 | * |
||
293 | * @param string $command |
||
294 | * @param string $table |
||
295 | * @param string $id |
||
296 | * @param string $value |
||
297 | * @param DataHandler $dataHandler |
||
298 | */ |
||
299 | public function processCmdmap_postProcess($command, $table, $id, $value, DataHandler $dataHandler) |
||
300 | { |
||
301 | if ($command === 'delete') { |
||
302 | if ($table === StagesService::TABLE_STAGE) { |
||
303 | $this->resetStageOfElements((int)$id); |
||
304 | } elseif ($table === WorkspaceService::TABLE_WORKSPACE) { |
||
305 | $this->flushWorkspaceElements((int)$id); |
||
306 | $this->emitUpdateTopbarSignal(); |
||
307 | } |
||
308 | } |
||
309 | } |
||
310 | |||
311 | public function processDatamap_afterAllOperations(DataHandler $dataHandler): void |
||
312 | { |
||
313 | if (isset($dataHandler->datamap[WorkspaceService::TABLE_WORKSPACE])) { |
||
314 | $this->emitUpdateTopbarSignal(); |
||
315 | } |
||
316 | } |
||
317 | |||
318 | /** |
||
319 | * Hook for \TYPO3\CMS\Core\DataHandling\DataHandler::moveRecord that cares about |
||
320 | * moving records that are *not* in the live workspace |
||
321 | * |
||
322 | * @param string $table the table of the record |
||
323 | * @param int $uid the ID of the record |
||
324 | * @param int $destPid Position to move to: $destPid: >=0 then it points to |
||
325 | * @param array $propArr Record properties, like header and pid (includes workspace overlay) |
||
326 | * @param array $moveRec Record properties, like header and pid (without workspace overlay) |
||
327 | * @param int $resolvedPid The final page ID of the record |
||
328 | * @param bool $recordWasMoved can be set so that other hooks or |
||
329 | * @param DataHandler $dataHandler |
||
330 | */ |
||
331 | public function moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $dataHandler) |
||
332 | { |
||
333 | // Only do something in Draft workspace |
||
334 | if ($dataHandler->BE_USER->workspace === 0) { |
||
335 | return; |
||
336 | } |
||
337 | $tableSupportsVersioning = BackendUtility::isTableWorkspaceEnabled($table); |
||
338 | $recordWasMoved = true; |
||
339 | $moveRecVersionState = VersionState::cast((int)($moveRec['t3ver_state'] ?? VersionState::DEFAULT_STATE)); |
||
340 | // Get workspace version of the source record, if any: |
||
341 | $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid'); |
||
342 | if ($tableSupportsVersioning) { |
||
343 | // Create version of record first, if it does not exist |
||
344 | if (empty($versionedRecord['uid'])) { |
||
345 | $dataHandler->versionizeRecord($table, $uid, 'MovePointer'); |
||
346 | $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid'); |
||
347 | if ((int)$resolvedPid !== (int)$propArr['pid']) { |
||
348 | $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid); |
||
349 | } |
||
350 | } elseif ($dataHandler->isRecordCopied($table, $uid) && (int)$dataHandler->copyMappingArray[$table][$uid] === (int)$versionedRecord['uid']) { |
||
351 | // If the record has been versioned before (e.g. cascaded parent-child structure), create only the move-placeholders |
||
352 | if ((int)$resolvedPid !== (int)$propArr['pid']) { |
||
353 | $this->moveRecord_processFields($dataHandler, $resolvedPid, $table, $uid); |
||
354 | } |
||
355 | } |
||
356 | } |
||
357 | // Check workspace permissions: |
||
358 | $workspaceAccessBlocked = []; |
||
359 | // Element was in "New/Deleted/Moved" so it can be moved... |
||
360 | $recIsNewVersion = $moveRecVersionState->equals(VersionState::NEW_PLACEHOLDER) || $moveRecVersionState->indicatesPlaceholder(); |
||
361 | $recordMustNotBeVersionized = $dataHandler->BE_USER->workspaceAllowsLiveEditingInTable($table); |
||
362 | $canMoveRecord = $recIsNewVersion || $tableSupportsVersioning; |
||
363 | // Workspace source check: |
||
364 | if (!$recIsNewVersion) { |
||
365 | $errorCode = $dataHandler->BE_USER->workspaceCannotEditRecord($table, $versionedRecord['uid'] ?: $uid); |
||
366 | if ($errorCode) { |
||
367 | $workspaceAccessBlocked['src1'] = 'Record could not be edited in workspace: ' . $errorCode . ' '; |
||
368 | } elseif (!$canMoveRecord && !$recordMustNotBeVersionized) { |
||
369 | $workspaceAccessBlocked['src2'] = 'Could not remove record from table "' . $table . '" from its page "' . $moveRec['pid'] . '" '; |
||
370 | } |
||
371 | } |
||
372 | // Workspace destination check: |
||
373 | // All records can be inserted if $recordMustNotBeVersionized is true. |
||
374 | // Only new versions can be inserted if $recordMustNotBeVersionized is FALSE. |
||
375 | if (!($recordMustNotBeVersionized || $canMoveRecord && !$recordMustNotBeVersionized)) { |
||
376 | $workspaceAccessBlocked['dest1'] = 'Could not insert record from table "' . $table . '" in destination PID "' . $resolvedPid . '" '; |
||
377 | } |
||
378 | |||
379 | if (empty($workspaceAccessBlocked)) { |
||
380 | $versionedRecordUid = (int)$versionedRecord['uid']; |
||
381 | // custom moving not needed, just behave like in live workspace (also for newly versioned records) |
||
382 | if (!$versionedRecordUid || !$tableSupportsVersioning || $recIsNewVersion) { |
||
383 | $recordWasMoved = false; |
||
384 | } else { |
||
385 | // If the move operation is done on a versioned record, which is |
||
386 | // NOT new/deleted placeholder, then mark the versioned record as "moved" |
||
387 | $this->moveRecord_moveVersionedRecord($table, (int)$uid, (int)$destPid, $versionedRecordUid, $dataHandler); |
||
388 | } |
||
389 | } else { |
||
390 | $dataHandler->newlog('Move attempt failed due to workspace restrictions: ' . implode(' // ', $workspaceAccessBlocked), SystemLogErrorClassification::USER_ERROR); |
||
391 | } |
||
392 | } |
||
393 | |||
394 | /** |
||
395 | * Processes fields of a moved record and follows references. |
||
396 | * |
||
397 | * @param DataHandler $dataHandler Calling DataHandler instance |
||
398 | * @param int $resolvedPageId Resolved real destination page id |
||
399 | * @param string $table Name of parent table |
||
400 | * @param int $uid UID of the parent record |
||
401 | */ |
||
402 | protected function moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid) |
||
403 | { |
||
404 | $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid); |
||
405 | if (empty($versionedRecord)) { |
||
406 | return; |
||
407 | } |
||
408 | foreach ($versionedRecord as $field => $value) { |
||
409 | if (empty($GLOBALS['TCA'][$table]['columns'][$field]['config'])) { |
||
410 | continue; |
||
411 | } |
||
412 | $this->moveRecord_processFieldValue( |
||
413 | $dataHandler, |
||
414 | $resolvedPageId, |
||
415 | $table, |
||
416 | $uid, |
||
417 | $value, |
||
418 | $GLOBALS['TCA'][$table]['columns'][$field]['config'] |
||
419 | ); |
||
420 | } |
||
421 | } |
||
422 | |||
423 | /** |
||
424 | * Processes a single field of a moved record and follows references. |
||
425 | * |
||
426 | * @param DataHandler $dataHandler Calling DataHandler instance |
||
427 | * @param int $resolvedPageId Resolved real destination page id |
||
428 | * @param string $table Name of parent table |
||
429 | * @param int $uid UID of the parent record |
||
430 | * @param string $value Value of the field of the parent record |
||
431 | * @param array $configuration TCA field configuration of the parent record |
||
432 | */ |
||
433 | protected function moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $value, array $configuration): void |
||
434 | { |
||
435 | $inlineFieldType = $dataHandler->getInlineFieldType($configuration); |
||
436 | $inlineProcessing = ( |
||
437 | ($inlineFieldType === 'list' || $inlineFieldType === 'field') |
||
438 | && BackendUtility::isTableWorkspaceEnabled($configuration['foreign_table']) |
||
439 | && (!isset($configuration['behaviour']['disableMovingChildrenWithParent']) || !$configuration['behaviour']['disableMovingChildrenWithParent']) |
||
440 | ); |
||
441 | |||
442 | if ($inlineProcessing) { |
||
443 | if ($table === 'pages') { |
||
444 | // If the inline elements are related to a page record, |
||
445 | // make sure they reside at that page and not at its parent |
||
446 | $resolvedPageId = $uid; |
||
447 | } |
||
448 | |||
449 | $dbAnalysis = $this->createRelationHandlerInstance(); |
||
450 | $dbAnalysis->start($value, $configuration['foreign_table'], '', $uid, $table, $configuration); |
||
451 | |||
452 | // Moving records to a positive destination will insert each |
||
453 | // record at the beginning, thus the order is reversed here: |
||
454 | foreach ($dbAnalysis->itemArray as $item) { |
||
455 | $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $item['table'], $item['id'], 'uid,t3ver_state'); |
||
456 | if (empty($versionedRecord)) { |
||
457 | continue; |
||
458 | } |
||
459 | $versionState = VersionState::cast($versionedRecord['t3ver_state']); |
||
460 | if ($versionState->indicatesPlaceholder()) { |
||
461 | continue; |
||
462 | } |
||
463 | $dataHandler->moveRecord($item['table'], $item['id'], $resolvedPageId); |
||
464 | } |
||
465 | } |
||
466 | } |
||
467 | |||
468 | /**************************** |
||
469 | ***** Stage Changes ****** |
||
470 | ****************************/ |
||
471 | /** |
||
472 | * Setting stage of record |
||
473 | * |
||
474 | * @param string $table Table name |
||
475 | * @param int $id |
||
476 | * @param int $stageId Stage ID to set |
||
477 | * @param string $comment Comment that goes into log |
||
478 | * @param DataHandler $dataHandler DataHandler object |
||
479 | * @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users |
||
480 | */ |
||
481 | protected function version_setStage($table, $id, $stageId, string $comment, DataHandler $dataHandler, array $notificationAlternativeRecipients = []) |
||
482 | { |
||
483 | if ($errorCode = $dataHandler->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) { |
||
484 | $dataHandler->newlog('Attempt to set stage for record failed: ' . $errorCode, SystemLogErrorClassification::USER_ERROR); |
||
485 | } elseif ($dataHandler->checkRecordUpdateAccess($table, $id)) { |
||
486 | $record = BackendUtility::getRecord($table, $id); |
||
487 | $workspaceInfo = $dataHandler->BE_USER->checkWorkspace($record['t3ver_wsid']); |
||
488 | // check if the user is allowed to the current stage, so it's also allowed to send to next stage |
||
489 | if ($dataHandler->BE_USER->workspaceCheckStageForCurrent($record['t3ver_stage'])) { |
||
490 | // Set stage of record: |
||
491 | GeneralUtility::makeInstance(ConnectionPool::class) |
||
492 | ->getConnectionForTable($table) |
||
493 | ->update( |
||
494 | $table, |
||
495 | [ |
||
496 | 't3ver_stage' => $stageId, |
||
497 | ], |
||
498 | ['uid' => (int)$id] |
||
499 | ); |
||
500 | |||
501 | if ($dataHandler->enableLogging) { |
||
502 | $propertyArray = $dataHandler->getRecordProperties($table, $id); |
||
503 | $pid = $propertyArray['pid']; |
||
504 | $dataHandler->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', -1, [], $dataHandler->eventPid($table, $id, $pid)); |
||
505 | } |
||
506 | // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere! |
||
507 | $dataHandler->log($table, $id, DatabaseAction::UPDATE, 0, SystemLogErrorClassification::MESSAGE, 'Stage raised...', 30, ['comment' => $comment, 'stage' => $stageId]); |
||
508 | if ((int)$workspaceInfo['stagechg_notification'] > 0) { |
||
509 | $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['shared'] = [$workspaceInfo, $stageId, $comment]; |
||
510 | $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = [$table, $id]; |
||
511 | $this->notificationEmailInfo[$workspaceInfo['uid'] . ':' . $stageId . ':' . $comment]['recipients'] = $notificationAlternativeRecipients; |
||
512 | } |
||
513 | } else { |
||
514 | $dataHandler->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', SystemLogErrorClassification::USER_ERROR); |
||
515 | } |
||
516 | } else { |
||
517 | $dataHandler->newlog('Attempt to set stage for record failed because you do not have edit access', SystemLogErrorClassification::USER_ERROR); |
||
518 | } |
||
519 | } |
||
520 | |||
521 | /***************************** |
||
522 | ***** CMD versioning ****** |
||
523 | *****************************/ |
||
524 | |||
525 | /** |
||
526 | * Publishing / Swapping (= switching) versions of a record |
||
527 | * Version from archive (future/past, called "swap version") will get the uid of the "t3ver_oid", the official element with uid = "t3ver_oid" will get the new versions old uid. PIDs are swapped also |
||
528 | * |
||
529 | * @param string $table Table name |
||
530 | * @param int $id UID of the online record to swap |
||
531 | * @param int $swapWith UID of the archived version to swap with! |
||
532 | * @param DataHandler $dataHandler DataHandler object |
||
533 | * @param string $comment Notification comment |
||
534 | * @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users |
||
535 | */ |
||
536 | protected function version_swap($table, $id, $swapWith, DataHandler $dataHandler, string $comment, $notificationAlternativeRecipients = []) |
||
537 | { |
||
538 | // Check prerequisites before start publishing |
||
539 | // Skip records that have been deleted during the current execution |
||
540 | if ($dataHandler->hasDeletedRecord($table, $id)) { |
||
541 | return; |
||
542 | } |
||
543 | |||
544 | // First, check if we may actually edit the online record |
||
545 | if (!$dataHandler->checkRecordUpdateAccess($table, $id)) { |
||
546 | $dataHandler->newlog( |
||
547 | sprintf( |
||
548 | 'Error: You cannot swap versions for record %s:%d you do not have access to edit!', |
||
549 | $table, |
||
550 | $id |
||
551 | ), |
||
552 | SystemLogErrorClassification::USER_ERROR |
||
553 | ); |
||
554 | return; |
||
555 | } |
||
556 | // Select the two versions: |
||
557 | // Currently live version, contents will be removed. |
||
558 | $curVersion = BackendUtility::getRecord($table, $id, '*'); |
||
559 | // Versioned records which contents will be moved into $curVersion |
||
560 | $isNewRecord = ((int)($curVersion['t3ver_state'] ?? 0) === VersionState::NEW_PLACEHOLDER); |
||
561 | if ($isNewRecord && is_array($curVersion)) { |
||
562 | $this->publishNewRecord($table, $curVersion, $dataHandler, $comment, (array)$notificationAlternativeRecipients); |
||
563 | return; |
||
564 | } |
||
565 | $swapVersion = BackendUtility::getRecord($table, $swapWith, '*'); |
||
566 | if (!(is_array($curVersion) && is_array($swapVersion))) { |
||
567 | $dataHandler->newlog( |
||
568 | sprintf( |
||
569 | 'Error: Either online or swap version for %s:%d->%d could not be selected!', |
||
570 | $table, |
||
571 | $id, |
||
572 | $swapWith |
||
573 | ), |
||
574 | SystemLogErrorClassification::SYSTEM_ERROR |
||
575 | ); |
||
576 | return; |
||
577 | } |
||
578 | $workspaceId = (int)$swapVersion['t3ver_wsid']; |
||
579 | if (!$dataHandler->BE_USER->workspacePublishAccess($workspaceId)) { |
||
580 | $dataHandler->newlog('User could not publish records from workspace #' . $workspaceId, SystemLogErrorClassification::USER_ERROR); |
||
581 | return; |
||
582 | } |
||
583 | $wsAccess = $dataHandler->BE_USER->checkWorkspace($workspaceId); |
||
584 | if (!($workspaceId <= 0 || !($wsAccess['publish_access'] & 1) || (int)$swapVersion['t3ver_stage'] === StagesService::STAGE_PUBLISH_ID)) { |
||
585 | $dataHandler->newlog('Records in workspace #' . $workspaceId . ' can only be published when in "Publish" stage.', SystemLogErrorClassification::USER_ERROR); |
||
586 | return; |
||
587 | } |
||
588 | if (!($dataHandler->doesRecordExist($table, $swapWith, Permission::PAGE_SHOW) && $dataHandler->checkRecordUpdateAccess($table, $swapWith))) { |
||
589 | $dataHandler->newlog('You cannot publish a record you do not have edit and show permissions for', SystemLogErrorClassification::USER_ERROR); |
||
590 | return; |
||
591 | } |
||
592 | // Check if the swapWith record really IS a version of the original! |
||
593 | if (!(((int)$swapVersion['t3ver_oid'] > 0 && (int)$curVersion['t3ver_oid'] === 0) && (int)$swapVersion['t3ver_oid'] === (int)$id)) { |
||
594 | $dataHandler->newlog('In offline record, either t3ver_oid was not set or the t3ver_oid didn\'t match the id of the online version as it must!', SystemLogErrorClassification::SYSTEM_ERROR); |
||
595 | return; |
||
596 | } |
||
597 | $versionState = new VersionState($swapVersion['t3ver_state']); |
||
598 | |||
599 | // Find fields to keep |
||
600 | $keepFields = $this->getUniqueFields($table); |
||
601 | // Sorting needs to be exchanged for moved records |
||
602 | if (!empty($GLOBALS['TCA'][$table]['ctrl']['sortby']) && !$versionState->equals(VersionState::MOVE_POINTER)) { |
||
603 | $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['sortby']; |
||
604 | } |
||
605 | // l10n-fields must be kept otherwise the localization |
||
606 | // will be lost during the publishing |
||
607 | if ($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) { |
||
608 | $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']; |
||
609 | } |
||
610 | // Swap "keepfields" |
||
611 | foreach ($keepFields as $fN) { |
||
612 | $tmp = $swapVersion[$fN]; |
||
613 | $swapVersion[$fN] = $curVersion[$fN]; |
||
614 | $curVersion[$fN] = $tmp; |
||
615 | } |
||
616 | // Preserve states: |
||
617 | $t3ver_state = []; |
||
618 | $t3ver_state['swapVersion'] = $swapVersion['t3ver_state']; |
||
619 | // Modify offline version to become online: |
||
620 | // Set pid for ONLINE (but not for moved records) |
||
621 | if (!$versionState->equals(VersionState::MOVE_POINTER)) { |
||
622 | $swapVersion['pid'] = (int)$curVersion['pid']; |
||
623 | } |
||
624 | // We clear this because t3ver_oid only make sense for offline versions |
||
625 | // and we want to prevent unintentional misuse of this |
||
626 | // value for online records. |
||
627 | $swapVersion['t3ver_oid'] = 0; |
||
628 | // In case of swapping and the offline record has a state |
||
629 | // (like 2 or 4 for deleting or move-pointer) we set the |
||
630 | // current workspace ID so the record is not deselected. |
||
631 | $swapVersion['t3ver_wsid'] = 0; |
||
632 | $swapVersion['t3ver_stage'] = 0; |
||
633 | $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE); |
||
634 | // Take care of relations in each field (e.g. IRRE): |
||
635 | if (is_array($GLOBALS['TCA'][$table]['columns'])) { |
||
636 | foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fieldConf) { |
||
637 | if (isset($fieldConf['config']) && is_array($fieldConf['config'])) { |
||
638 | $this->version_swap_processFields($table, $fieldConf['config'], $curVersion, $swapVersion, $dataHandler); |
||
639 | } |
||
640 | } |
||
641 | } |
||
642 | unset($swapVersion['uid']); |
||
643 | // Modify online version to become offline: |
||
644 | unset($curVersion['uid']); |
||
645 | // Mark curVersion to contain the oid |
||
646 | $curVersion['t3ver_oid'] = (int)$id; |
||
647 | $curVersion['t3ver_wsid'] = 0; |
||
648 | // Increment lifecycle counter |
||
649 | $curVersion['t3ver_stage'] = 0; |
||
650 | $curVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE); |
||
651 | // Registering and swapping MM relations in current and swap records: |
||
652 | $dataHandler->version_remapMMForVersionSwap($table, $id, $swapWith); |
||
653 | // Generating proper history data to prepare logging |
||
654 | $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion); |
||
655 | $dataHandler->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion); |
||
656 | |||
657 | // Execute swapping: |
||
658 | $sqlErrors = []; |
||
659 | $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); |
||
660 | |||
661 | $platform = $connection->getDatabasePlatform(); |
||
662 | $tableDetails = null; |
||
663 | if ($platform instanceof SQLServerPlatform) { |
||
664 | // mssql needs to set proper PARAM_LOB and others to update fields |
||
665 | $tableDetails = $connection->getSchemaManager()->listTableDetails($table); |
||
666 | } |
||
667 | |||
668 | try { |
||
669 | $types = []; |
||
670 | |||
671 | if ($platform instanceof SQLServerPlatform) { |
||
672 | foreach ($curVersion as $columnName => $columnValue) { |
||
673 | $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType(); |
||
674 | } |
||
675 | } |
||
676 | |||
677 | $connection->update( |
||
678 | $table, |
||
679 | $swapVersion, |
||
680 | ['uid' => (int)$id], |
||
681 | $types |
||
682 | ); |
||
683 | } catch (DBALException $e) { |
||
684 | $sqlErrors[] = $e->getPrevious()->getMessage(); |
||
685 | } |
||
686 | |||
687 | if (empty($sqlErrors)) { |
||
688 | try { |
||
689 | $types = []; |
||
690 | if ($platform instanceof SQLServerPlatform) { |
||
691 | foreach ($curVersion as $columnName => $columnValue) { |
||
692 | $types[$columnName] = $tableDetails->getColumn($columnName)->getType()->getBindingType(); |
||
693 | } |
||
694 | } |
||
695 | |||
696 | $connection->update( |
||
697 | $table, |
||
698 | $curVersion, |
||
699 | ['uid' => (int)$swapWith], |
||
700 | $types |
||
701 | ); |
||
702 | } catch (DBALException $e) { |
||
703 | $sqlErrors[] = $e->getPrevious()->getMessage(); |
||
704 | } |
||
705 | } |
||
706 | |||
707 | if (!empty($sqlErrors)) { |
||
708 | $dataHandler->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), SystemLogErrorClassification::SYSTEM_ERROR); |
||
709 | } else { |
||
710 | // Update localized elements to use the live l10n_parent now |
||
711 | $this->updateL10nOverlayRecordsOnPublish($table, $id, $swapWith, $workspaceId, $dataHandler); |
||
712 | // Register swapped ids for later remapping: |
||
713 | $this->remappedIds[$table][$id] = $swapWith; |
||
714 | $this->remappedIds[$table][$swapWith] = $id; |
||
715 | if ((int)$t3ver_state['swapVersion'] === VersionState::DELETE_PLACEHOLDER) { |
||
716 | // We're publishing a delete placeholder t3ver_state = 2. This means the live record should |
||
717 | // be set to deleted. We're currently in some workspace and deal with a live record here. Thus, |
||
718 | // we temporarily set backend user workspace to 0 so all operations happen as in live. |
||
719 | $currentUserWorkspace = $dataHandler->BE_USER->workspace; |
||
720 | $dataHandler->BE_USER->workspace = 0; |
||
721 | $dataHandler->deleteEl($table, $id, true); |
||
722 | $dataHandler->BE_USER->workspace = $currentUserWorkspace; |
||
723 | } |
||
724 | if ($dataHandler->enableLogging) { |
||
725 | $dataHandler->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Publishing successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, -1, [], $dataHandler->eventPid($table, $id, $swapVersion['pid'])); |
||
726 | } |
||
727 | |||
728 | // Set log entry for live record: |
||
729 | $propArr = $dataHandler->getRecordPropertiesFromRow($table, $swapVersion); |
||
730 | if (($propArr['t3ver_oid'] ?? 0) > 0) { |
||
731 | $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated'); |
||
732 | } else { |
||
733 | $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated'); |
||
734 | } |
||
735 | $dataHandler->log($table, $id, DatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, $label, 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']); |
||
736 | $dataHandler->setHistory($table, $id); |
||
737 | // Set log entry for offline record: |
||
738 | $propArr = $dataHandler->getRecordPropertiesFromRow($table, $curVersion); |
||
739 | if (($propArr['t3ver_oid'] ?? 0) > 0) { |
||
740 | $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.offline_record_updated'); |
||
741 | } else { |
||
742 | $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated'); |
||
743 | } |
||
744 | $dataHandler->log($table, $swapWith, DatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, $label, 10, [$propArr['header'], $table . ':' . $swapWith], $propArr['event_pid']); |
||
745 | $dataHandler->setHistory($table, $swapWith); |
||
746 | |||
747 | $stageId = StagesService::STAGE_PUBLISH_EXECUTE_ID; |
||
748 | $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment; |
||
749 | $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment]; |
||
750 | $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = [$table, $id]; |
||
751 | $this->notificationEmailInfo[$notificationEmailInfoKey]['recipients'] = $notificationAlternativeRecipients; |
||
752 | // Write to log with stageId -20 (STAGE_PUBLISH_EXECUTE_ID) |
||
753 | if ($dataHandler->enableLogging) { |
||
754 | $propArr = $dataHandler->getRecordProperties($table, $id); |
||
755 | $pid = $propArr['pid']; |
||
756 | $dataHandler->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', -1, [], $dataHandler->eventPid($table, $id, $pid)); |
||
757 | } |
||
758 | $dataHandler->log($table, $id, DatabaseAction::UPDATE, 0, SystemLogErrorClassification::MESSAGE, 'Published', 30, ['comment' => $comment, 'stage' => $stageId]); |
||
759 | |||
760 | // Clear cache: |
||
761 | $dataHandler->registerRecordIdForPageCacheClearing($table, $id); |
||
762 | // If published, delete the record from the database |
||
763 | if ($table === 'pages') { |
||
764 | // Note on fifth argument false: At this point both $curVersion and $swapVersion page records are |
||
765 | // identical in DB. deleteEl() would now usually find all records assigned to our obsolete |
||
766 | // page which at the same time belong to our current version page, and would delete them. |
||
767 | // To suppress this, false tells deleteEl() to only delete the obsolete page but not its assigned records. |
||
768 | $dataHandler->deleteEl($table, $swapWith, true, true, false); |
||
769 | } else { |
||
770 | $dataHandler->deleteEl($table, $swapWith, true, true); |
||
771 | } |
||
772 | |||
773 | // Update reference index of the live record - which could have been a workspace record in case 'new' |
||
774 | $dataHandler->updateRefIndex($table, $id, 0); |
||
775 | // The 'swapWith' record has been deleted, so we can drop any reference index the record is involved in |
||
776 | $dataHandler->registerReferenceIndexRowsForDrop($table, $swapWith, (int)$dataHandler->BE_USER->workspace); |
||
777 | } |
||
778 | } |
||
779 | |||
780 | /** |
||
781 | * If an editor is doing "partial" publishing, the translated children need to be "linked" to the now pointed |
||
782 | * live record, as if the versioned record (which is deleted) would have never existed. |
||
783 | * |
||
784 | * This is related to the l10n_source and l10n_parent fields. |
||
785 | * |
||
786 | * This needs to happen before the hook calls DataHandler->deleteEl() otherwise the children get deleted as well. |
||
787 | * |
||
788 | * @param string $table the database table of the published record |
||
789 | * @param int $liveId the live version / online version of the record that was just published |
||
790 | * @param int $previouslyUsedVersionId the versioned record ID (wsid>0) which is about to be deleted |
||
791 | * @param int $workspaceId the workspace ID |
||
792 | * @param DataHandler $dataHandler |
||
793 | */ |
||
794 | protected function updateL10nOverlayRecordsOnPublish(string $table, int $liveId, int $previouslyUsedVersionId, int $workspaceId, DataHandler $dataHandler): void |
||
795 | { |
||
796 | if (!BackendUtility::isTableLocalizable($table)) { |
||
797 | return; |
||
798 | } |
||
799 | if (!BackendUtility::isTableWorkspaceEnabled($table)) { |
||
800 | return; |
||
801 | } |
||
802 | $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); |
||
803 | $queryBuilder = $connection->createQueryBuilder(); |
||
804 | $queryBuilder->getRestrictions()->removeAll(); |
||
805 | |||
806 | $l10nParentFieldName = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']; |
||
807 | $constraints = $queryBuilder->expr()->eq( |
||
808 | $l10nParentFieldName, |
||
809 | $queryBuilder->createNamedParameter($previouslyUsedVersionId, \PDO::PARAM_INT) |
||
810 | ); |
||
811 | $translationSourceFieldName = $GLOBALS['TCA'][$table]['ctrl']['translationSource'] ?? null; |
||
812 | if ($translationSourceFieldName) { |
||
813 | $constraints = $queryBuilder->expr()->orX( |
||
814 | $constraints, |
||
815 | $queryBuilder->expr()->eq( |
||
816 | $translationSourceFieldName, |
||
817 | $queryBuilder->createNamedParameter($previouslyUsedVersionId, \PDO::PARAM_INT) |
||
818 | ) |
||
819 | ); |
||
820 | } |
||
821 | |||
822 | $queryBuilder |
||
823 | ->select('uid', $l10nParentFieldName) |
||
824 | ->from($table) |
||
825 | ->where( |
||
826 | $constraints, |
||
827 | $queryBuilder->expr()->eq( |
||
828 | 't3ver_wsid', |
||
829 | $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT) |
||
830 | ) |
||
831 | ); |
||
832 | |||
833 | if ($translationSourceFieldName) { |
||
834 | $queryBuilder->addSelect($translationSourceFieldName); |
||
835 | } |
||
836 | |||
837 | $statement = $queryBuilder->execute(); |
||
838 | while ($record = $statement->fetch()) { |
||
839 | $updateFields = []; |
||
840 | $dataTypes = [\PDO::PARAM_INT]; |
||
841 | if ((int)$record[$l10nParentFieldName] === $previouslyUsedVersionId) { |
||
842 | $updateFields[$l10nParentFieldName] = $liveId; |
||
843 | $dataTypes[] = \PDO::PARAM_INT; |
||
844 | } |
||
845 | if ($translationSourceFieldName && (int)$record[$translationSourceFieldName] === $previouslyUsedVersionId) { |
||
846 | $updateFields[$translationSourceFieldName] = $liveId; |
||
847 | $dataTypes[] = \PDO::PARAM_INT; |
||
848 | } |
||
849 | |||
850 | if (empty($updateFields)) { |
||
851 | continue; |
||
852 | } |
||
853 | |||
854 | $connection->update( |
||
855 | $table, |
||
856 | $updateFields, |
||
857 | ['uid' => (int)$record['uid']], |
||
858 | $dataTypes |
||
859 | ); |
||
860 | $dataHandler->updateRefIndex($table, $record['uid']); |
||
861 | } |
||
862 | } |
||
863 | |||
864 | /** |
||
865 | * Processes fields of a record for the publishing/swapping process. |
||
866 | * Basically this takes care of IRRE (type "inline") child references. |
||
867 | * |
||
868 | * @param string $tableName Table name |
||
869 | * @param array $configuration TCA field configuration |
||
870 | * @param array $liveData Live record data |
||
871 | * @param array $versionData Version record data |
||
872 | * @param DataHandler $dataHandler Calling data-handler object |
||
873 | */ |
||
874 | protected function version_swap_processFields($tableName, array $configuration, array $liveData, array $versionData, DataHandler $dataHandler) |
||
904 | ); |
||
905 | } |
||
906 | } |
||
907 | |||
908 | /** |
||
909 | * When a new record in a workspace is published, there is no "replacing" the online version with |
||
910 | * the versioned record, but instead the workspace ID and the state is changed. |
||
911 | * |
||
912 | * @param string $table |
||
913 | * @param array $newRecordInWorkspace |
||
914 | * @param DataHandler $dataHandler |
||
915 | * @param string $comment |
||
916 | * @param array $notificationAlternativeRecipients |
||
917 | */ |
||
918 | protected function publishNewRecord(string $table, array $newRecordInWorkspace, DataHandler $dataHandler, string $comment, array $notificationAlternativeRecipients): void |
||
919 | { |
||
920 | $id = (int)$newRecordInWorkspace['uid']; |
||
921 | $workspaceId = (int)$newRecordInWorkspace['t3ver_wsid']; |
||
922 | if (!$dataHandler->BE_USER->workspacePublishAccess($workspaceId)) { |
||
923 | $dataHandler->newlog('User could not publish records from workspace #' . $workspaceId, SystemLogErrorClassification::USER_ERROR); |
||
924 | return; |
||
925 | } |
||
926 | $wsAccess = $dataHandler->BE_USER->checkWorkspace($workspaceId); |
||
927 | if (!($workspaceId <= 0 || !($wsAccess['publish_access'] & 1) || (int)$newRecordInWorkspace['t3ver_stage'] === StagesService::STAGE_PUBLISH_ID)) { |
||
928 | $dataHandler->newlog('Records in workspace #' . $workspaceId . ' can only be published when in "Publish" stage.', SystemLogErrorClassification::USER_ERROR); |
||
929 | return; |
||
930 | } |
||
931 | if (!($dataHandler->doesRecordExist($table, $id, Permission::PAGE_SHOW) && $dataHandler->checkRecordUpdateAccess($table, $id))) { |
||
932 | $dataHandler->newlog('You cannot publish a record you do not have edit and show permissions for', SystemLogErrorClassification::USER_ERROR); |
||
933 | return; |
||
934 | } |
||
935 | |||
936 | // Modify versioned record to become online |
||
937 | $updatedFields = [ |
||
938 | 't3ver_oid' => 0, |
||
939 | 't3ver_wsid' => 0, |
||
940 | 't3ver_stage' => 0, |
||
941 | 't3ver_state' => VersionState::DEFAULT_STATE |
||
942 | ]; |
||
943 | |||
944 | try { |
||
945 | $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); |
||
946 | $connection->update( |
||
947 | $table, |
||
948 | $updatedFields, |
||
949 | [ |
||
950 | 'uid' => (int)$id |
||
951 | ], |
||
952 | [ |
||
953 | \PDO::PARAM_INT, |
||
954 | \PDO::PARAM_INT, |
||
955 | \PDO::PARAM_INT, |
||
956 | \PDO::PARAM_INT, |
||
957 | \PDO::PARAM_INT |
||
958 | ] |
||
959 | ); |
||
960 | } catch (DBALException $e) { |
||
961 | $dataHandler->newlog('During Publishing: SQL errors happened: ' . $e->getPrevious()->getMessage(), SystemLogErrorClassification::SYSTEM_ERROR); |
||
962 | } |
||
963 | |||
964 | if ($dataHandler->enableLogging) { |
||
965 | $dataHandler->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Publishing successful for table "' . $table . '" uid ' . $id . ' (new record)', -1, [], $dataHandler->eventPid($table, $id, $newRecordInWorkspace['pid'])); |
||
966 | } |
||
967 | |||
968 | // Set log entry for record |
||
969 | $propArr = $dataHandler->getRecordPropertiesFromRow($table, $newRecordInWorkspace); |
||
970 | $label = $this->getLanguageService()->sL('LLL:EXT:workspaces/Resources/Private/Language/locallang_tcemain.xlf:version_swap.online_record_updated'); |
||
971 | $dataHandler->log($table, $id, DatabaseAction::UPDATE, $propArr['pid'], SystemLogErrorClassification::MESSAGE, $label, 10, [$propArr['header'], $table . ':' . $id], $propArr['event_pid']); |
||
972 | $dataHandler->setHistory($table, $id); |
||
973 | |||
974 | $stageId = StagesService::STAGE_PUBLISH_EXECUTE_ID; |
||
975 | $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment; |
||
976 | $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = [$wsAccess, $stageId, $comment]; |
||
977 | $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = [$table, $id]; |
||
978 | $this->notificationEmailInfo[$notificationEmailInfoKey]['recipients'] = $notificationAlternativeRecipients; |
||
979 | // Write to log with stageId -20 (STAGE_PUBLISH_EXECUTE_ID) |
||
980 | if ($dataHandler->enableLogging) { |
||
981 | $dataHandler->log($table, $id, SystemLogGenericAction::UNDEFINED, 0, SystemLogErrorClassification::MESSAGE, 'Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', -1, [], $dataHandler->eventPid($table, $id, $newRecordInWorkspace['pid'])); |
||
982 | } |
||
983 | $dataHandler->log($table, $id, DatabaseAction::UPDATE, 0, SystemLogErrorClassification::MESSAGE, 'Published', 30, ['comment' => $comment, 'stage' => $stageId]); |
||
984 | |||
985 | // Clear cache |
||
986 | $dataHandler->registerRecordIdForPageCacheClearing($table, $id); |
||
987 | // Update the reference index: Drop the references in the workspace, but update them in the live workspace |
||
988 | $dataHandler->registerReferenceIndexRowsForDrop($table, $id, $workspaceId); |
||
989 | $dataHandler->updateRefIndex($table, $id, 0); |
||
990 | $this->updateReferenceIndexForL10nOverlays($table, $id, $workspaceId, $dataHandler); |
||
991 | } |
||
992 | |||
993 | /** |
||
994 | * A new record was just published, but the reference index for the localized elements needs |
||
995 | * an update too. |
||
996 | * |
||
997 | * @param string $table |
||
998 | * @param int $newVersionedRecordId |
||
999 | * @param int $workspaceId |
||
1000 | * @param DataHandler $dataHandler |
||
1001 | */ |
||
1002 | protected function updateReferenceIndexForL10nOverlays(string $table, int $newVersionedRecordId, int $workspaceId, DataHandler $dataHandler): void |
||
1003 | { |
||
1004 | if (!BackendUtility::isTableLocalizable($table)) { |
||
1005 | return; |
||
1006 | } |
||
1007 | if (!BackendUtility::isTableWorkspaceEnabled($table)) { |
||
1008 | return; |
||
1009 | } |
||
1010 | $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table); |
||
1011 | $queryBuilder = $connection->createQueryBuilder(); |
||
1012 | $queryBuilder->getRestrictions()->removeAll(); |
||
1013 | |||
1014 | $l10nParentFieldName = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']; |
||
1015 | $constraints = $queryBuilder->expr()->eq( |
||
1016 | $l10nParentFieldName, |
||
1017 | $queryBuilder->createNamedParameter($newVersionedRecordId, \PDO::PARAM_INT) |
||
1018 | ); |
||
1019 | $translationSourceFieldName = $GLOBALS['TCA'][$table]['ctrl']['translationSource'] ?? null; |
||
1020 | if ($translationSourceFieldName) { |
||
1021 | $constraints = $queryBuilder->expr()->orX( |
||
1022 | $constraints, |
||
1023 | $queryBuilder->expr()->eq( |
||
1024 | $translationSourceFieldName, |
||
1025 | $queryBuilder->createNamedParameter($newVersionedRecordId, \PDO::PARAM_INT) |
||
1026 | ) |
||
1027 | ); |
||
1028 | } |
||
1029 | |||
1030 | $queryBuilder |
||
1031 | ->select('uid', $l10nParentFieldName) |
||
1032 | ->from($table) |
||
1033 | ->where( |
||
1034 | $constraints, |
||
1035 | $queryBuilder->expr()->eq( |
||
1036 | 't3ver_wsid', |
||
1037 | $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT) |
||
1038 | ) |
||
1039 | ); |
||
1040 | |||
1041 | if ($translationSourceFieldName) { |
||
1042 | $queryBuilder->addSelect($translationSourceFieldName); |
||
1043 | } |
||
1044 | |||
1045 | $statement = $queryBuilder->execute(); |
||
1046 | while ($record = $statement->fetch()) { |
||
1047 | $dataHandler->updateRefIndex($table, $record['uid']); |
||
1048 | } |
||
1049 | } |
||
1050 | |||
1051 | /** |
||
1052 | * Updates foreign field sorting values of versioned and live |
||
1053 | * parents after(!) the whole structure has been published. |
||
1054 | * |
||
1055 | * This method is used as callback function in |
||
1056 | * DataHandlerHook::version_swap_procBasedOnFieldType(). |
||
1057 | * Sorting fields ("sortby") are not modified during the |
||
1058 | * workspace publishing/swapping process directly. |
||
1059 | * |
||
1060 | * @param string $parentId |
||
1061 | * @param string $foreignTableName |
||
1062 | * @param int[] $foreignIds |
||
1063 | * @param array $configuration |
||
1064 | * @param int $targetWorkspaceId |
||
1065 | * @internal |
||
1066 | */ |
||
1067 | public function updateInlineForeignFieldSorting($parentId, $foreignTableName, $foreignIds, array $configuration, $targetWorkspaceId) |
||
1068 | { |
||
1069 | $remappedIds = []; |
||
1070 | // Use remapped ids (live id <-> version id) |
||
1071 | foreach ($foreignIds as $foreignId) { |
||
1072 | if (!empty($this->remappedIds[$foreignTableName][$foreignId])) { |
||
1073 | $remappedIds[] = $this->remappedIds[$foreignTableName][$foreignId]; |
||
1074 | } else { |
||
1075 | $remappedIds[] = $foreignId; |
||
1076 | } |
||
1077 | } |
||
1078 | |||
1079 | $relationHandler = $this->createRelationHandlerInstance(); |
||
1080 | $relationHandler->setWorkspaceId($targetWorkspaceId); |
||
1081 | $relationHandler->setUseLiveReferenceIds(false); |
||
1082 | $relationHandler->start(implode(',', $remappedIds), $foreignTableName); |
||
1083 | $relationHandler->processDeletePlaceholder(); |
||
1084 | $relationHandler->writeForeignField($configuration, $parentId); |
||
|
|||
1085 | } |
||
1086 | |||
1087 | /** |
||
1088 | * In case a sys_workspace_stage record is deleted we do a hard reset |
||
1089 | * for all existing records in that stage to avoid that any of these end up |
||
1090 | * as orphan records. |
||
1091 | * |
||
1092 | * @param int $stageId Elements with this stage are reset |
||
1093 | */ |
||
1094 | protected function resetStageOfElements(int $stageId): void |
||
1095 | { |
||
1096 | foreach ($this->getTcaTables() as $tcaTable) { |
||
1097 | if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) { |
||
1098 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) |
||
1099 | ->getQueryBuilderForTable($tcaTable); |
||
1100 | |||
1101 | $queryBuilder |
||
1102 | ->update($tcaTable) |
||
1103 | ->set('t3ver_stage', StagesService::STAGE_EDIT_ID) |
||
1104 | ->where( |
||
1105 | $queryBuilder->expr()->eq( |
||
1106 | 't3ver_stage', |
||
1107 | $queryBuilder->createNamedParameter($stageId, \PDO::PARAM_INT) |
||
1108 | ), |
||
1109 | $queryBuilder->expr()->gt( |
||
1110 | 't3ver_wsid', |
||
1111 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
1112 | ) |
||
1113 | ) |
||
1114 | ->execute(); |
||
1115 | } |
||
1116 | } |
||
1117 | } |
||
1118 | |||
1119 | /** |
||
1120 | * Flushes (remove, no soft delete!) elements of a particular workspace to avoid orphan records. |
||
1121 | * This is used if an admin deletes a sys_workspace record. |
||
1122 | * |
||
1123 | * @param int $workspaceId The workspace to be flushed |
||
1124 | */ |
||
1125 | protected function flushWorkspaceElements(int $workspaceId): void |
||
1126 | { |
||
1127 | $command = []; |
||
1128 | foreach ($this->getTcaTables() as $tcaTable) { |
||
1129 | if (BackendUtility::isTableWorkspaceEnabled($tcaTable)) { |
||
1130 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) |
||
1131 | ->getQueryBuilderForTable($tcaTable); |
||
1132 | $queryBuilder->getRestrictions()->removeAll(); |
||
1133 | $result = $queryBuilder |
||
1134 | ->select('uid') |
||
1135 | ->from($tcaTable) |
||
1136 | ->where( |
||
1137 | $queryBuilder->expr()->eq( |
||
1138 | 't3ver_wsid', |
||
1139 | $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT) |
||
1140 | ), |
||
1141 | // t3ver_oid >= 0 basically omits placeholder records here, those would otherwise |
||
1142 | // fail to delete later in DH->discard() and would create "can't do that" log entries. |
||
1143 | $queryBuilder->expr()->orX( |
||
1144 | $queryBuilder->expr()->gt( |
||
1145 | 't3ver_oid', |
||
1146 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
1147 | ), |
||
1148 | $queryBuilder->expr()->eq( |
||
1149 | 't3ver_state', |
||
1150 | $queryBuilder->createNamedParameter(VersionState::NEW_PLACEHOLDER, \PDO::PARAM_INT) |
||
1151 | ) |
||
1152 | ) |
||
1153 | ) |
||
1154 | ->orderBy('uid') |
||
1155 | ->execute(); |
||
1156 | |||
1157 | while (($recordId = $result->fetchOne()) !== false) { |
||
1158 | $command[$tcaTable][$recordId]['version']['action'] = 'flush'; |
||
1159 | } |
||
1160 | } |
||
1161 | } |
||
1162 | if (!empty($command)) { |
||
1163 | // Execute the command array via DataHandler to flush all records from this workspace. |
||
1164 | // Switch to target workspace temporarily, otherwise DH->discard() do not |
||
1165 | // operate on correct workspace if fetching additional records. |
||
1166 | $backendUser = $GLOBALS['BE_USER']; |
||
1167 | $savedWorkspace = $backendUser->workspace; |
||
1168 | $backendUser->workspace = $workspaceId; |
||
1169 | $context = GeneralUtility::makeInstance(Context::class); |
||
1170 | $savedWorkspaceContext = $context->getAspect('workspace'); |
||
1171 | $context->setAspect('workspace', new WorkspaceAspect($workspaceId)); |
||
1172 | |||
1173 | $dataHandler = GeneralUtility::makeInstance(DataHandler::class); |
||
1174 | $dataHandler->start([], $command, $backendUser); |
||
1175 | $dataHandler->process_cmdmap(); |
||
1176 | |||
1177 | $backendUser->workspace = $savedWorkspace; |
||
1178 | $context->setAspect('workspace', $savedWorkspaceContext); |
||
1179 | } |
||
1180 | } |
||
1181 | |||
1182 | /** |
||
1183 | * Gets all defined TCA tables. |
||
1184 | * |
||
1185 | * @return array |
||
1186 | */ |
||
1187 | protected function getTcaTables(): array |
||
1188 | { |
||
1189 | return array_keys($GLOBALS['TCA']); |
||
1190 | } |
||
1191 | |||
1192 | /** |
||
1193 | * Flushes the workspace cache for current workspace and for the virtual "all workspaces" too. |
||
1194 | * |
||
1195 | * @param int $workspaceId The workspace to be flushed in cache |
||
1196 | */ |
||
1197 | protected function flushWorkspaceCacheEntriesByWorkspaceId(int $workspaceId): void |
||
1198 | { |
||
1199 | $workspacesCache = GeneralUtility::makeInstance(CacheManager::class)->getCache('workspaces_cache'); |
||
1200 | $workspacesCache->flushByTag($workspaceId); |
||
1201 | } |
||
1202 | |||
1203 | /******************************* |
||
1204 | ***** helper functions ****** |
||
1205 | *******************************/ |
||
1206 | |||
1207 | /** |
||
1208 | * Finds all elements for swapping versions in workspace |
||
1209 | * |
||
1210 | * @param string $table Table name of the original element to swap |
||
1211 | * @param int $id UID of the original element to swap (online) |
||
1212 | * @param int $offlineId As above but offline |
||
1213 | * @return array Element data. Key is table name, values are array with first element as online UID, second - offline UID |
||
1214 | */ |
||
1215 | public function findPageElementsForVersionSwap($table, $id, $offlineId) |
||
1216 | { |
||
1217 | $rec = BackendUtility::getRecord($table, $offlineId, 't3ver_wsid'); |
||
1218 | $workspaceId = (int)$rec['t3ver_wsid']; |
||
1219 | $elementData = []; |
||
1220 | if ($workspaceId === 0) { |
||
1221 | return $elementData; |
||
1222 | } |
||
1223 | // Get page UID for LIVE and workspace |
||
1224 | if ($table !== 'pages') { |
||
1225 | $rec = BackendUtility::getRecord($table, $id, 'pid'); |
||
1226 | $pageId = $rec['pid']; |
||
1227 | $rec = BackendUtility::getRecord('pages', $pageId); |
||
1228 | BackendUtility::workspaceOL('pages', $rec, $workspaceId); |
||
1229 | $offlinePageId = $rec['_ORIG_uid']; |
||
1230 | } else { |
||
1231 | $pageId = $id; |
||
1232 | $offlinePageId = $offlineId; |
||
1233 | } |
||
1234 | // Traversing all tables supporting versioning: |
||
1235 | foreach ($GLOBALS['TCA'] as $table => $cfg) { |
||
1236 | if (BackendUtility::isTableWorkspaceEnabled($table) && $table !== 'pages') { |
||
1237 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) |
||
1238 | ->getQueryBuilderForTable($table); |
||
1239 | |||
1240 | $queryBuilder->getRestrictions() |
||
1241 | ->removeAll() |
||
1242 | ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); |
||
1243 | |||
1244 | $statement = $queryBuilder |
||
1245 | ->select('A.uid AS offlineUid', 'B.uid AS uid') |
||
1246 | ->from($table, 'A') |
||
1247 | ->from($table, 'B') |
||
1248 | ->where( |
||
1249 | $queryBuilder->expr()->gt( |
||
1250 | 'A.t3ver_oid', |
||
1251 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
1252 | ), |
||
1253 | $queryBuilder->expr()->eq( |
||
1254 | 'B.pid', |
||
1255 | $queryBuilder->createNamedParameter($pageId, \PDO::PARAM_INT) |
||
1256 | ), |
||
1257 | $queryBuilder->expr()->eq( |
||
1258 | 'A.t3ver_wsid', |
||
1259 | $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT) |
||
1260 | ), |
||
1261 | $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid')) |
||
1262 | ) |
||
1263 | ->execute(); |
||
1264 | |||
1265 | while ($row = $statement->fetch()) { |
||
1266 | $elementData[$table][] = [$row['uid'], $row['offlineUid']]; |
||
1267 | } |
||
1268 | } |
||
1269 | } |
||
1270 | if ($offlinePageId && $offlinePageId != $pageId) { |
||
1271 | $elementData['pages'][] = [$pageId, $offlinePageId]; |
||
1272 | } |
||
1273 | |||
1274 | return $elementData; |
||
1275 | } |
||
1276 | |||
1277 | /** |
||
1278 | * Searches for all elements from all tables on the given pages in the same workspace. |
||
1279 | * |
||
1280 | * @param array $pageIdList List of PIDs to search |
||
1281 | * @param int $workspaceId Workspace ID |
||
1282 | * @param array $elementList List of found elements. Key is table name, value is array of element UIDs |
||
1283 | */ |
||
1284 | public function findPageElementsForVersionStageChange(array $pageIdList, $workspaceId, array &$elementList) |
||
1285 | { |
||
1286 | if ($workspaceId == 0) { |
||
1287 | return; |
||
1288 | } |
||
1289 | // Traversing all tables supporting versioning: |
||
1290 | foreach ($GLOBALS['TCA'] as $table => $cfg) { |
||
1291 | if (BackendUtility::isTableWorkspaceEnabled($table) && $table !== 'pages') { |
||
1292 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) |
||
1293 | ->getQueryBuilderForTable($table); |
||
1294 | |||
1295 | $queryBuilder->getRestrictions() |
||
1296 | ->removeAll() |
||
1297 | ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); |
||
1298 | |||
1299 | $statement = $queryBuilder |
||
1300 | ->select('A.uid') |
||
1301 | ->from($table, 'A') |
||
1302 | ->from($table, 'B') |
||
1303 | ->where( |
||
1304 | $queryBuilder->expr()->gt( |
||
1305 | 'A.t3ver_oid', |
||
1306 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
1307 | ), |
||
1308 | $queryBuilder->expr()->in( |
||
1309 | 'B.pid', |
||
1310 | $queryBuilder->createNamedParameter($pageIdList, Connection::PARAM_INT_ARRAY) |
||
1311 | ), |
||
1312 | $queryBuilder->expr()->eq( |
||
1313 | 'A.t3ver_wsid', |
||
1314 | $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT) |
||
1315 | ), |
||
1316 | $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid')) |
||
1317 | ) |
||
1318 | ->groupBy('A.uid') |
||
1319 | ->execute(); |
||
1320 | |||
1321 | while ($row = $statement->fetch()) { |
||
1322 | $elementList[$table][] = $row['uid']; |
||
1323 | } |
||
1324 | if (is_array($elementList[$table])) { |
||
1325 | // Yes, it is possible to get non-unique array even with DISTINCT above! |
||
1326 | // It happens because several UIDs are passed in the array already. |
||
1327 | $elementList[$table] = array_unique($elementList[$table]); |
||
1328 | } |
||
1329 | } |
||
1330 | } |
||
1331 | } |
||
1332 | |||
1333 | /** |
||
1334 | * Finds page UIDs for the element from table <code>$table</code> with UIDs from <code>$idList</code> |
||
1335 | * |
||
1336 | * @param string $table Table to search |
||
1337 | * @param array $idList List of records' UIDs |
||
1338 | * @param int $workspaceId Workspace ID. We need this parameter because user can be in LIVE but he still can publish DRAFT from ws module! |
||
1339 | * @param array $pageIdList List of found page UIDs |
||
1340 | * @param array $elementList List of found element UIDs. Key is table name, value is list of UIDs |
||
1341 | */ |
||
1342 | public function findPageIdsForVersionStateChange($table, array $idList, $workspaceId, array &$pageIdList, array &$elementList) |
||
1343 | { |
||
1344 | if ($workspaceId == 0) { |
||
1345 | return; |
||
1346 | } |
||
1347 | |||
1348 | $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) |
||
1349 | ->getQueryBuilderForTable($table); |
||
1350 | $queryBuilder->getRestrictions() |
||
1351 | ->removeAll() |
||
1352 | ->add(GeneralUtility::makeInstance(DeletedRestriction::class)); |
||
1353 | |||
1354 | $statement = $queryBuilder |
||
1355 | ->select('B.pid') |
||
1356 | ->from($table, 'A') |
||
1357 | ->from($table, 'B') |
||
1358 | ->where( |
||
1359 | $queryBuilder->expr()->gt( |
||
1360 | 'A.t3ver_oid', |
||
1361 | $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT) |
||
1362 | ), |
||
1363 | $queryBuilder->expr()->eq( |
||
1364 | 'A.t3ver_wsid', |
||
1365 | $queryBuilder->createNamedParameter($workspaceId, \PDO::PARAM_INT) |
||
1366 | ), |
||
1367 | $queryBuilder->expr()->in( |
||
1368 | 'A.uid', |
||
1369 | $queryBuilder->createNamedParameter($idList, Connection::PARAM_INT_ARRAY) |
||
1370 | ), |
||
1371 | $queryBuilder->expr()->eq('A.t3ver_oid', $queryBuilder->quoteIdentifier('B.uid')) |
||
1372 | ) |
||
1373 | ->groupBy('B.pid') |
||
1374 | ->execute(); |
||
1375 | |||
1376 | while ($row = $statement->fetch()) { |
||
1377 | $pageIdList[] = $row['pid']; |
||
1378 | // Find ws version |
||
1379 | // Note: cannot use BackendUtility::getRecordWSOL() |
||
1380 | // here because it does not accept workspace id! |
||
1381 | $rec = BackendUtility::getRecord('pages', $row[0]); |
||
1382 | BackendUtility::workspaceOL('pages', $rec, $workspaceId); |
||
1383 | if ($rec['_ORIG_uid']) { |
||
1384 | $elementList['pages'][$row[0]] = $rec['_ORIG_uid']; |
||
1385 | } |
||
1386 | } |
||
1387 | // The line below is necessary even with DISTINCT |
||
1388 | // because several elements can be passed by caller |
||
1389 | $pageIdList = array_unique($pageIdList); |
||
1390 | } |
||
1391 | |||
1392 | /** |
||
1393 | * Finds real page IDs for state change. |
||
1394 | * |
||
1395 | * @param array $idList List of page UIDs, possibly versioned |
||
1396 | */ |
||
1397 | public function findRealPageIds(array &$idList): void |
||
1398 | { |
||
1399 | foreach ($idList as $key => $id) { |
||
1400 | $rec = BackendUtility::getRecord('pages', $id, 't3ver_oid'); |
||
1401 | if ($rec['t3ver_oid'] > 0) { |
||
1402 | $idList[$key] = $rec['t3ver_oid']; |
||
1403 | } |
||
1404 | } |
||
1405 | } |
||
1406 | |||
1407 | /** |
||
1408 | * Moves a versioned record, which is not new or deleted. |
||
1409 | * |
||
1410 | * This is critical for a versioned record to be marked as MOVED (t3ver_state=4) |
||
1411 | * |
||
1412 | * @param string $table Table name to move |
||
1413 | * @param int $liveUid Record uid to move (online record) |
||
1414 | * @param int $destPid Position to move to: $destPid: >=0 then it points to a page-id on which to insert the record (as the first element). <0 then it points to a uid from its own table after which to insert it (works if |
||
1415 | * @param int $versionedRecordUid UID of offline version of online record |
||
1416 | * @param DataHandler $dataHandler DataHandler object |
||
1417 | * @see moveRecord() |
||
1418 | */ |
||
1419 | protected function moveRecord_moveVersionedRecord(string $table, int $liveUid, int $destPid, int $versionedRecordUid, DataHandler $dataHandler): void |
||
1420 | { |
||
1421 | // If a record gets moved after a record that already has a versioned record |
||
1422 | // then the versioned record needs to be placed after the existing one |
||
1423 | $originalRecordDestinationPid = $destPid; |
||
1424 | $movedTargetRecordInWorkspace = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, abs($destPid), 'uid'); |
||
1425 | if (is_array($movedTargetRecordInWorkspace) && $destPid < 0) { |
||
1426 | $destPid = -$movedTargetRecordInWorkspace['uid']; |
||
1427 | } |
||
1428 | $dataHandler->moveRecord_raw($table, $versionedRecordUid, $destPid); |
||
1429 | |||
1430 | $versionedRecord = BackendUtility::getRecord($table, $versionedRecordUid, 'uid,t3ver_state'); |
||
1431 | if (!VersionState::cast($versionedRecord['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)) { |
||
1432 | // Update the state of this record to a move placeholder. This is allowed if the |
||
1433 | // record is a 'changed' (t3ver_state=0) record: Changing a record and moving it |
||
1434 | // around later, should switch it from 'changed' to 'moved'. Deleted placeholders |
||
1435 | // however are an 'end-state', they should not be switched to a move placeholder. |
||
1436 | // Scenario: For a live page that has a localization, the localization is first |
||
1437 | // marked as to-delete in workspace, creating a delete placeholder for that |
||
1438 | // localization. Later, the page is moved around, moving the localization along |
||
1439 | // with the default language record. The localization should then NOT be switched |
||
1440 | // from 'to-delete' to 'moved', this would loose the 'to-delete' information. |
||
1441 | GeneralUtility::makeInstance(ConnectionPool::class) |
||
1442 | ->getConnectionForTable($table) |
||
1443 | ->update( |
||
1444 | $table, |
||
1445 | [ |
||
1446 | 't3ver_state' => (string)new VersionState(VersionState::MOVE_POINTER) |
||
1447 | ], |
||
1448 | [ |
||
1449 | 'uid' => (int)$versionedRecordUid |
||
1450 | ] |
||
1451 | ); |
||
1452 | } |
||
1453 | |||
1454 | // Check for the localizations of that element and move them as well |
||
1455 | $dataHandler->moveL10nOverlayRecords($table, $liveUid, $destPid, $originalRecordDestinationPid); |
||
1456 | } |
||
1457 | |||
1458 | /** |
||
1459 | * Gets an instance of the command map helper. |
||
1460 | * |
||
1461 | * @param DataHandler $dataHandler DataHandler object |
||
1462 | * @return CommandMap |
||
1463 | */ |
||
1464 | public function getCommandMap(DataHandler $dataHandler): CommandMap |
||
1465 | { |
||
1466 | return GeneralUtility::makeInstance( |
||
1467 | CommandMap::class, |
||
1468 | $this, |
||
1469 | $dataHandler, |
||
1470 | $dataHandler->cmdmap, |
||
1471 | $dataHandler->BE_USER->workspace |
||
1472 | ); |
||
1473 | } |
||
1474 | |||
1475 | protected function emitUpdateTopbarSignal(): void |
||
1476 | { |
||
1477 | BackendUtility::setUpdateSignal('updateTopbar'); |
||
1478 | } |
||
1479 | |||
1480 | /** |
||
1481 | * Returns all fieldnames from a table which have the unique evaluation type set. |
||
1482 | * |
||
1483 | * @param string $table Table name |
||
1484 | * @return array Array of fieldnames |
||
1485 | */ |
||
1486 | protected function getUniqueFields($table): array |
||
1498 | } |
||
1499 | |||
1500 | /** |
||
1501 | * Straight db based record deletion: sets deleted = 1 for soft-delete |
||
1502 | * enabled tables, or removes row from table. Used for move placeholder |
||
1503 | * records sometimes. |
||
1504 | */ |
||
1505 | protected function softOrHardDeleteSingleRecord(string $table, int $uid): void |
||
1506 | { |
||
1507 | $deleteField = $GLOBALS['TCA'][$table]['ctrl']['delete'] ?? null; |
||
1508 | if ($deleteField) { |
||
1509 | GeneralUtility::makeInstance(ConnectionPool::class) |
||
1510 | ->getConnectionForTable($table) |
||
1511 | ->update( |
||
1512 | $table, |
||
1513 | [$deleteField => 1], |
||
1514 | ['uid' => $uid], |
||
1515 | [\PDO::PARAM_INT] |
||
1516 | ); |
||
1517 | } else { |
||
1518 | GeneralUtility::makeInstance(ConnectionPool::class) |
||
1519 | ->getConnectionForTable($table) |
||
1520 | ->delete( |
||
1521 | $table, |
||
1522 | ['uid' => $uid] |
||
1523 | ); |
||
1524 | } |
||
1525 | } |
||
1526 | |||
1527 | /** |
||
1528 | * @return RelationHandler |
||
1529 | */ |
||
1530 | protected function createRelationHandlerInstance(): RelationHandler |
||
1533 | } |
||
1534 | |||
1535 | /** |
||
1536 | * @return LanguageService |
||
1537 | */ |
||
1538 | protected function getLanguageService(): LanguageService |
||
1541 | } |
||
1542 | } |
||
1543 |