[BUGFIX] Set correct workspace ID when updating referenceIndex for files
[Packages/TYPO3.CMS.git] / typo3 / sysext / version / Classes / Hook / DataHandlerHook.php
1 <?php
2 namespace TYPO3\CMS\Version\Hook;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Backend\Utility\BackendUtility;
18 use TYPO3\CMS\Core\Database\ReferenceIndex;
19 use TYPO3\CMS\Core\DataHandling\DataHandler;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21 use TYPO3\CMS\Core\Versioning\VersionState;
22
23 /**
24 * Contains some parts for staging, versioning and workspaces
25 * to interact with the TYPO3 Core Engine
26 */
27 class DataHandlerHook {
28
29 /**
30 * For accumulating information about workspace stages raised
31 * on elements so a single mail is sent as notification.
32 * previously called "accumulateForNotifEmail" in tcemain
33 *
34 * @var array
35 */
36 protected $notificationEmailInfo = array();
37
38 /**
39 * General comment, eg. for staging in workspaces
40 *
41 * @var string
42 */
43 protected $generalComment = '';
44
45 /**
46 * Contains remapped IDs.
47 *
48 * @var array
49 */
50 protected $remappedIds = array();
51
52 /****************************
53 ***** Cmdmap Hooks ******
54 ****************************/
55 /**
56 * hook that is called before any cmd of the commandmap is executed
57 *
58 * @param DataHandler $tcemainObj reference to the main tcemain object
59 * @return void
60 */
61 public function processCmdmap_beforeStart(DataHandler $tcemainObj) {
62 // Reset notification array
63 $this->notificationEmailInfo = array();
64 // Resolve dependencies of version/workspaces actions:
65 $tcemainObj->cmdmap = $this->getCommandMap($tcemainObj)->process()->get();
66 }
67
68 /**
69 * hook that is called when no prepared command was found
70 *
71 * @param string $command the command to be executed
72 * @param string $table the table of the record
73 * @param int $id the ID of the record
74 * @param mixed $value the value containing the data
75 * @param bool $commandIsProcessed can be set so that other hooks or
76 * @param DataHandler $tcemainObj reference to the main tcemain object
77 * @return void
78 */
79 public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, DataHandler $tcemainObj) {
80 // custom command "version"
81 if ($command == 'version') {
82 $commandIsProcessed = TRUE;
83 $action = (string)$value['action'];
84 $comment = (isset($value['comment']) && $value['comment'] ? $value['comment'] : $this->generalComment);
85 $notificationAlternativeRecipients = (isset($value['notificationAlternativeRecipients'])) && is_array($value['notificationAlternativeRecipients']) ? $value['notificationAlternativeRecipients'] : array();
86 switch ($action) {
87 case 'new':
88 // check if page / branch versioning is needed,
89 // or if "element" version can be used
90 $versionizeTree = -1;
91 if (isset($value['treeLevels'])) {
92 $versionizeTree = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($value['treeLevels'], -1, 100);
93 }
94 if ($table == 'pages' && $versionizeTree >= 0) {
95 $this->versionizePages($id, $value['label'], $versionizeTree, $tcemainObj);
96 } else {
97 $tcemainObj->versionizeRecord($table, $id, $value['label']);
98 }
99 break;
100 case 'swap':
101 $this->version_swap($table, $id, $value['swapWith'], $value['swapIntoWS'],
102 $tcemainObj,
103 $comment,
104 TRUE,
105 $notificationAlternativeRecipients
106 );
107 break;
108 case 'clearWSID':
109 $this->version_clearWSID($table, $id, FALSE, $tcemainObj);
110 break;
111 case 'flush':
112 $this->version_clearWSID($table, $id, TRUE, $tcemainObj);
113 break;
114 case 'setStage':
115 $elementIds = GeneralUtility::trimExplode(',', $id, TRUE);
116 foreach ($elementIds as $elementId) {
117 $this->version_setStage($table, $elementId, $value['stageId'],
118 $comment,
119 TRUE,
120 $tcemainObj,
121 $notificationAlternativeRecipients
122 );
123 }
124 break;
125 default:
126 // Do nothing
127 }
128 }
129 }
130
131 /**
132 * hook that is called AFTER all commands of the commandmap was
133 * executed
134 *
135 * @param DataHandler $tcemainObj reference to the main tcemain object
136 * @return void
137 */
138 public function processCmdmap_afterFinish(DataHandler $tcemainObj) {
139 // Empty accumulation array:
140 foreach ($this->notificationEmailInfo as $notifItem) {
141 $this->notifyStageChange($notifItem['shared'][0], $notifItem['shared'][1], implode(', ', $notifItem['elements']), 0, $notifItem['shared'][2], $tcemainObj, $notifItem['alternativeRecipients']);
142 }
143 // Reset notification array
144 $this->notificationEmailInfo = array();
145 // Reset remapped IDs
146 $this->remappedIds = array();
147 }
148
149 /**
150 * hook that is called when an element shall get deleted
151 *
152 * @param string $table the table of the record
153 * @param int $id the ID of the record
154 * @param array $record The accordant database record
155 * @param bool $recordWasDeleted can be set so that other hooks or
156 * @param DataHandler $tcemainObj reference to the main tcemain object
157 * @return void
158 */
159 public function processCmdmap_deleteAction($table, $id, array $record, &$recordWasDeleted, DataHandler $tcemainObj) {
160 // only process the hook if it wasn't processed
161 // by someone else before
162 if (!$recordWasDeleted) {
163 $recordWasDeleted = TRUE;
164 // For Live version, try if there is a workspace version because if so, rather "delete" that instead
165 // Look, if record is an offline version, then delete directly:
166 if ($record['pid'] != -1) {
167 if ($wsVersion = BackendUtility::getWorkspaceVersionOfRecord($tcemainObj->BE_USER->workspace, $table, $id)) {
168 $record = $wsVersion;
169 $id = $record['uid'];
170 }
171 }
172 $recordVersionState = VersionState::cast($record['t3ver_state']);
173 // Look, if record is an offline version, then delete directly:
174 if ($record['pid'] == -1) {
175 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS']) {
176 // In Live workspace, delete any. In other workspaces there must be match.
177 if ($tcemainObj->BE_USER->workspace == 0 || (int)$record['t3ver_wsid'] == $tcemainObj->BE_USER->workspace) {
178 $liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state');
179 // Processing can be skipped if a delete placeholder shall be swapped/published
180 // during the current request. Thus it will be deleted later on...
181 $liveRecordVersionState = VersionState::cast($liveRec['t3ver_state']);
182 if ($recordVersionState->equals(VersionState::DELETE_PLACEHOLDER) && !empty($liveRec['uid'])
183 && !empty($tcemainObj->cmdmap[$table][$liveRec['uid']]['version']['action'])
184 && !empty($tcemainObj->cmdmap[$table][$liveRec['uid']]['version']['swapWith'])
185 && $tcemainObj->cmdmap[$table][$liveRec['uid']]['version']['action'] === 'swap'
186 && $tcemainObj->cmdmap[$table][$liveRec['uid']]['version']['swapWith'] == $id
187 ) {
188 return NULL;
189 }
190
191 if ($record['t3ver_wsid'] > 0 && $recordVersionState->equals(VersionState::DEFAULT_STATE)) {
192 // Change normal versioned record to delete placeholder
193 // Happens when an edited record is deleted
194 $updateFields = array(
195 't3ver_label' => 'DELETED!',
196 't3ver_state' => 2,
197 );
198 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . $id, $updateFields);
199 // Delete localization overlays:
200 $tcemainObj->deleteL10nOverlayRecords($table, $id);
201
202 } elseif ($record['t3ver_wsid'] == 0 || !$liveRecordVersionState->indicatesPlaceholder()) {
203 // Delete those in WS 0 + if their live records state was not "Placeholder".
204 $tcemainObj->deleteEl($table, $id);
205 } else {
206 // If live record was placeholder (new/deleted), rather clear
207 // it from workspace (because it clears both version and placeholder).
208 $this->version_clearWSID($table, $id, FALSE, $tcemainObj);
209 }
210 } else {
211 $tcemainObj->newlog('Tried to delete record from another workspace', 1);
212 }
213 } else {
214 $tcemainObj->newlog('Versioning not enabled for record with PID = -1!', 2);
215 }
216 } elseif ($res = $tcemainObj->BE_USER->workspaceAllowLiveRecordsInPID($record['pid'], $table)) {
217 // Look, if record is "online" or in a versionized branch, then delete directly.
218 if ($res > 0) {
219 $tcemainObj->deleteEl($table, $id);
220 } else {
221 $tcemainObj->newlog('Stage of root point did not allow for deletion', 1);
222 }
223 } elseif ($recordVersionState->equals(VersionState::MOVE_PLACEHOLDER)) {
224 // Placeholders for moving operations are deletable directly.
225 // Get record which its a placeholder for and reset the t3ver_state of that:
226 if ($wsRec = BackendUtility::getWorkspaceVersionOfRecord($record['t3ver_wsid'], $table, $record['t3ver_move_id'], 'uid')) {
227 // Clear the state flag of the workspace version of the record
228 // Setting placeholder state value for version (so it can know it is currently a new version...)
229 $updateFields = array(
230 't3ver_state' => (string)new VersionState(VersionState::DEFAULT_STATE)
231 );
232 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$wsRec['uid'], $updateFields);
233 }
234 $tcemainObj->deleteEl($table, $id);
235 } else {
236 // Otherwise, try to delete by versioning:
237 $copyMappingArray = $tcemainObj->copyMappingArray;
238 $tcemainObj->versionizeRecord($table, $id, 'DELETED!', TRUE);
239 // Determine newly created versions:
240 // (remove placeholders are copied and modified, thus they appear in the copyMappingArray)
241 $versionizedElements = GeneralUtility::arrayDiffAssocRecursive($tcemainObj->copyMappingArray, $copyMappingArray);
242 // Delete localization overlays:
243 foreach ($versionizedElements as $versionizedTableName => $versionizedOriginalIds) {
244 foreach ($versionizedOriginalIds as $versionizedOriginalId => $_) {
245 $tcemainObj->deleteL10nOverlayRecords($versionizedTableName, $versionizedOriginalId);
246 }
247 }
248 }
249 }
250 }
251
252 /**
253 * Hook for \TYPO3\CMS\Core\DataHandling\DataHandler::moveRecord that cares about
254 * moving records that are *not* in the live workspace
255 *
256 * @param string $table the table of the record
257 * @param int $uid the ID of the record
258 * @param int $destPid Position to move to: $destPid: >=0 then it points to
259 * @param array $propArr Record properties, like header and pid (includes workspace overlay)
260 * @param array $moveRec Record properties, like header and pid (without workspace overlay)
261 * @param int $resolvedPid The final page ID of the record
262 * @param bool $recordWasMoved can be set so that other hooks or
263 * @param DataHandler $tcemainObj
264 * @return void
265 */
266 public function moveRecord($table, $uid, $destPid, array $propArr, array $moveRec, $resolvedPid, &$recordWasMoved, DataHandler $tcemainObj) {
267 // Only do something in Draft workspace
268 if ($tcemainObj->BE_USER->workspace !== 0) {
269 if ($destPid < 0) {
270 // Fetch move placeholder, since it might point to a new page in the current workspace
271 $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid,pid');
272 if ($movePlaceHolder !== FALSE) {
273 $resolvedPid = $movePlaceHolder['pid'];
274 }
275 }
276 $recordWasMoved = TRUE;
277 $moveRecVersionState = VersionState::cast($moveRec['t3ver_state']);
278 // Get workspace version of the source record, if any:
279 $WSversion = BackendUtility::getWorkspaceVersionOfRecord($tcemainObj->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
280 // Handle move-placeholders if the current record is not one already
281 if (
282 BackendUtility::isTableMovePlaceholderAware($table)
283 && !$moveRecVersionState->equals(VersionState::MOVE_PLACEHOLDER)
284 ) {
285 // Create version of record first, if it does not exist
286 if (empty($WSversion['uid'])) {
287 $tcemainObj->versionizeRecord($table, $uid, 'MovePointer');
288 $WSversion = BackendUtility::getWorkspaceVersionOfRecord($tcemainObj->BE_USER->workspace, $table, $uid, 'uid,t3ver_oid');
289 $this->moveRecord_processFields($tcemainObj, $resolvedPid, $table, $uid);
290 // If the record has been versioned before (e.g. cascaded parent-child structure), create only the move-placeholders
291 } elseif ($tcemainObj->isRecordCopied($table, $uid) && (int)$tcemainObj->copyMappingArray[$table][$uid] === (int)$WSversion['uid']) {
292 $this->moveRecord_processFields($tcemainObj, $resolvedPid, $table, $uid);
293 }
294 }
295 // Check workspace permissions:
296 $workspaceAccessBlocked = array();
297 // Element was in "New/Deleted/Moved" so it can be moved...
298 $recIsNewVersion = $moveRecVersionState->indicatesPlaceholder();
299 $destRes = $tcemainObj->BE_USER->workspaceAllowLiveRecordsInPID($resolvedPid, $table);
300 $canMoveRecord = ($recIsNewVersion || BackendUtility::isTableMovePlaceholderAware($table));
301 // Workspace source check:
302 if (!$recIsNewVersion) {
303 $errorCode = $tcemainObj->BE_USER->workspaceCannotEditRecord($table, $WSversion['uid'] ? $WSversion['uid'] : $uid);
304 if ($errorCode) {
305 $workspaceAccessBlocked['src1'] = 'Record could not be edited in workspace: ' . $errorCode . ' ';
306 } elseif (!$canMoveRecord && $tcemainObj->BE_USER->workspaceAllowLiveRecordsInPID($moveRec['pid'], $table) <= 0) {
307 $workspaceAccessBlocked['src2'] = 'Could not remove record from table "' . $table . '" from its page "' . $moveRec['pid'] . '" ';
308 }
309 }
310 // Workspace destination check:
311 // All records can be inserted if $destRes is greater than zero.
312 // Only new versions can be inserted if $destRes is FALSE.
313 // NO RECORDS can be inserted if $destRes is negative which indicates a stage
314 // not allowed for use. If "versioningWS" is version 2, moving can take place of versions.
315 if (!($destRes > 0 || $canMoveRecord && !$destRes)) {
316 $workspaceAccessBlocked['dest1'] = 'Could not insert record from table "' . $table . '" in destination PID "' . $resolvedPid . '" ';
317 } elseif ($destRes == 1 && $WSversion['uid']) {
318 $workspaceAccessBlocked['dest2'] = 'Could not insert other versions in destination PID ';
319 }
320 if (!count($workspaceAccessBlocked)) {
321 // If the move operation is done on a versioned record, which is
322 // NOT new/deleted placeholder and versioningWS is in version 2, then...
323 if ($WSversion['uid'] && !$recIsNewVersion && BackendUtility::isTableMovePlaceholderAware($table)) {
324 $this->moveRecord_wsPlaceholders($table, $uid, $destPid, $WSversion['uid'], $tcemainObj);
325 } else {
326 // moving not needed, just behave like in live workspace
327 $recordWasMoved = FALSE;
328 }
329 } else {
330 $tcemainObj->newlog('Move attempt failed due to workspace restrictions: ' . implode(' // ', $workspaceAccessBlocked), 1);
331 }
332 }
333 }
334
335 /**
336 * Processes fields of a moved record and follows references.
337 *
338 * @param DataHandler $dataHandler Calling DataHandler instance
339 * @param int $resolvedPageId Resolved real destination page id
340 * @param string $table Name of parent table
341 * @param int $uid UID of the parent record
342 * @return void
343 */
344 protected function moveRecord_processFields(DataHandler $dataHandler, $resolvedPageId, $table, $uid) {
345 $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $table, $uid);
346 if (empty($versionedRecord)) {
347 return;
348 }
349 foreach ($versionedRecord as $field => $value) {
350 if (empty($GLOBALS['TCA'][$table]['columns'][$field]['config'])) {
351 continue;
352 }
353 $this->moveRecord_processFieldValue(
354 $dataHandler, $resolvedPageId,
355 $table, $uid, $field, $value,
356 $GLOBALS['TCA'][$table]['columns'][$field]['config']
357 );
358 }
359 }
360
361 /**
362 * Processes a single field of a moved record and follows references.
363 *
364 * @param DataHandler $dataHandler Calling DataHandler instance
365 * @param int $resolvedPageId Resolved real destination page id
366 * @param string $table Name of parent table
367 * @param int $uid UID of the parent record
368 * @param string $field Name of the field of the parent record
369 * @param string $value Value of the field of the parent record
370 * @param array $configuration TCA field configuration of the parent record
371 * @return void
372 */
373 protected function moveRecord_processFieldValue(DataHandler $dataHandler, $resolvedPageId, $table, $uid, $field, $value, array $configuration) {
374 $inlineFieldType = $dataHandler->getInlineFieldType($configuration);
375 $inlineProcessing = (
376 ($inlineFieldType === 'list' || $inlineFieldType === 'field')
377 && BackendUtility::isTableMovePlaceholderAware($configuration['foreign_table'])
378 && (!isset($configuration['behaviour']['disableMovingChildrenWithParent']) || !$configuration['behaviour']['disableMovingChildrenWithParent'])
379 );
380
381 if ($inlineProcessing) {
382 if ($table === 'pages') {
383 // If the inline elements are related to a page record,
384 // make sure they reside at that page and not at its parent
385 $resolvedPageId = $uid;
386 }
387
388 $dbAnalysis = $this->createRelationHandlerInstance();
389 $dbAnalysis->start($value, $configuration['foreign_table'], '', $uid, $table, $configuration);
390
391 // Moving records to a positive destination will insert each
392 // record at the beginning, thus the order is reversed here:
393 foreach ($dbAnalysis->itemArray as $item) {
394 $versionedRecord = BackendUtility::getWorkspaceVersionOfRecord($dataHandler->BE_USER->workspace, $item['table'], $item['id'], 'uid,t3ver_state');
395 if (empty($versionedRecord) || VersionState::cast($versionedRecord['t3ver_state'])->indicatesPlaceholder()) {
396 continue;
397 }
398 $dataHandler->moveRecord($item['table'], $item['id'], $resolvedPageId);
399 }
400 }
401 }
402
403 /****************************
404 ***** Notifications ******
405 ****************************/
406 /**
407 * Send an email notification to users in workspace
408 *
409 * @param array $stat Workspace access array from \TYPO3\CMS\Core\Authentication\BackendUserAuthentication::checkWorkspace()
410 * @param int $stageId New Stage number: 0 = editing, 1= just ready for review, 10 = ready for publication, -1 = rejected!
411 * @param string $table Table name of element (or list of element names if $id is zero)
412 * @param int $id Record uid of element (if zero, then $table is used as reference to element(s) alone)
413 * @param string $comment User comment sent along with action
414 * @param DataHandler $tcemainObj TCEmain object
415 * @param array $notificationAlternativeRecipients List of recipients to notify instead of be_users selected by sys_workspace, list is generated by workspace extension module
416 * @return void
417 */
418 protected function notifyStageChange(array $stat, $stageId, $table, $id, $comment, DataHandler $tcemainObj, array $notificationAlternativeRecipients = array()) {
419 $workspaceRec = BackendUtility::getRecord('sys_workspace', $stat['uid']);
420 // So, if $id is not set, then $table is taken to be the complete element name!
421 $elementName = $id ? $table . ':' . $id : $table;
422 if (is_array($workspaceRec)) {
423 // Get the new stage title from workspaces library, if workspaces extension is installed
424 if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('workspaces')) {
425 $stageService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\StagesService::class);
426 $newStage = $stageService->getStageTitle((int)$stageId);
427 } else {
428 // @todo CONSTANTS SHOULD BE USED - tx_service_workspace_workspaces
429 // @todo use localized labels
430 // Compile label:
431 switch ((int)$stageId) {
432 case 1:
433 $newStage = 'Ready for review';
434 break;
435 case 10:
436 $newStage = 'Ready for publishing';
437 break;
438 case -1:
439 $newStage = 'Element was rejected!';
440 break;
441 case 0:
442 $newStage = 'Rejected element was noticed and edited';
443 break;
444 default:
445 $newStage = 'Unknown state change!?';
446 }
447 }
448 if (count($notificationAlternativeRecipients) == 0) {
449 // Compile list of recipients:
450 $emails = array();
451 switch ((int)$stat['stagechg_notification']) {
452 case 1:
453 switch ((int)$stageId) {
454 case 1:
455 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']);
456 break;
457 case 10:
458 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], TRUE);
459 break;
460 case -1:
461 // List of elements to reject:
462 $allElements = explode(',', $elementName);
463 // Traverse them, and find the history of each
464 foreach ($allElements as $elRef) {
465 list($eTable, $eUid) = explode(':', $elRef);
466 $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('log_data,tstamp,userid', 'sys_log', 'action=6 and details_nr=30
467 AND tablename=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($eTable, 'sys_log') . '
468 AND recuid=' . (int)$eUid, '', 'uid DESC');
469 // Find all implicated since the last stage-raise from editing to review:
470 foreach ($rows as $dat) {
471 $data = unserialize($dat['log_data']);
472 $emails = $this->getEmailsForStageChangeNotification($dat['userid'], TRUE) + $emails;
473 if ($data['stage'] == 1) {
474 break;
475 }
476 }
477 }
478 break;
479 case 0:
480 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']);
481 break;
482 default:
483 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], TRUE);
484 }
485 break;
486 case 10:
487 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], TRUE);
488 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']) + $emails;
489 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']) + $emails;
490 break;
491 default:
492 // Do nothing
493 }
494 } else {
495 $emails = $notificationAlternativeRecipients;
496 }
497 // prepare and then send the emails
498 if (count($emails)) {
499 // Path to record is found:
500 list($elementTable, $elementUid) = explode(':', $elementName);
501 $elementUid = (int)$elementUid;
502 $elementRecord = BackendUtility::getRecord($elementTable, $elementUid);
503 $recordTitle = BackendUtility::getRecordTitle($elementTable, $elementRecord);
504 if ($elementTable == 'pages') {
505 $pageUid = $elementUid;
506 } else {
507 BackendUtility::fixVersioningPid($elementTable, $elementRecord);
508 $pageUid = ($elementUid = $elementRecord['pid']);
509 }
510 // fetch the TSconfig settings for the email
511 // old way, options are TCEMAIN.notificationEmail_body/subject
512 $TCEmainTSConfig = $tcemainObj->getTCEMAIN_TSconfig($pageUid);
513 // new way, options are
514 // pageTSconfig: tx_version.workspaces.stageNotificationEmail.subject
515 // userTSconfig: page.tx_version.workspaces.stageNotificationEmail.subject
516 $pageTsConfig = BackendUtility::getPagesTSconfig($pageUid);
517 $emailConfig = $pageTsConfig['tx_version.']['workspaces.']['stageNotificationEmail.'];
518 $markers = array(
519 '###RECORD_TITLE###' => $recordTitle,
520 '###RECORD_PATH###' => BackendUtility::getRecordPath($elementUid, '', 20),
521 '###SITE_NAME###' => $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
522 '###SITE_URL###' => GeneralUtility::getIndpEnv('TYPO3_SITE_URL') . TYPO3_mainDir,
523 '###WORKSPACE_TITLE###' => $workspaceRec['title'],
524 '###WORKSPACE_UID###' => $workspaceRec['uid'],
525 '###ELEMENT_NAME###' => $elementName,
526 '###NEXT_STAGE###' => $newStage,
527 '###COMMENT###' => $comment,
528 // See: #30212 - keep both markers for compatibility
529 '###USER_REALNAME###' => $tcemainObj->BE_USER->user['realName'],
530 '###USER_FULLNAME###' => $tcemainObj->BE_USER->user['realName'],
531 '###USER_USERNAME###' => $tcemainObj->BE_USER->user['username']
532 );
533 // add marker for preview links if workspace extension is loaded
534 if (\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('workspaces')) {
535 $this->workspaceService = GeneralUtility::makeInstance(\TYPO3\CMS\Workspaces\Service\WorkspaceService::class);
536 // only generate the link if the marker is in the template - prevents database from getting to much entries
537 if (GeneralUtility::isFirstPartOfStr($emailConfig['message'], 'LLL:')) {
538 $tempEmailMessage = $GLOBALS['LANG']->sL($emailConfig['message']);
539 } else {
540 $tempEmailMessage = $emailConfig['message'];
541 }
542 if (strpos($tempEmailMessage, '###PREVIEW_LINK###') !== FALSE) {
543 $markers['###PREVIEW_LINK###'] = $this->workspaceService->generateWorkspacePreviewLink($elementUid);
544 }
545 unset($tempEmailMessage);
546 $markers['###SPLITTED_PREVIEW_LINK###'] = $this->workspaceService->generateWorkspaceSplittedPreviewLink($elementUid, TRUE);
547 }
548 // Hook for preprocessing of the content for formmails:
549 if (is_array($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'])) {
550 foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/version/class.tx_version_tcemain.php']['notifyStageChange-postModifyMarkers'] as $_classRef) {
551 $_procObj =& GeneralUtility::getUserObj($_classRef);
552 $markers = $_procObj->postModifyMarkers($markers, $this);
553 }
554 }
555 // send an email to each individual user, to ensure the
556 // multilanguage version of the email
557 $emailRecipients = array();
558 // an array of language objects that are needed
559 // for emails with different languages
560 $languageObjects = array(
561 $GLOBALS['LANG']->lang => $GLOBALS['LANG']
562 );
563 // loop through each recipient and send the email
564 foreach ($emails as $recipientData) {
565 // don't send an email twice
566 if (isset($emailRecipients[$recipientData['email']])) {
567 continue;
568 }
569 $emailSubject = $emailConfig['subject'];
570 $emailMessage = $emailConfig['message'];
571 $emailRecipients[$recipientData['email']] = $recipientData['email'];
572 // check if the email needs to be localized
573 // in the users' language
574 if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:') || GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) {
575 $recipientLanguage = $recipientData['lang'] ? $recipientData['lang'] : 'default';
576 if (!isset($languageObjects[$recipientLanguage])) {
577 // a LANG object in this language hasn't been
578 // instantiated yet, so this is done here
579 /** @var $languageObject \TYPO3\CMS\Lang\LanguageService */
580 $languageObject = GeneralUtility::makeInstance(\TYPO3\CMS\Lang\LanguageService::class);
581 $languageObject->init($recipientLanguage);
582 $languageObjects[$recipientLanguage] = $languageObject;
583 } else {
584 $languageObject = $languageObjects[$recipientLanguage];
585 }
586 if (GeneralUtility::isFirstPartOfStr($emailSubject, 'LLL:')) {
587 $emailSubject = $languageObject->sL($emailSubject);
588 }
589 if (GeneralUtility::isFirstPartOfStr($emailMessage, 'LLL:')) {
590 $emailMessage = $languageObject->sL($emailMessage);
591 }
592 }
593 $emailSubject = \TYPO3\CMS\Core\Html\HtmlParser::substituteMarkerArray($emailSubject, $markers, '', TRUE, TRUE);
594 $emailMessage = \TYPO3\CMS\Core\Html\HtmlParser::substituteMarkerArray($emailMessage, $markers, '', TRUE, TRUE);
595 // Send an email to the recipient
596 /** @var $mail \TYPO3\CMS\Core\Mail\MailMessage */
597 $mail = GeneralUtility::makeInstance(\TYPO3\CMS\Core\Mail\MailMessage::class);
598 if (!empty($recipientData['realName'])) {
599 $recipient = array($recipientData['email'] => $recipientData['realName']);
600 } else {
601 $recipient = $recipientData['email'];
602 }
603 $mail->setTo($recipient)
604 ->setSubject($emailSubject)
605 ->setFrom(\TYPO3\CMS\Core\Utility\MailUtility::getSystemFrom())
606 ->setBody($emailMessage);
607 $mail->send();
608 }
609 $emailRecipients = implode(',', $emailRecipients);
610 $tcemainObj->newlog2('Notification email for stage change was sent to "' . $emailRecipients . '"', $table, $id);
611 }
612 }
613 }
614
615 /**
616 * Return be_users that should be notified on stage change from input list.
617 * previously called notifyStageChange_getEmails() in tcemain
618 *
619 * @param string $listOfUsers List of backend users, on the form "be_users_10,be_users_2" or "10,2" in case noTablePrefix is set.
620 * @param bool $noTablePrefix If set, the input list are integers and not strings.
621 * @return array Array of emails
622 */
623 protected function getEmailsForStageChangeNotification($listOfUsers, $noTablePrefix = FALSE) {
624 $users = GeneralUtility::trimExplode(',', $listOfUsers, TRUE);
625 $emails = array();
626 foreach ($users as $userIdent) {
627 if ($noTablePrefix) {
628 $id = (int)$userIdent;
629 } else {
630 list($table, $id) = GeneralUtility::revExplode('_', $userIdent, 2);
631 }
632 if ($table === 'be_users' || $noTablePrefix) {
633 if ($userRecord = BackendUtility::getRecord('be_users', $id, 'uid,email,lang,realName', BackendUtility::BEenableFields('be_users'))) {
634 if (trim($userRecord['email']) !== '') {
635 $emails[$id] = $userRecord;
636 }
637 }
638 }
639 }
640 return $emails;
641 }
642
643 /****************************
644 ***** Stage Changes ******
645 ****************************/
646 /**
647 * Setting stage of record
648 *
649 * @param string $table Table name
650 * @param int $integer Record UID
651 * @param int $stageId Stage ID to set
652 * @param string $comment Comment that goes into log
653 * @param bool $notificationEmailInfo Accumulate state changes in memory for compiled notification email?
654 * @param DataHandler $tcemainObj TCEmain object
655 * @param array $notificationAlternativeRecipients comma separated list of recipients to notify instead of normal be_users
656 * @return void
657 */
658 protected function version_setStage($table, $id, $stageId, $comment = '', $notificationEmailInfo = FALSE, DataHandler $tcemainObj, array $notificationAlternativeRecipients = array()) {
659 if ($errorCode = $tcemainObj->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) {
660 $tcemainObj->newlog('Attempt to set stage for record failed: ' . $errorCode, 1);
661 } elseif ($tcemainObj->checkRecordUpdateAccess($table, $id)) {
662 $record = BackendUtility::getRecord($table, $id);
663 $stat = $tcemainObj->BE_USER->checkWorkspace($record['t3ver_wsid']);
664 // check if the usere is allowed to the current stage, so it's also allowed to send to next stage
665 if ($GLOBALS['BE_USER']->workspaceCheckStageForCurrent($record['t3ver_stage'])) {
666 // Set stage of record:
667 $updateData = array(
668 't3ver_stage' => $stageId
669 );
670 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$id, $updateData);
671 $tcemainObj->newlog2('Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', $table, $id);
672 // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere!
673 $tcemainObj->log($table, $id, 6, 0, 0, 'Stage raised...', 30, array('comment' => $comment, 'stage' => $stageId));
674 if ((int)$stat['stagechg_notification'] > 0) {
675 if ($notificationEmailInfo) {
676 $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['shared'] = array($stat, $stageId, $comment);
677 $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = $table . ':' . $id;
678 $this->notificationEmailInfo[$stat['uid'] . ':' . $stageId . ':' . $comment]['alternativeRecipients'] = $notificationAlternativeRecipients;
679 } else {
680 $this->notifyStageChange($stat, $stageId, $table, $id, $comment, $tcemainObj, $notificationAlternativeRecipients);
681 }
682 }
683 } else {
684 $tcemainObj->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', 1);
685 }
686 } else {
687 $tcemainObj->newlog('Attempt to set stage for record failed because you do not have edit access', 1);
688 }
689 }
690
691 /*****************************
692 ***** CMD versioning ******
693 *****************************/
694 /**
695 * Creates a new version of a page including content and possible subpages.
696 *
697 * @param int $uid Page uid to create new version of.
698 * @param string $label Version label
699 * @param int $versionizeTree Indicating "treeLevel" - "page" (0) or "branch" (>=1) ["element" type must call versionizeRecord() directly]
700 * @param DataHandler $tcemainObj TCEmain object
701 * @return void
702 * @see copyPages()
703 */
704 protected function versionizePages($uid, $label, $versionizeTree, DataHandler $tcemainObj) {
705 $uid = (int)$uid;
706 // returns the branch
707 $brExist = $tcemainObj->doesBranchExist('', $uid, $tcemainObj->pMap['show'], 1);
708 // Checks if we had permissions
709 if ($brExist != -1) {
710 // Make list of tables that should come along with a new version of the page:
711 $verTablesArray = array();
712 $allTables = array_keys($GLOBALS['TCA']);
713 foreach ($allTables as $tableName) {
714 if ($tableName != 'pages' && ($versionizeTree > 0 || $GLOBALS['TCA'][$tableName]['ctrl']['versioning_followPages'])) {
715 $verTablesArray[] = $tableName;
716 }
717 }
718 // Remove the possible inline child tables from the tables to be versioniozed automatically:
719 $verTablesArray = array_diff($verTablesArray, $this->getPossibleInlineChildTablesOfParentTable('pages'));
720 // Begin to copy pages if we're allowed to:
721 if ($versionizeTree === -1) {
722 // Versionize this page:
723 $theNewRootID = $tcemainObj->versionizeRecord('pages', $uid, $label, FALSE, $versionizeTree);
724 if ($theNewRootID) {
725 $this->rawCopyPageContent($uid, $theNewRootID, $verTablesArray, $tcemainObj);
726 // If we're going to copy recursively...:
727 if ($versionizeTree > 0) {
728 // Get ALL subpages to copy (read permissions respected - they should NOT be...):
729 $CPtable = $tcemainObj->int_pageTreeInfo(array(), $uid, (int)$versionizeTree, $theNewRootID);
730 // Now copying the subpages
731 foreach ($CPtable as $thePageUid => $thePagePid) {
732 $newPid = $tcemainObj->copyMappingArray['pages'][$thePagePid];
733 if (isset($newPid)) {
734 $theNewRootID = $tcemainObj->copyRecord_raw('pages', $thePageUid, $newPid);
735 $this->rawCopyPageContent($thePageUid, $theNewRootID, $verTablesArray, $tcemainObj);
736 } else {
737 $tcemainObj->newlog('Something went wrong during copying branch (for versioning)', 1);
738 break;
739 }
740 }
741 }
742 } else {
743 $tcemainObj->newlog('The root version could not be created!', 1);
744 }
745 } else {
746 $tcemainObj->newlog('Versioning type "' . $versionizeTree . '" was not allowed in workspace', 1);
747 }
748 } else {
749 $tcemainObj->newlog('Could not read all subpages to versionize.', 1);
750 }
751 }
752
753 /**
754 * Swapping versions of a record
755 * 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
756 *
757 * @param string $table Table name
758 * @param int $id UID of the online record to swap
759 * @param int $swapWith UID of the archived version to swap with!
760 * @param bool $swapIntoWS If set, swaps online into workspace instead of publishing out of workspace.
761 * @param DataHandler $tcemainObj TCEmain object
762 * @param string $comment Notification comment
763 * @param bool $notificationEmailInfo Accumulate state changes in memory for compiled notification email?
764 * @param array $notificationAlternativeRecipients comma separated list of recipients to notificate instead of normal be_users
765 * @return void
766 */
767 protected function version_swap($table, $id, $swapWith, $swapIntoWS = 0, DataHandler $tcemainObj, $comment = '', $notificationEmailInfo = FALSE, $notificationAlternativeRecipients = array()) {
768 // First, check if we may actually edit the online record
769 if ($tcemainObj->checkRecordUpdateAccess($table, $id)) {
770 // Select the two versions:
771 $curVersion = BackendUtility::getRecord($table, $id, '*');
772 $swapVersion = BackendUtility::getRecord($table, $swapWith, '*');
773 $movePlh = array();
774 $movePlhID = 0;
775 if (is_array($curVersion) && is_array($swapVersion)) {
776 if ($tcemainObj->BE_USER->workspacePublishAccess($swapVersion['t3ver_wsid'])) {
777 $wsAccess = $tcemainObj->BE_USER->checkWorkspace($swapVersion['t3ver_wsid']);
778 if ($swapVersion['t3ver_wsid'] <= 0 || !($wsAccess['publish_access'] & 1) || (int)$swapVersion['t3ver_stage'] === -10) {
779 if ($tcemainObj->doesRecordExist($table, $swapWith, 'show') && $tcemainObj->checkRecordUpdateAccess($table, $swapWith)) {
780 if (!$swapIntoWS || $tcemainObj->BE_USER->workspaceSwapAccess()) {
781 // Check if the swapWith record really IS a version of the original!
782 if (((int)$swapVersion['pid'] == -1 && (int)$curVersion['pid'] >= 0) && (int)$swapVersion['t3ver_oid'] === (int)$id) {
783 // Lock file name:
784 $lockFileName = PATH_site . 'typo3temp/swap_locking/' . $table . ':' . $id . '.ser';
785 if (!@is_file($lockFileName)) {
786 // Write lock-file:
787 GeneralUtility::writeFileToTypo3tempDir($lockFileName, serialize(array(
788 'tstamp' => $GLOBALS['EXEC_TIME'],
789 'user' => $tcemainObj->BE_USER->user['username'],
790 'curVersion' => $curVersion,
791 'swapVersion' => $swapVersion
792 )));
793 // Find fields to keep
794 $keepFields = $this->getUniqueFields($table);
795 if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
796 $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['sortby'];
797 }
798 // l10n-fields must be kept otherwise the localization
799 // will be lost during the publishing
800 if (!isset($GLOBALS['TCA'][$table]['ctrl']['transOrigPointerTable']) && $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']) {
801 $keepFields[] = $GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField'];
802 }
803 // Swap "keepfields"
804 foreach ($keepFields as $fN) {
805 $tmp = $swapVersion[$fN];
806 $swapVersion[$fN] = $curVersion[$fN];
807 $curVersion[$fN] = $tmp;
808 }
809 // Preserve states:
810 $t3ver_state = array();
811 $t3ver_state['swapVersion'] = $swapVersion['t3ver_state'];
812 $t3ver_state['curVersion'] = $curVersion['t3ver_state'];
813 // Modify offline version to become online:
814 $tmp_wsid = $swapVersion['t3ver_wsid'];
815 // Set pid for ONLINE
816 $swapVersion['pid'] = (int)$curVersion['pid'];
817 // We clear this because t3ver_oid only make sense for offline versions
818 // and we want to prevent unintentional misuse of this
819 // value for online records.
820 $swapVersion['t3ver_oid'] = 0;
821 // In case of swapping and the offline record has a state
822 // (like 2 or 4 for deleting or move-pointer) we set the
823 // current workspace ID so the record is not deselected
824 // in the interface by BackendUtility::versioningPlaceholderClause()
825 $swapVersion['t3ver_wsid'] = 0;
826 if ($swapIntoWS) {
827 if ($t3ver_state['swapVersion'] > 0) {
828 $swapVersion['t3ver_wsid'] = $tcemainObj->BE_USER->workspace;
829 } else {
830 $swapVersion['t3ver_wsid'] = (int)$curVersion['t3ver_wsid'];
831 }
832 }
833 $swapVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
834 $swapVersion['t3ver_stage'] = 0;
835 if (!$swapIntoWS) {
836 $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
837 }
838 // Moving element.
839 if ((int)$GLOBALS['TCA'][$table]['ctrl']['versioningWS'] >= 2) {
840 // && $t3ver_state['swapVersion']==4 // Maybe we don't need this?
841 if ($plhRec = BackendUtility::getMovePlaceholder($table, $id, 't3ver_state,pid,uid' . ($GLOBALS['TCA'][$table]['ctrl']['sortby'] ? ',' . $GLOBALS['TCA'][$table]['ctrl']['sortby'] : ''))) {
842 $movePlhID = $plhRec['uid'];
843 $movePlh['pid'] = $swapVersion['pid'];
844 $swapVersion['pid'] = (int)$plhRec['pid'];
845 $curVersion['t3ver_state'] = (int)$swapVersion['t3ver_state'];
846 $swapVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
847 if ($GLOBALS['TCA'][$table]['ctrl']['sortby']) {
848 // sortby is a "keepFields" which is why this will work...
849 $movePlh[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
850 $swapVersion[$GLOBALS['TCA'][$table]['ctrl']['sortby']] = $plhRec[$GLOBALS['TCA'][$table]['ctrl']['sortby']];
851 }
852 }
853 }
854 unset($swapVersion['uid']);
855 // Modify online version to become offline:
856 unset($curVersion['uid']);
857 // Set pid for OFFLINE
858 $curVersion['pid'] = -1;
859 $curVersion['t3ver_oid'] = (int)$id;
860 $curVersion['t3ver_wsid'] = $swapIntoWS ? (int)$tmp_wsid : 0;
861 $curVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
862 $curVersion['t3ver_count'] = $curVersion['t3ver_count'] + 1;
863 // Increment lifecycle counter
864 $curVersion['t3ver_stage'] = 0;
865 if (!$swapIntoWS) {
866 $curVersion['t3ver_state'] = (string)new VersionState(VersionState::DEFAULT_STATE);
867 }
868 // Registering and swapping MM relations in current and swap records:
869 $tcemainObj->version_remapMMForVersionSwap($table, $id, $swapWith);
870 // Generating proper history data to prepare logging
871 $tcemainObj->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion);
872 $tcemainObj->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion);
873 // Execute swapping:
874 $sqlErrors = array();
875 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$id, $swapVersion);
876 if ($GLOBALS['TYPO3_DB']->sql_error()) {
877 $sqlErrors[] = $GLOBALS['TYPO3_DB']->sql_error();
878 } else {
879 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$swapWith, $curVersion);
880 if ($GLOBALS['TYPO3_DB']->sql_error()) {
881 $sqlErrors[] = $GLOBALS['TYPO3_DB']->sql_error();
882 } else {
883 unlink($lockFileName);
884 }
885 }
886 if (!count($sqlErrors)) {
887 // Register swapped ids for later remapping:
888 $this->remappedIds[$table][$id] = $swapWith;
889 $this->remappedIds[$table][$swapWith] = $id;
890 // If a moving operation took place...:
891 if ($movePlhID) {
892 // Remove, if normal publishing:
893 if (!$swapIntoWS) {
894 // For delete + completely delete!
895 $tcemainObj->deleteEl($table, $movePlhID, TRUE, TRUE);
896 } else {
897 // Otherwise update the movePlaceholder:
898 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$movePlhID, $movePlh);
899 $tcemainObj->addRemapStackRefIndex($table, $movePlhID);
900 }
901 }
902 // Checking for delete:
903 // Delete only if new/deleted placeholders are there.
904 if (!$swapIntoWS && ((int)$t3ver_state['swapVersion'] === 1 || (int)$t3ver_state['swapVersion'] === 2)) {
905 // Force delete
906 $tcemainObj->deleteEl($table, $id, TRUE);
907 }
908 $tcemainObj->newlog2(($swapIntoWS ? 'Swapping' : 'Publishing') . ' successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, $table, $id, $swapVersion['pid']);
909 // Update reference index of the live record:
910 $tcemainObj->addRemapStackRefIndex($table, $id);
911 // Set log entry for live record:
912 $propArr = $tcemainObj->getRecordPropertiesFromRow($table, $swapVersion);
913 if ($propArr['_ORIG_pid'] == -1) {
914 $label = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_tcemain.xlf:version_swap.offline_record_updated');
915 } else {
916 $label = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_tcemain.xlf:version_swap.online_record_updated');
917 }
918 $theLogId = $tcemainObj->log($table, $id, 2, $propArr['pid'], 0, $label, 10, array($propArr['header'], $table . ':' . $id), $propArr['event_pid']);
919 $tcemainObj->setHistory($table, $id, $theLogId);
920 // Update reference index of the offline record:
921 $tcemainObj->addRemapStackRefIndex($table, $swapWith);
922 // Set log entry for offline record:
923 $propArr = $tcemainObj->getRecordPropertiesFromRow($table, $curVersion);
924 if ($propArr['_ORIG_pid'] == -1) {
925 $label = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_tcemain.xlf:version_swap.offline_record_updated');
926 } else {
927 $label = $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_tcemain.xlf:version_swap.online_record_updated');
928 }
929 $theLogId = $tcemainObj->log($table, $swapWith, 2, $propArr['pid'], 0, $label, 10, array($propArr['header'], $table . ':' . $swapWith), $propArr['event_pid']);
930 $tcemainObj->setHistory($table, $swapWith, $theLogId);
931
932 $stageId = -20; // \TYPO3\CMS\Workspaces\Service\StagesService::STAGE_PUBLISH_EXECUTE_ID;
933 if ($notificationEmailInfo) {
934 $notificationEmailInfoKey = $wsAccess['uid'] . ':' . $stageId . ':' . $comment;
935 $this->notificationEmailInfo[$notificationEmailInfoKey]['shared'] = array($wsAccess, $stageId, $comment);
936 $this->notificationEmailInfo[$notificationEmailInfoKey]['elements'][] = $table . ':' . $id;
937 $this->notificationEmailInfo[$notificationEmailInfoKey]['alternativeRecipients'] = $notificationAlternativeRecipients;
938 } else {
939 $this->notifyStageChange($wsAccess, $stageId, $table, $id, $comment, $tcemainObj, $notificationAlternativeRecipients);
940 }
941 // Write to log with stageId -20
942 $tcemainObj->newlog2('Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', $table, $id);
943 $tcemainObj->log($table, $id, 6, 0, 0, 'Published', 30, array('comment' => $comment, 'stage' => $stageId));
944
945 // Clear cache:
946 $tcemainObj->registerRecordIdForPageCacheClearing($table, $id);
947 // Checking for "new-placeholder" and if found, delete it (BUT FIRST after swapping!):
948 if (!$swapIntoWS && $t3ver_state['curVersion'] > 0) {
949 // For delete + completely delete!
950 $tcemainObj->deleteEl($table, $swapWith, TRUE, TRUE);
951 }
952
953 //Update reference index for live workspace too:
954 /** @var $refIndexObj \TYPO3\CMS\Core\Database\ReferenceIndex */
955 $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
956 $refIndexObj->setWorkspaceId(0);
957 $refIndexObj->updateRefIndexTable($table, $id);
958 $refIndexObj->updateRefIndexTable($table, $swapWith);
959 } else {
960 $tcemainObj->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), 2);
961 }
962 } else {
963 $tcemainObj->newlog('A swapping lock file was present. Either another swap process is already running or a previous swap process failed. Ask your administrator to handle the situation.', 2);
964 }
965 } else {
966 $tcemainObj->newlog('In swap version, either pid was not -1 or the t3ver_oid didn\'t match the id of the online version as it must!', 2);
967 }
968 } else {
969 $tcemainObj->newlog('Workspace #' . $swapVersion['t3ver_wsid'] . ' does not support swapping.', 1);
970 }
971 } else {
972 $tcemainObj->newlog('You cannot publish a record you do not have edit and show permissions for', 1);
973 }
974 } else {
975 $tcemainObj->newlog('Records in workspace #' . $swapVersion['t3ver_wsid'] . ' can only be published when in "Publish" stage.', 1);
976 }
977 } else {
978 $tcemainObj->newlog('User could not publish records from workspace #' . $swapVersion['t3ver_wsid'], 1);
979 }
980 } else {
981 $tcemainObj->newlog('Error: Either online or swap version could not be selected!', 2);
982 }
983 } else {
984 $tcemainObj->newlog('Error: You cannot swap versions for a record you do not have access to edit!', 1);
985 }
986 }
987
988 /**
989 * Writes remapped foreign field (IRRE).
990 *
991 * @param \TYPO3\CMS\Core\Database\RelationHandler $dbAnalysis Instance that holds the sorting order of child records
992 * @param array $configuration The TCA field configuration
993 * @param int $parentId The uid of the parent record
994 * @return void
995 */
996 public function writeRemappedForeignField(\TYPO3\CMS\Core\Database\RelationHandler $dbAnalysis, array $configuration, $parentId) {
997 foreach ($dbAnalysis->itemArray as &$item) {
998 if (isset($this->remappedIds[$item['table']][$item['id']])) {
999 $item['id'] = $this->remappedIds[$item['table']][$item['id']];
1000 }
1001 }
1002 $dbAnalysis->writeForeignField($configuration, $parentId);
1003 }
1004
1005 /**
1006 * Release version from this workspace (and into "Live" workspace but as an offline version).
1007 *
1008 * @param string $table Table name
1009 * @param int $id Record UID
1010 * @param bool $flush If set, will completely delete element
1011 * @param DataHandler $tcemainObj TCEmain object
1012 * @return void
1013 */
1014 protected function version_clearWSID($table, $id, $flush = FALSE, DataHandler $tcemainObj) {
1015 if ($errorCode = $tcemainObj->BE_USER->workspaceCannotEditOfflineVersion($table, $id)) {
1016 $tcemainObj->newlog('Attempt to reset workspace for record failed: ' . $errorCode, 1);
1017 } elseif ($tcemainObj->checkRecordUpdateAccess($table, $id)) {
1018 if ($liveRec = BackendUtility::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state')) {
1019 // Clear workspace ID:
1020 $updateData = array(
1021 't3ver_wsid' => 0,
1022 't3ver_tstamp' => $GLOBALS['EXEC_TIME']
1023 );
1024 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$id, $updateData);
1025 // Clear workspace ID for live version AND DELETE IT as well because it is a new record!
1026 if (
1027 VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER)
1028 || VersionState::cast($liveRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)
1029 ) {
1030 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$liveRec['uid'], $updateData);
1031 // THIS assumes that the record was placeholder ONLY for ONE record (namely $id)
1032 $tcemainObj->deleteEl($table, $liveRec['uid'], TRUE);
1033 }
1034 // If "deleted" flag is set for the version that got released
1035 // it doesn't make sense to keep that "placeholder" anymore and we delete it completly.
1036 $wsRec = BackendUtility::getRecord($table, $id);
1037 if (
1038 $flush
1039 || (
1040 VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::NEW_PLACEHOLDER)
1041 || VersionState::cast($wsRec['t3ver_state'])->equals(VersionState::DELETE_PLACEHOLDER)
1042 )
1043 ) {
1044 $tcemainObj->deleteEl($table, $id, TRUE, TRUE);
1045 }
1046 // Remove the move-placeholder if found for live record.
1047 if ((int)$GLOBALS['TCA'][$table]['ctrl']['versioningWS'] >= 2) {
1048 if ($plhRec = BackendUtility::getMovePlaceholder($table, $liveRec['uid'], 'uid')) {
1049 $tcemainObj->deleteEl($table, $plhRec['uid'], TRUE, TRUE);
1050 }
1051 }
1052 }
1053 } else {
1054 $tcemainObj->newlog('Attempt to reset workspace for record failed because you do not have edit access', 1);
1055 }
1056 }
1057
1058 /*******************************
1059 ***** helper functions ******
1060 *******************************/
1061 /**
1062 * Copies all records from tables in $copyTablesArray from page with $old_pid to page with $new_pid
1063 * Uses raw-copy for the operation (meant for versioning!)
1064 *
1065 * @param int $oldPageId Current page id.
1066 * @param int $newPageId New page id
1067 * @param array $copyTablesArray Array of tables from which to copy
1068 * @param DataHandler $tcemainObj TCEmain object
1069 * @return void
1070 * @see versionizePages()
1071 */
1072 protected function rawCopyPageContent($oldPageId, $newPageId, array $copyTablesArray, DataHandler $tcemainObj) {
1073 if ($newPageId) {
1074 foreach ($copyTablesArray as $table) {
1075 // all records under the page is copied.
1076 if ($table && is_array($GLOBALS['TCA'][$table]) && $table !== 'pages') {
1077 $mres = $GLOBALS['TYPO3_DB']->exec_SELECTquery('uid', $table, 'pid=' . (int)$oldPageId . $tcemainObj->deleteClause($table));
1078 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($mres)) {
1079 // Check, if this record has already been copied by a parent record as relation:
1080 if (!$tcemainObj->copyMappingArray[$table][$row['uid']]) {
1081 // Copying each of the underlying records (method RAW)
1082 $tcemainObj->copyRecord_raw($table, $row['uid'], $newPageId);
1083 }
1084 }
1085 $GLOBALS['TYPO3_DB']->sql_free_result($mres);
1086 }
1087 }
1088 }
1089 }
1090
1091 /**
1092 * Finds all elements for swapping versions in workspace
1093 *
1094 * @param string $table Table name of the original element to swap
1095 * @param int $id UID of the original element to swap (online)
1096 * @param int $offlineId As above but offline
1097 * @return array Element data. Key is table name, values are array with first element as online UID, second - offline UID
1098 */
1099 public function findPageElementsForVersionSwap($table, $id, $offlineId) {
1100 $rec = BackendUtility::getRecord($table, $offlineId, 't3ver_wsid');
1101 $workspaceId = $rec['t3ver_wsid'];
1102 $elementData = array();
1103 if ($workspaceId != 0) {
1104 // Get page UID for LIVE and workspace
1105 if ($table != 'pages') {
1106 $rec = BackendUtility::getRecord($table, $id, 'pid');
1107 $pageId = $rec['pid'];
1108 $rec = BackendUtility::getRecord('pages', $pageId);
1109 BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1110 $offlinePageId = $rec['_ORIG_uid'];
1111 } else {
1112 $pageId = $id;
1113 $offlinePageId = $offlineId;
1114 }
1115 // Traversing all tables supporting versioning:
1116 foreach ($GLOBALS['TCA'] as $table => $cfg) {
1117 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $table !== 'pages') {
1118 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('A.uid AS offlineUid, B.uid AS uid', $table . ' A,' . $table . ' B', 'A.pid=-1 AND B.pid=' . $pageId . ' AND A.t3ver_wsid=' . $workspaceId . ' AND B.uid=A.t3ver_oid' . BackendUtility::deleteClause($table, 'A') . BackendUtility::deleteClause($table, 'B'));
1119 while (FALSE != ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
1120 $elementData[$table][] = array($row[1], $row[0]);
1121 }
1122 $GLOBALS['TYPO3_DB']->sql_free_result($res);
1123 }
1124 }
1125 if ($offlinePageId && $offlinePageId != $pageId) {
1126 $elementData['pages'][] = array($pageId, $offlinePageId);
1127 }
1128 }
1129 return $elementData;
1130 }
1131
1132 /**
1133 * Searches for all elements from all tables on the given pages in the same workspace.
1134 *
1135 * @param array $pageIdList List of PIDs to search
1136 * @param int $workspaceId Workspace ID
1137 * @param array $elementList List of found elements. Key is table name, value is array of element UIDs
1138 * @return void
1139 */
1140 public function findPageElementsForVersionStageChange(array $pageIdList, $workspaceId, array &$elementList) {
1141 if ($workspaceId != 0) {
1142 // Traversing all tables supporting versioning:
1143 foreach ($GLOBALS['TCA'] as $table => $cfg) {
1144 if ($GLOBALS['TCA'][$table]['ctrl']['versioningWS'] && $table !== 'pages') {
1145 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('DISTINCT A.uid', $table . ' A,' . $table . ' B', 'A.pid=-1' . ' AND A.t3ver_wsid=' . $workspaceId . ' AND B.pid IN (' . implode(',', $pageIdList) . ') AND A.t3ver_oid=B.uid' . BackendUtility::deleteClause($table, 'A') . BackendUtility::deleteClause($table, 'B'));
1146 while (FALSE !== ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
1147 $elementList[$table][] = $row[0];
1148 }
1149 $GLOBALS['TYPO3_DB']->sql_free_result($res);
1150 if (is_array($elementList[$table])) {
1151 // Yes, it is possible to get non-unique array even with DISTINCT above!
1152 // It happens because several UIDs are passed in the array already.
1153 $elementList[$table] = array_unique($elementList[$table]);
1154 }
1155 }
1156 }
1157 }
1158 }
1159
1160 /**
1161 * Finds page UIDs for the element from table <code>$table</code> with UIDs from <code>$idList</code>
1162 *
1163 * @param string $table Table to search
1164 * @param array $idList List of records' UIDs
1165 * @param int $workspaceId Workspace ID. We need this parameter because user can be in LIVE but he still can publisg DRAFT from ws module!
1166 * @param array $pageIdList List of found page UIDs
1167 * @param array $elementList List of found element UIDs. Key is table name, value is list of UIDs
1168 * @return void
1169 */
1170 public function findPageIdsForVersionStateChange($table, array $idList, $workspaceId, array &$pageIdList, array &$elementList) {
1171 if ($workspaceId != 0) {
1172 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('DISTINCT B.pid', $table . ' A,' . $table . ' B', 'A.pid=-1' . ' AND A.t3ver_wsid=' . $workspaceId . ' AND A.uid IN (' . implode(',', $idList) . ') AND A.t3ver_oid=B.uid' . BackendUtility::deleteClause($table, 'A') . BackendUtility::deleteClause($table, 'B'));
1173 while (FALSE !== ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
1174 $pageIdList[] = $row[0];
1175 // Find ws version
1176 // Note: cannot use BackendUtility::getRecordWSOL()
1177 // here because it does not accept workspace id!
1178 $rec = BackendUtility::getRecord('pages', $row[0]);
1179 BackendUtility::workspaceOL('pages', $rec, $workspaceId);
1180 if ($rec['_ORIG_uid']) {
1181 $elementList['pages'][$row[0]] = $rec['_ORIG_uid'];
1182 }
1183 }
1184 $GLOBALS['TYPO3_DB']->sql_free_result($res);
1185 // The line below is necessary even with DISTINCT
1186 // because several elements can be passed by caller
1187 $pageIdList = array_unique($pageIdList);
1188 }
1189 }
1190
1191 /**
1192 * Finds real page IDs for state change.
1193 *
1194 * @param array $idList List of page UIDs, possibly versioned
1195 * @return void
1196 */
1197 public function findRealPageIds(array &$idList) {
1198 foreach ($idList as $key => $id) {
1199 $rec = BackendUtility::getRecord('pages', $id, 't3ver_oid');
1200 if ($rec['t3ver_oid'] > 0) {
1201 $idList[$key] = $rec['t3ver_oid'];
1202 }
1203 }
1204 }
1205
1206 /**
1207 * Creates a move placeholder for workspaces.
1208 * USE ONLY INTERNALLY
1209 * Moving placeholder: Can be done because the system sees it as a placeholder for NEW elements like t3ver_state=VersionState::NEW_PLACEHOLDER
1210 * Moving original: Will either create the placeholder if it doesn't exist or move existing placeholder in workspace.
1211 *
1212 * @param string $table Table name to move
1213 * @param int $uid Record uid to move (online record)
1214 * @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
1215 * @param int $wsUid UID of offline version of online record
1216 * @param DataHandler $tcemainObj TCEmain object
1217 * @return void
1218 * @see moveRecord()
1219 */
1220 protected function moveRecord_wsPlaceholders($table, $uid, $destPid, $wsUid, DataHandler $tcemainObj) {
1221 // If a record gets moved after a record that already has a placeholder record
1222 // then the new placeholder record needs to be after the existing one
1223 $originalRecordDestinationPid = $destPid;
1224 if ($destPid < 0) {
1225 $movePlaceHolder = BackendUtility::getMovePlaceholder($table, abs($destPid), 'uid');
1226 if ($movePlaceHolder !== FALSE) {
1227 $destPid = -$movePlaceHolder['uid'];
1228 }
1229 }
1230 if ($plh = BackendUtility::getMovePlaceholder($table, $uid, 'uid')) {
1231 // If already a placeholder exists, move it:
1232 $tcemainObj->moveRecord_raw($table, $plh['uid'], $destPid);
1233 } else {
1234 // First, we create a placeholder record in the Live workspace that
1235 // represents the position to where the record is eventually moved to.
1236 $newVersion_placeholderFieldArray = array();
1237
1238 // Use property for move placeholders if set (since TYPO3 CMS 6.2)
1239 if (isset($GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForMovePlaceholders'])) {
1240 $shadowColumnsForMovePlaceholder = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForMovePlaceholders'];
1241 // Fallback to property for new placeholder (existed long time before TYPO3 CMS 6.2)
1242 } elseif (isset($GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'])) {
1243 $shadowColumnsForMovePlaceholder = $GLOBALS['TCA'][$table]['ctrl']['shadowColumnsForNewPlaceholders'];
1244 }
1245
1246 // Set values from the versioned record to the move placeholder
1247 if (!empty($shadowColumnsForMovePlaceholder)) {
1248 $versionedRecord = BackendUtility::getRecord($table, $wsUid);
1249 $shadowColumns = GeneralUtility::trimExplode(',', $shadowColumnsForMovePlaceholder, TRUE);
1250 foreach ($shadowColumns as $shadowColumn) {
1251 if (isset($versionedRecord[$shadowColumn])) {
1252 $newVersion_placeholderFieldArray[$shadowColumn] = $versionedRecord[$shadowColumn];
1253 }
1254 }
1255 }
1256
1257 if ($GLOBALS['TCA'][$table]['ctrl']['crdate']) {
1258 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['crdate']] = $GLOBALS['EXEC_TIME'];
1259 }
1260 if ($GLOBALS['TCA'][$table]['ctrl']['cruser_id']) {
1261 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['cruser_id']] = $tcemainObj->userid;
1262 }
1263 if ($GLOBALS['TCA'][$table]['ctrl']['tstamp']) {
1264 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['tstamp']] = $GLOBALS['EXEC_TIME'];
1265 }
1266 if ($table == 'pages') {
1267 // Copy page access settings from original page to placeholder
1268 $perms_clause = $tcemainObj->BE_USER->getPagePermsClause(1);
1269 $access = BackendUtility::readPageAccess($uid, $perms_clause);
1270 $newVersion_placeholderFieldArray['perms_userid'] = $access['perms_userid'];
1271 $newVersion_placeholderFieldArray['perms_groupid'] = $access['perms_groupid'];
1272 $newVersion_placeholderFieldArray['perms_user'] = $access['perms_user'];
1273 $newVersion_placeholderFieldArray['perms_group'] = $access['perms_group'];
1274 $newVersion_placeholderFieldArray['perms_everybody'] = $access['perms_everybody'];
1275 }
1276 $newVersion_placeholderFieldArray['t3ver_label'] = 'MovePlaceholder #' . $uid;
1277 $newVersion_placeholderFieldArray['t3ver_move_id'] = $uid;
1278 // Setting placeholder state value for temporary record
1279 $newVersion_placeholderFieldArray['t3ver_state'] = (string)new VersionState(VersionState::MOVE_PLACEHOLDER);
1280 // Setting workspace - only so display of place holders can filter out those from other workspaces.
1281 $newVersion_placeholderFieldArray['t3ver_wsid'] = $tcemainObj->BE_USER->workspace;
1282 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['label']] = '[MOVE-TO PLACEHOLDER for #' . $uid . ', WS#' . $tcemainObj->BE_USER->workspace . ']';
1283 // moving localized records requires to keep localization-settings for the placeholder too
1284 if (array_key_exists('languageField', $GLOBALS['TCA'][$table]['ctrl']) && array_key_exists('transOrigPointerField', $GLOBALS['TCA'][$table]['ctrl'])) {
1285 $l10nParentRec = BackendUtility::getRecord($table, $uid);
1286 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['languageField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['languageField']];
1287 $newVersion_placeholderFieldArray[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']] = $l10nParentRec[$GLOBALS['TCA'][$table]['ctrl']['transOrigPointerField']];
1288 unset($l10nParentRec);
1289 }
1290 // Initially, create at root level.
1291 $newVersion_placeholderFieldArray['pid'] = 0;
1292 $id = 'NEW_MOVE_PLH';
1293 // Saving placeholder as 'original'
1294 $tcemainObj->insertDB($table, $id, $newVersion_placeholderFieldArray, FALSE);
1295 // Move the new placeholder from temporary root-level to location:
1296 $tcemainObj->moveRecord_raw($table, $tcemainObj->substNEWwithIDs[$id], $destPid);
1297 // Move the workspace-version of the original to be the version of the move-to-placeholder:
1298 // Setting placeholder state value for version (so it can know it is currently a new version...)
1299 $updateFields = array(
1300 't3ver_state' => (string)new VersionState(VersionState::MOVE_POINTER)
1301 );
1302 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . (int)$wsUid, $updateFields);
1303 }
1304 // Check for the localizations of that element and move them as well
1305 $tcemainObj->moveL10nOverlayRecords($table, $uid, $destPid, $originalRecordDestinationPid);
1306 }
1307
1308 /**
1309 * Gets all possible child tables that are used on each parent table as field.
1310 *
1311 * @param string $parentTable Name of the parent table
1312 * @param array $possibleInlineChildren Collected possible inline children
1313 * @return array
1314 */
1315 protected function getPossibleInlineChildTablesOfParentTable($parentTable, array $possibleInlineChildren = array()) {
1316 foreach ($GLOBALS['TCA'][$parentTable]['columns'] as $parentField => $parentFieldDefinition) {
1317 if (isset($parentFieldDefinition['config']['type'])) {
1318 $parentFieldConfiguration = $parentFieldDefinition['config'];
1319 if ($parentFieldConfiguration['type'] == 'inline' && isset($parentFieldConfiguration['foreign_table'])) {
1320 if (!in_array($parentFieldConfiguration['foreign_table'], $possibleInlineChildren)) {
1321 $possibleInlineChildren = $this->getPossibleInlineChildTablesOfParentTable($parentFieldConfiguration['foreign_table'], array_merge($possibleInlineChildren, $parentFieldConfiguration['foreign_table']));
1322 }
1323 }
1324 }
1325 }
1326 return $possibleInlineChildren;
1327 }
1328
1329 /**
1330 * Gets an instance of the command map helper.
1331 *
1332 * @param DataHandler $tceMain TCEmain object
1333 * @return \TYPO3\CMS\Version\DataHandler\CommandMap
1334 */
1335 public function getCommandMap(DataHandler $tceMain) {
1336 return GeneralUtility::makeInstance(
1337 \TYPO3\CMS\Version\DataHandler\CommandMap::class,
1338 $this,
1339 $tceMain,
1340 $tceMain->cmdmap,
1341 $tceMain->BE_USER->workspace
1342 );
1343 }
1344
1345 /**
1346 * Returns all fieldnames from a table which have the unique evaluation type set.
1347 *
1348 * @param string $table Table name
1349 * @return array Array of fieldnames
1350 */
1351 protected function getUniqueFields($table) {
1352 $listArr = array();
1353 if ($GLOBALS['TCA'][$table]['columns']) {
1354 foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $configArr) {
1355 if ($configArr['config']['type'] === 'input') {
1356 $evalCodesArray = GeneralUtility::trimExplode(',', $configArr['config']['eval'], TRUE);
1357 if (in_array('uniqueInPid', $evalCodesArray) || in_array('unique', $evalCodesArray)) {
1358 $listArr[] = $field;
1359 }
1360 }
1361 }
1362 }
1363 return $listArr;
1364 }
1365
1366 /**
1367 * @return \TYPO3\CMS\Core\Database\RelationHandler
1368 */
1369 protected function createRelationHandlerInstance() {
1370 return GeneralUtility::makeInstance(\TYPO3\CMS\Core\Database\RelationHandler::class);
1371 }
1372
1373 }