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