2 /***************************************************************
5 * (c) 1999-2010 Kasper Skårhøj (kasperYYYY@typo3.com)
6 * (c) 2010 Benjamin Mack (benni@typo3.org)
10 * This script is part of the TYPO3 project. The TYPO3 project is
11 * free software; you can redistribute it and/or modify
12 * it under the terms of the GNU General Public License as published by
13 * the Free Software Foundation; either version 2 of the License, or
14 * (at your option) any later version.
16 * The GNU General Public License can be found at
17 * http://www.gnu.org/copyleft/gpl.html.
18 * A copy is found in the textfile GPL.txt and important notices to the license
19 * from the author is found in LICENSE.txt distributed with these scripts.
22 * This script is distributed in the hope that it will be useful,
23 * but WITHOUT ANY WARRANTY; without even the implied warranty of
24 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
25 * GNU General Public License for more details.
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
31 * Contains some parts for staging, versioning and workspaces
32 * to interact with the TYPO3 Core Engine
35 class tx_version_tcemain
{
37 // For accumulating information about workspace stages raised
38 // on elements so a single mail is sent as notification.
39 // previously called "accumulateForNotifEmail" in tcemain
40 protected $notificationEmailInfo = array();
42 // general comment, eg. for staging in workspaces.
43 protected $generalComment = '';
46 /****************************
47 ***** Cmdmap Hooks ******
48 ****************************/
51 * hook that is called before any cmd of the commandmap is
53 * @param $tcemainObj reference to the main tcemain object
56 public function processCmdmap_beforeStart(&$tcemainObj) {
57 // Reset notification array
58 $this->notificationEmailInfo
= array();
63 * hook that is called when no prepared command was found
65 * @param $command the command to be executed
66 * @param $table the table of the record
67 * @param $id the ID of the record
68 * @param $value the value containing the data
69 * @param $commandIsProcessed can be set so that other hooks or
70 * TCEmain knows that the default cmd doesn't have to be called
71 * @param $tcemainObj reference to the main tcemain object
74 public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, &$tcemainObj) {
77 // custom command "version"
78 if ($command == 'version') {
79 $commandWasProcessed = TRUE;
80 $action = (string) $value['action'];
84 // check if page / branch versioning is needed,
85 // or if "element" version can be used
87 if (isset($value['treeLevels'])) {
88 $versionizeTree = t3lib_div
::intInRange($value['treeLevels'], -1, 100);
90 if ($table == 'pages' && $versionizeTree >= 0) {
91 $this->versionizePages($id, $value['label'], $versionizeTree, $tcemainObj);
93 $tcemainObj->versionizeRecord($table, $id, $value['label']);
98 $swapMode = $tcemainObj->BE_USER
->getTSConfigVal('options.workspaces.swapMode');
99 $elementList = array();
100 if ($swapMode == 'any' ||
($swapMode == 'page' && $table == 'pages')) {
101 // check if we are allowed to do synchronios publish.
102 // We must have a single element in the cmdmap to be allowed
103 if (count($tcemainObj->cmdmap
) == 1 && count($tcemainObj->cmdmap
[$table]) == 1) {
104 $elementList = $this->findPageElementsForVersionSwap($table, $id, $value['swapWith']);
107 if (count($elementList) == 0) {
108 $elementList[$table][] = array($id, $value['swapWith']);
110 foreach ($elementList as $tbl => $idList) {
111 foreach ($idList as $idKey => $idSet) {
112 $this->version_swap($tbl, $idSet[0], $idSet[1], $value['swapIntoWS'], $tcemainObj);
118 $this->version_clearWSID($table, $id, FALSE, $tcemainObj);
122 $this->version_clearWSID($table, $id, TRUE, $tcemainObj);
126 $elementList = array();
127 $idList = $elementList[$table] = t3lib_div
::trimExplode(',', $id, 1);
128 $setStageMode = $tcemainObj->BE_USER
->getTSConfigVal('options.workspaces.changeStageMode');
129 if ($setStageMode == 'any' ||
$setStageMode == 'page') {
130 if (count($idList) == 1) {
131 $rec = t3lib_BEfunc
::getRecord($table, $idList[0], 't3ver_wsid');
132 $workspaceId = $rec['t3ver_wsid'];
134 $workspaceId = $tcemainObj->BE_USER
->workspace
;
136 if ($table !== 'pages') {
137 if ($setStageMode == 'any') {
138 // (1) Find page to change stage and (2)
139 // find other elements from the same ws to change stage
140 $pageIdList = array();
141 $this->findPageIdsForVersionStateChange($table, $idList, $workspaceId, $pageIdList, $elementList);
142 $this->findPageElementsForVersionStageChange($pageIdList, $workspaceId, $elementList);
145 // Find all elements from the same ws to change stage
146 $this->findRealPageIds($idList);
147 $this->findPageElementsForVersionStageChange($idList, $workspaceId, $elementList);
151 foreach ($elementList as $tbl => $elementIdList) {
152 foreach ($elementIdList as $elementId) {
153 $this->version_setStage($tbl, $elementId, $value['stageId'], ($value['comment'] ?
$value['comment'] : $this->generalComment
), TRUE, $tcemainObj);
162 * hook that is called AFTER all commands of the commandmap was
164 * @param $tcemainObj reference to the main tcemain object
167 public function processCmdmap_afterFinish(&$tcemainObj) {
168 // Empty accumulation array:
169 foreach ($this->notificationEmailInfo
as $notifItem) {
170 $this->notifyStageChange($notifItem['shared'][0], $notifItem['shared'][1], implode(', ', $notifItem['elements']), 0, $notifItem['shared'][2], $tcemainObj);
173 // Reset notification array
174 $this->notificationEmailInfo
= array();
179 * hook that is called AFTER all commands of the commandmap was
181 * @param $tcemainObj reference to the main tcemain object
184 public function processCmdmap_deleteAction($table, $id, $record, &$recordWasDeleted, &$tcemainObj) {
185 $id = $record['uid'];
187 // For Live version, try if there is a workspace version because if so, rather "delete" that instead
188 // Look, if record is an offline version, then delete directly:
189 if ($record['pid'] != -1) {
190 if ($wsVersion = t3lib_BEfunc
::getWorkspaceVersionOfRecord($tcemainObj->BE_USER
->workspace
, $table, $id)) {
191 $record = $wsVersion;
192 $id = $record['uid'];
196 // Look, if record is an offline version, then delete directly:
197 if ($record['pid'] == -1) {
198 if ($TCA[$table]['ctrl']['versioningWS']) {
199 // In Live workspace, delete any. In other workspaces there must be match.
200 if ($tcemainObj->BE_USER
->workspace
== 0 ||
(int) $record['t3ver_wsid'] == $tcemainObj->BE_USER
->workspace
) {
201 $liveRec = t3lib_BEfunc
::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state');
203 // Delete those in WS 0 + if their live records state was not "Placeholder".
204 if ($record['t3ver_wsid']==0 ||
(int) $liveRec['t3ver_state'] <= 0) {
205 $tcemainObj->deleteEl($table, $id);
207 // If live record was placeholder (new/deleted), rather clear
208 // it from workspace (because it clears both version and placeholder).
209 $this->version_clearWSID($table, $id, FALSE, $tcemainObj);
211 } else $tcemainObj->newlog('Tried to delete record from another workspace',1);
212 } else $tcemainObj->newlog('Versioning not enabled for record with PID = -1!',2);
213 } elseif ($res = $tcemainObj->BE_USER
->workspaceAllowLiveRecordsInPID($record['pid'], $table)) {
214 // Look, if record is "online" or in a versionized branch, then delete directly.
216 $tcemainObj->deleteEl($table, $id);
218 $tcemainObj->newlog('Stage of root point did not allow for deletion',1);
220 } elseif ((int)$record['t3ver_state']===3) {
221 // Placeholders for moving operations are deletable directly.
223 // Get record which its a placeholder for and reset the t3ver_state of that:
224 if ($wsRec = t3lib_BEfunc
::getWorkspaceVersionOfRecord($record['t3ver_wsid'], $table, $record['t3ver_move_id'], 'uid')) {
225 // Clear the state flag of the workspace version of the record
226 // Setting placeholder state value for version (so it can know it is currently a new version...)
227 $updateFields = array(
230 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($wsRec['uid']), $updateFields);
232 $tcemainObj->deleteEl($table, $id);
234 // Otherwise, try to delete by versioning:
235 $tcemainObj->versionizeRecord($table, $id, 'DELETED!', TRUE);
236 $tcemainObj->deleteL10nOverlayRecords($table, $id);
241 /****************************
242 ***** Notifications ******
243 ****************************/
246 * Send an email notification to users in workspace
248 * @param array Workspace access array (from t3lib_userauthgroup::checkWorkspace())
249 * @param integer New Stage number: 0 = editing, 1= just ready for review, 10 = ready for publication, -1 = rejected!
250 * @param string Table name of element (or list of element names if $id is zero)
251 * @param integer Record uid of element (if zero, then $table is used as reference to element(s) alone)
252 * @param string User comment sent along with action
255 protected function notifyStageChange($stat, $stageId, $table, $id, $comment, $tcemainObj) {
256 $workspaceRec = t3lib_BEfunc
::getRecord('sys_workspace', $stat['uid']);
257 // So, if $id is not set, then $table is taken to be the complete element name!
258 $elementName = $id ?
$table . ':' . $id : $table;
260 if (is_array($workspaceRec)) {
263 switch ((int)$stageId) {
265 $newStage = 'Ready for review';
268 $newStage = 'Ready for publishing';
271 $newStage = 'Element was rejected!';
274 $newStage = 'Rejected element was noticed and edited';
277 $newStage = 'Unknown state change!?';
281 // Compile list of recipients:
283 switch((int)$stat['stagechg_notification']) {
285 switch((int)$stageId) {
287 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']);
290 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], TRUE);
293 # $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']);
294 # $emails = array_merge($emails,$this->getEmailsForStageChangeNotification($workspaceRec['members']));
296 // List of elements to reject:
297 $allElements = explode(',', $elementName);
298 // Traverse them, and find the history of each
299 foreach ($allElements as $elRef) {
300 list($eTable, $eUid) = explode(':', $elRef);
302 $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
303 'log_data,tstamp,userid',
305 'action=6 and details_nr=30
306 AND tablename=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($eTable, 'sys_log') . '
307 AND recuid=' . intval($eUid),
311 // Find all implicated since the last stage-raise from editing to review:
312 foreach ($rows as $dat) {
313 $data = unserialize($dat['log_data']);
315 $emails = array_merge($emails, $this->getEmailsForStageChangeNotification($dat['userid'], TRUE));
317 if ($data['stage'] == 1) {
325 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']);
329 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], TRUE);
335 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], TRUE);
336 $emails = array_merge($emails, $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']));
337 $emails = array_merge($emails, $this->getEmailsForStageChangeNotification($workspaceRec['members']));
340 $emails = array_unique($emails);
342 // Path to record is found:
343 list($eTable,$eUid) = explode(':', $elementName);
344 $eUid = intval($eUid);
345 $rr = t3lib_BEfunc
::getRecord($eTable, $eUid);
346 $recTitle = t3lib_BEfunc
::getRecordTitle($eTable, $rr);
347 if ($eTable != 'pages') {
348 t3lib_BEfunc
::fixVersioningPid($eTable, $rr);
351 $path = t3lib_BEfunc
::getRecordPath($eUid, '', 20);
353 // ALternative messages:
354 $TSConfig = $tcemainObj->getTCEMAIN_TSconfig($eUid);
355 $body = trim($TSConfig['notificationEmail_body']) ?
trim($TSConfig['notificationEmail_body']) : '
356 At the TYPO3 site "%s" (%s)
357 in workspace "%s" (#%s)
358 the stage has changed for the element(s) "%11$s" (%s) at location "%10$s" in the page tree:
365 State was change by %s (username: %s)
367 $subject = trim($TSConfig['notificationEmail_subject']) ?
trim($TSConfig['notificationEmail_subject']) : 'TYPO3 Workspace Note: Stage Change for %s';
370 if (count($emails)) {
371 $message = sprintf($body,
372 $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
373 t3lib_div
::getIndpEnv('TYPO3_SITE_URL').TYPO3_mainDir
,
374 $workspaceRec['title'],
375 $workspaceRec['uid'],
379 $this->BE_USER
->user
['realName'],
380 $this->BE_USER
->user
['username'],
384 t3lib_div
::plainMailEncoded(
385 implode(',', $emails),
386 sprintf($subject, $elementName),
390 $tcemainObj->newlog2('Notification email for stage change was sent to "' . implode(', ', $emails) . '"', $table, $id);
396 * Return emails addresses of be_users from input list.
397 * previously called notifyStageChange_getEmails() in tcemain
399 * @param string List of backend users, on the form "be_users_10,be_users_2" or "10,2" in case noTablePrefix is set.
400 * @param boolean If set, the input list are integers and not strings.
401 * @return array Array of emails
403 protected function getEmailsForStageChangeNotification($listOfUsers, $noTablePrefix = FALSE) {
404 $users = t3lib_div
::trimExplode(',', $listOfUsers, 1);
406 foreach ($users as $userIdent) {
407 if ($noTablePrefix) {
408 $id = intval($userIdent);
410 list($table, $id) = t3lib_div
::revExplode('_', $userIdent, 2);
412 if ($table === 'be_users' ||
$noTablePrefix) {
413 if ($userRecord = t3lib_BEfunc
::getRecord('be_users', $id, 'email')) {
414 if (strlen(trim($userRecord['email']))) {
415 $emails[$id] = $userRecord['email'];
425 /****************************
426 ***** Stage Changes ******
427 ****************************/
430 * Setting stage of record
432 * @param string Table name
433 * @param integer Record UID
434 * @param integer Stage ID to set
435 * @param string Comment that goes into log
436 * @param boolean Accumulate state changes in memory for compiled notification email?
439 protected function version_setStage($table, $id, $stageId, $comment = '', $notificationEmailInfo = FALSE, $tcemainObj) {
440 if ($errorCode = $tcemainObj->BE_USER
->workspaceCannotEditOfflineVersion($table, $id)) {
441 $tcemainObj->newlog('Attempt to set stage for record failed: ' . $errorCode, 1);
442 } elseif ($tcemainObj->checkRecordUpdateAccess($table, $id)) {
443 $record = t3lib_BEfunc
::getRecord($table, $id);
444 $stat = $tcemainObj->BE_USER
->checkWorkspace($record['t3ver_wsid']);
446 if (t3lib_div
::inList('admin,online,offline,reviewer,owner', $stat['_ACCESS']) ||
($stageId <= 1 && $stat['_ACCESS'] === 'member')) {
448 // Set stage of record:
450 't3ver_stage' => $stageId
452 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($id), $updateData);
453 $tcemainObj->newlog2('Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', $table, $id);
455 // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere!
456 $tcemainObj->log($table, $id, 6, 0, 0, 'Stage raised...', 30, array('comment' => $comment, 'stage' => $stageId));
458 if ((int)$stat['stagechg_notification'] > 0) {
459 if ($notificationEmailInfo) {
460 $this->notificationEmailInfo
[$stat['uid'] . ':' . $stageId . ':' . $comment]['shared'] = array($stat, $stageId, $comment);
461 $this->notificationEmailInfo
[$stat['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = $table . ':' . $id;
463 $this->notifyStageChange($stat, $stageId, $table, $id, $comment, $tcemainObj);
466 } else $tcemainObj->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', 1);
467 } else $tcemainObj->newlog('Attempt to set stage for record failed because you do not have edit access', 1);
472 /*****************************
473 ***** CMD versioning ******
474 *****************************/
477 * Creates a new version of a page including content and possible subpages.
479 * @param integer Page uid to create new version of.
480 * @param string Version label
481 * @param integer Indicating "treeLevel" - "page" (0) or "branch" (>=1) ["element" type must call versionizeRecord() directly]
485 protected function versionizePages($uid, $label, $versionizeTree, &$tcemainObj) {
489 // returns the branch
490 $brExist = $tcemainObj->doesBranchExist('', $uid, $tcemainObj->pMap
['show'], 1);
492 // Checks if we had permissions
493 if ($brExist != -1) {
495 // Make list of tables that should come along with a new version of the page:
496 $verTablesArray = array();
497 $allTables = array_keys($TCA);
498 foreach ($allTables as $tableName) {
499 if ($tableName != 'pages' && ($versionizeTree > 0 ||
$TCA[$tableName]['ctrl']['versioning_followPages'])) {
500 $verTablesArray[] = $tableName;
504 // Begin to copy pages if we're allowed to:
505 if ($tcemainObj->BE_USER
->workspaceVersioningTypeAccess($versionizeTree)) {
507 // Versionize this page:
508 $theNewRootID = $tcemainObj->versionizeRecord('pages', $uid, $label, FALSE, $versionizeTree);
510 $tcemainObj->rawCopyPageContent($uid, $theNewRootID, $verTablesArray, $tcemainObj);
512 // If we're going to copy recursively...:
513 if ($versionizeTree > 0) {
515 // Get ALL subpages to copy (read permissions respected - they should NOT be...):
516 $CPtable = $tcemainObj->int_pageTreeInfo(array(), $uid, intval($versionizeTree), $theNewRootID);
518 // Now copying the subpages
519 foreach ($CPtable as $thePageUid => $thePagePid) {
520 $newPid = $tcemainObj->copyMappingArray
['pages'][$thePagePid];
521 if (isset($newPid)) {
522 $theNewRootID = $tcemainObj->copyRecord_raw('pages', $thePageUid, $newPid);
523 $tcemainObj->rawCopyPageContent($thePageUid, $theNewRootID, $verTablesArray, $tcemainObj);
525 $tcemainObj->newlog('Something went wrong during copying branch (for versioning)', 1);
529 } // else the page was not copied. Too bad...
530 } else $tcemainObj->newlog('The root version could not be created!',1);
531 } else $tcemainObj->newlog('Versioning type "'.$versionizeTree.'" was not allowed in workspace',1);
532 } else $tcemainObj->newlog('Could not read all subpages to versionize.',1);
537 * Swapping versions of a record
538 * 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
540 * @param string Table name
541 * @param integer UID of the online record to swap
542 * @param integer UID of the archived version to swap with!
543 * @param boolean If set, swaps online into workspace instead of publishing out of workspace.
546 protected function version_swap($table, $id, $swapWith, $swapIntoWS=0, &$tcemainObj) {
549 // First, check if we may actually edit the online record
550 if ($tcemainObj->checkRecordUpdateAccess($table, $id)) {
552 // Select the two versions:
553 $curVersion = t3lib_BEfunc
::getRecord($table, $id, '*');
554 $swapVersion = t3lib_BEfunc
::getRecord($table, $swapWith, '*');
558 if (is_array($curVersion) && is_array($swapVersion)) {
559 if ($tcemainObj->BE_USER
->workspacePublishAccess($swapVersion['t3ver_wsid'])) {
560 $wsAccess = $tcemainObj->BE_USER
->checkWorkspace($swapVersion['t3ver_wsid']);
561 if ($swapVersion['t3ver_wsid'] <= 0 ||
!($wsAccess['publish_access'] & 1) ||
(int)$swapVersion['t3ver_stage'] === 10) {
562 if ($tcemainObj->doesRecordExist($table,$swapWith,'show') && $tcemainObj->checkRecordUpdateAccess($table,$swapWith)) {
563 if (!$swapIntoWS ||
$tcemainObj->BE_USER
->workspaceSwapAccess()) {
565 // Check if the swapWith record really IS a version of the original!
566 if ((int)$swapVersion['pid'] == -1 && (int)$curVersion['pid'] >= 0 && !strcmp($swapVersion['t3ver_oid'], $id)) {
569 $lockFileName = PATH_site
.'typo3temp/swap_locking/' . $table . ':' . $id . '.ser';
571 if (!@is_file
($lockFileName)) {
574 t3lib_div
::writeFileToTypo3tempDir($lockFileName, serialize(array(
575 'tstamp' => $GLOBALS['EXEC_TIME'],
576 'user' => $tcemainObj->BE_USER
->user
['username'],
577 'curVersion' => $curVersion,
578 'swapVersion' => $swapVersion
581 // Find fields to keep
582 $keepFields = $tcemainObj->getUniqueFields($table);
583 if ($TCA[$table]['ctrl']['sortby']) {
584 $keepFields[] = $TCA[$table]['ctrl']['sortby'];
586 // l10n-fields must be kept otherwise the localization
587 // will be lost during the publishing
588 if (!isset($TCA[$table]['ctrl']['transOrigPointerTable']) && $TCA[$table]['ctrl']['transOrigPointerField']) {
589 $keepFields[] = $TCA[$table]['ctrl']['transOrigPointerField'];
593 foreach ($keepFields as $fN) {
594 $tmp = $swapVersion[$fN];
595 $swapVersion[$fN] = $curVersion[$fN];
596 $curVersion[$fN] = $tmp;
600 $t3ver_state = array();
601 $t3ver_state['swapVersion'] = $swapVersion['t3ver_state'];
602 $t3ver_state['curVersion'] = $curVersion['t3ver_state'];
604 // Modify offline version to become online:
605 $tmp_wsid = $swapVersion['t3ver_wsid'];
606 // Set pid for ONLINE
607 $swapVersion['pid'] = intval($curVersion['pid']);
608 // We clear this because t3ver_oid only make sense for offline versions
609 // and we want to prevent unintentional misuse of this
610 // value for online records.
611 $swapVersion['t3ver_oid'] = 0;
612 // In case of swapping and the offline record has a state
613 // (like 2 or 4 for deleting or move-pointer) we set the
614 // current workspace ID so the record is not deselected
615 // in the interface by t3lib_BEfunc::versioningPlaceholderClause()
616 $swapVersion['t3ver_wsid'] = 0;
618 if ($t3ver_state['swapVersion'] > 0) {
619 $swapVersion['t3ver_wsid'] = $tcemainObj->BE_USER
->workspace
;
621 $swapVersion['t3ver_wsid'] = intval($curVersion['t3ver_wsid']);
624 $swapVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
625 $swapVersion['t3ver_stage'] = 0;
627 $swapVersion['t3ver_state'] = 0;
631 if ((int)$TCA[$table]['ctrl']['versioningWS']>=2) { // && $t3ver_state['swapVersion']==4 // Maybe we don't need this?
632 if ($plhRec = t3lib_BEfunc
::getMovePlaceholder($table, $id, 't3ver_state,pid,uid' . ($TCA[$table]['ctrl']['sortby'] ?
',' . $TCA[$table]['ctrl']['sortby'] : ''))) {
633 $movePlhID = $plhRec['uid'];
634 $movePlh['pid'] = $swapVersion['pid'];
635 $swapVersion['pid'] = intval($plhRec['pid']);
637 $curVersion['t3ver_state'] = intval($swapVersion['t3ver_state']);
638 $swapVersion['t3ver_state'] = 0;
640 if ($TCA[$table]['ctrl']['sortby']) {
641 // sortby is a "keepFields" which is why this will work...
642 $movePlh[$TCA[$table]['ctrl']['sortby']] = $swapVersion[$TCA[$table]['ctrl']['sortby']];
643 $swapVersion[$TCA[$table]['ctrl']['sortby']] = $plhRec[$TCA[$table]['ctrl']['sortby']];
648 // Take care of relations in each field (e.g. IRRE):
649 if (is_array($GLOBALS['TCA'][$table]['columns'])) {
650 foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fieldConf) {
651 $this->version_swap_procBasedOnFieldType(
652 $table, $field, $fieldConf['config'], $curVersion, $swapVersion, $tcemainObj
656 unset($swapVersion['uid']);
658 // Modify online version to become offline:
659 unset($curVersion['uid']);
660 // Set pid for OFFLINE
661 $curVersion['pid'] = -1;
662 $curVersion['t3ver_oid'] = intval($id);
663 $curVersion['t3ver_wsid'] = ($swapIntoWS ?
intval($tmp_wsid) : 0);
664 $curVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
665 $curVersion['t3ver_count'] = $curVersion['t3ver_count']+
1; // Increment lifecycle counter
666 $curVersion['t3ver_stage'] = 0;
668 $curVersion['t3ver_state'] = 0;
671 // Keeping the swapmode state
672 if ($table === 'pages') {
673 $curVersion['t3ver_swapmode'] = $swapVersion['t3ver_swapmode'];
676 // Registering and swapping MM relations in current and swap records:
677 $tcemainObj->version_remapMMForVersionSwap($table, $id, $swapWith);
679 // Generating proper history data to prepare logging
680 $tcemainObj->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion);
681 $tcemainObj->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion);
684 $sqlErrors = array();
685 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($id), $swapVersion);
686 if ($GLOBALS['TYPO3_DB']->sql_error()) {
687 $sqlErrors[] = $GLOBALS['TYPO3_DB']->sql_error();
689 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($swapWith), $curVersion);
690 if ($GLOBALS['TYPO3_DB']->sql_error()) {
691 $sqlErrors[] = $GLOBALS['TYPO3_DB']->sql_error();
693 unlink($lockFileName);
697 if (!count($sqlErrors)) {
699 // If a moving operation took place...:
701 // Remove, if normal publishing:
703 // For delete + completely delete!
704 $tcemainObj->deleteEl($table, $movePlhID, TRUE, TRUE);
706 // Otherwise update the movePlaceholder:
707 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($movePlhID), $movePlh);
708 $tcemainObj->updateRefIndex($table, $movePlhID);
712 // Checking for delete:
713 // Delete only if new/deleted placeholders are there.
714 if (!$swapIntoWS && ((int)$t3ver_state['swapVersion'] === 1 ||
(int)$t3ver_state['swapVersion'] === 2)) {
716 $tcemainObj->deleteEl($table, $id, TRUE);
719 $tcemainObj->newlog2(($swapIntoWS ?
'Swapping' : 'Publishing') . ' successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, $table, $id, $swapVersion['pid']);
721 // Update reference index of the live record:
722 $tcemainObj->updateRefIndex($table, $id);
724 // Set log entry for live record:
725 $propArr = $tcemainObj->getRecordPropertiesFromRow($table, $swapVersion);
726 if ($propArr['_ORIG_pid'] == -1) {
727 $label = $GLOBALS['LANG']->sL ('LLL:EXT:lang/locallang_tcemain.xml:version_swap.offline_record_updated');
729 $label = $GLOBALS['LANG']->sL ('LLL:EXT:lang/locallang_tcemain.xml:version_swap.online_record_updated');
731 $theLogId = $tcemainObj->log($table, $id, 2, $propArr['pid'], 0, $label , 10, array($propArr['header'], $table . ':' . $id), $propArr['event_pid']);
732 $tcemainObj->setHistory($table, $id, $theLogId);
734 // Update reference index of the offline record:
735 $tcemainObj->updateRefIndex($table, $swapWith);
736 // Set log entry for offline record:
737 $propArr = $tcemainObj->getRecordPropertiesFromRow($table, $curVersion);
738 if ($propArr['_ORIG_pid'] == -1) {
739 $label = $GLOBALS['LANG']->sL ('LLL:EXT:lang/locallang_tcemain.xml:version_swap.offline_record_updated');
741 $label = $GLOBALS['LANG']->sL ('LLL:EXT:lang/locallang_tcemain.xml:version_swap.online_record_updated');
743 $theLogId = $tcemainObj->log($table, $swapWith, 2, $propArr['pid'], 0, $label, 10, array($propArr['header'], $table . ':' . $swapWith), $propArr['event_pid']);
744 $tcemainObj->setHistory($table, $swapWith, $theLogId);
746 // SWAPPING pids for subrecords:
747 if ($table=='pages' && $swapVersion['t3ver_swapmode'] >= 0) {
749 // Collect table names that should be copied along with the tables:
750 foreach ($TCA as $tN => $tCfg) {
751 // For "Branch" publishing swap ALL,
752 // otherwise for "page" publishing, swap only "versioning_followPages" tables
753 if ($swapVersion['t3ver_swapmode'] > 0 ||
$TCA[$tN]['ctrl']['versioning_followPages']) {
754 $temporaryPid = -($id+
1000000);
756 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($tN, 'pid=' . intval($id), array('pid' => $temporaryPid));
757 if ($GLOBALS['TYPO3_DB']->sql_error()) {
758 $sqlErrors[] = $GLOBALS['TYPO3_DB']->sql_error();
761 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($tN, 'pid=' . intval($swapWith), array('pid' => $id));
762 if ($GLOBALS['TYPO3_DB']->sql_error()) {
763 $sqlErrors[] = $GLOBALS['TYPO3_DB']->sql_error();
766 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($tN, 'pid=' . intval($temporaryPid), array('pid' => $swapWith));
767 if ($GLOBALS['TYPO3_DB']->sql_error()) {
768 $sqlErrors[] = $GLOBALS['TYPO3_DB']->sql_error();
771 if (count($sqlErrors)) {
772 $tcemainObj->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), 2);
778 $tcemainObj->clear_cache($table, $id);
780 // Checking for "new-placeholder" and if found, delete it (BUT FIRST after swapping!):
781 if (!$swapIntoWS && $t3ver_state['curVersion']>0) {
782 // For delete + completely delete!
783 $tcemainObj->deleteEl($table, $swapWith, TRUE, TRUE);
785 } else $tcemainObj->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), 2);
786 } else $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);
787 } else $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);
788 } else $tcemainObj->newlog('Workspace #' . $swapVersion['t3ver_wsid'] . ' does not support swapping.', 1);
789 } else $tcemainObj->newlog('You cannot publish a record you do not have edit and show permissions for', 1);
790 } else $tcemainObj->newlog('Records in workspace #' . $swapVersion['t3ver_wsid'] . ' can only be published when in "Publish" stage.', 1);
791 } else $tcemainObj->newlog('User could not publish records from workspace #' . $swapVersion['t3ver_wsid'], 1);
792 } else $tcemainObj->newlog('Error: Either online or swap version could not be selected!', 2);
793 } else $tcemainObj->newlog('Error: You cannot swap versions for a record you do not have access to edit!', 1);
798 * Update relations on version/workspace swapping.
800 * @param string $table: Record Table
801 * @param string $field: Record field
802 * @param array $conf: TCA configuration of current field
803 * @param string $curVersion: Reference to the current (original) record
804 * @param string $swapVersion: Reference to the record (workspace/versionized) to publish in or swap with
807 protected function version_swap_procBasedOnFieldType($table, $field, $conf, &$curVersion, &$swapVersion, $tcemainObj) {
808 $inlineType = $tcemainObj->getInlineFieldType($conf);
810 // Process pointer fields on normalized database:
811 if ($inlineType == 'field') {
812 // Read relations that point to the current record (e.g. live record):
813 $dbAnalysisCur = t3lib_div
::makeInstance('t3lib_loadDBGroup');
814 $dbAnalysisCur->start('', $conf['foreign_table'], '', $curVersion['uid'], $table, $conf);
815 // Read relations that point to the record to be swapped with e.g. draft record):
816 $dbAnalysisSwap = t3lib_div
::makeInstance('t3lib_loadDBGroup');
817 $dbAnalysisSwap->start('', $conf['foreign_table'], '', $swapVersion['uid'], $table, $conf);
818 // Update relations for both (workspace/versioning) sites:
819 $dbAnalysisCur->writeForeignField($conf, $curVersion['uid'], $swapVersion['uid']);
820 $dbAnalysisSwap->writeForeignField($conf, $swapVersion['uid'], $curVersion['uid']);
822 // Swap field values (CSV):
823 // BUT: These values will be swapped back in the next steps, when the *CHILD RECORD ITSELF* is swapped!
824 } elseif ($inlineType == 'list') {
825 $tempValue = $curVersion[$field];
826 $curVersion[$field] = $swapVersion[$field];
827 $swapVersion[$field] = $tempValue;
834 * Release version from this workspace (and into "Live" workspace but as an offline version).
836 * @param string Table name
837 * @param integer Record UID
838 * @param boolean If set, will completely delete element
841 protected function version_clearWSID($table, $id, $flush = FALSE, &$tcemainObj) {
844 if ($errorCode = $tcemainObj->BE_USER
->workspaceCannotEditOfflineVersion($table, $id)) {
845 $tcemainObj->newlog('Attempt to reset workspace for record failed: ' . $errorCode, 1);
846 } elseif ($tcemainObj->checkRecordUpdateAccess($table, $id)) {
847 if ($liveRec = t3lib_BEfunc
::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state')) {
848 // Clear workspace ID:
852 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($id), $updateData);
854 // Clear workspace ID for live version AND DELETE IT as well because it is a new record!
855 if ((int) $liveRec['t3ver_state'] == 1 ||
(int) $liveRec['t3ver_state'] == 2) {
856 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table,'uid=' . intval($liveRec['uid']), $updateData);
857 // THIS assumes that the record was placeholder ONLY for ONE record (namely $id)
858 $tcemainObj->deleteEl($table, $liveRec['uid'], TRUE);
861 // If "deleted" flag is set for the version that got released
862 // it doesn't make sense to keep that "placeholder" anymore and we delete it completly.
863 $wsRec = t3lib_BEfunc
::getRecord($table, $id);
864 if ($flush ||
((int) $wsRec['t3ver_state'] == 1 ||
(int) $wsRec['t3ver_state'] == 2)) {
865 $tcemainObj->deleteEl($table, $id, TRUE, TRUE);
868 // Remove the move-placeholder if found for live record.
869 if ((int)$TCA[$table]['ctrl']['versioningWS'] >= 2) {
870 if ($plhRec = t3lib_BEfunc
::getMovePlaceholder($table, $liveRec['uid'], 'uid')) {
871 $tcemainObj->deleteEl($table, $plhRec['uid'], TRUE, TRUE);
875 } else $tcemainObj->newlog('Attempt to reset workspace for record failed because you do not have edit access',1);
879 /*******************************
880 ***** helper functions ******
881 *******************************/
885 * Copies all records from tables in $copyTablesArray from page with $old_pid to page with $new_pid
886 * Uses raw-copy for the operation (meant for versioning!)
888 * @param integer Current page id.
889 * @param integer New page id
890 * @param array Array of tables from which to copy
892 * @see versionizePages()
894 protected function rawCopyPageContent($oldPageId, $newPageId, $copyTablesArray, &$tcemainObj) {
898 foreach ($copyTablesArray as $table) {
899 // all records under the page is copied.
900 if ($table && is_array($TCA[$table]) && $table != 'pages') {
901 $mres = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
904 'pid=' . intval($oldPageId) . $tcemainObj->deleteClause($table)
906 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($mres)) {
907 // Check, if this record has already been copied by a parent record as relation:
908 if (!$this->copyMappingArray
[$table][$row['uid']]) {
909 // Copying each of the underlying records (method RAW)
910 $tcemainObj->copyRecord_raw($table, $row['uid'], $newPageId);
913 $GLOBALS['TYPO3_DB']->sql_free_result($mres);
921 * Finds all elements for swapping versions in workspace
923 * @param string $table Table name of the original element to swap
924 * @param int $id UID of the original element to swap (online)
925 * @param int $offlineId As above but offline
926 * @return array Element data. Key is table name, values are array with first element as online UID, second - offline UID
928 protected function findPageElementsForVersionSwap($table, $id, $offlineId) {
931 $rec = t3lib_BEfunc
::getRecord($table, $offlineId, 't3ver_wsid');
932 $workspaceId = $rec['t3ver_wsid'];
934 $elementData = array();
935 if ($workspaceId != 0) {
936 // Get page UID for LIVE and workspace
937 if ($table != 'pages') {
938 $rec = t3lib_BEfunc
::getRecord($table, $id, 'pid');
939 $pageId = $rec['pid'];
940 $rec = t3lib_BEfunc
::getRecord('pages', $pageId);
941 t3lib_BEfunc
::workspaceOL('pages', $rec, $workspaceId);
942 $offlinePageId = $rec['_ORIG_uid'];
945 $offlinePageId = $offlineId;
948 // Traversing all tables supporting versioning:
949 foreach ($TCA as $table => $cfg) {
950 if ($TCA[$table]['ctrl']['versioningWS'] && $table != 'pages') {
951 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('A.uid AS offlineUid, B.uid AS uid',
952 $table . ' A,' . $table . ' B',
953 'A.pid=-1 AND B.pid=' . $pageId . ' AND A.t3ver_wsid=' . $workspaceId .
954 ' AND B.uid=A.t3ver_oid' .
955 t3lib_BEfunc
::deleteClause($table, 'A') . t3lib_BEfunc
::deleteClause($table, 'B'));
956 while (FALSE != ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
957 $elementData[$table][] = array($row[1], $row[0]);
959 $GLOBALS['TYPO3_DB']->sql_free_result($res);
962 if ($offlinePageId && $offlinePageId != $pageId) {
963 $elementData['pages'][] = array($pageId, $offlinePageId);
970 * Searches for all elements from all tables on the given pages in the same workspace.
972 * @param array $pageIdList List of PIDs to search
973 * @param int $workspaceId Workspace ID
974 * @param array $elementList List of found elements. Key is table name, value is array of element UIDs
977 protected function findPageElementsForVersionStageChange($pageIdList, $workspaceId, &$elementList) {
980 if ($workspaceId != 0) {
981 // Traversing all tables supporting versioning:
982 foreach ($TCA as $table => $cfg) {
983 if ($TCA[$table]['ctrl']['versioningWS'] && $table != 'pages') {
984 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('DISTINCT A.uid',
985 $table . ' A,' . $table . ' B',
986 'A.pid=-1' . // Offline version
987 ' AND A.t3ver_wsid=' . $workspaceId .
988 ' AND B.pid IN (' . implode(',', $pageIdList) . ') AND A.t3ver_oid=B.uid' .
989 t3lib_BEfunc
::deleteClause($table,'A').
990 t3lib_BEfunc
::deleteClause($table,'B')
992 while (FALSE !== ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
993 $elementList[$table][] = $row[0];
995 $GLOBALS['TYPO3_DB']->sql_free_result($res);
996 if (is_array($elementList[$table])) {
997 // Yes, it is possible to get non-unique array even with DISTINCT above!
998 // It happens because several UIDs are passed in the array already.
999 $elementList[$table] = array_unique($elementList[$table]);
1008 * Finds page UIDs for the element from table <code>$table</code> with UIDs from <code>$idList</code>
1010 * @param array $table Table to search
1011 * @param array $idList List of records' UIDs
1012 * @param int $workspaceId Workspace ID. We need this parameter because user can be in LIVE but he still can publisg DRAFT from ws module!
1013 * @param array $pageIdList List of found page UIDs
1014 * @param array $elementList List of found element UIDs. Key is table name, value is list of UIDs
1017 protected function findPageIdsForVersionStateChange($table, $idList, $workspaceId, &$pageIdList, &$elementList) {
1018 if ($workspaceId != 0) {
1019 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('DISTINCT B.pid',
1020 $table . ' A,' . $table . ' B',
1021 'A.pid=-1' . // Offline version
1022 ' AND A.t3ver_wsid=' . $workspaceId .
1023 ' AND A.uid IN (' . implode(',', $idList) . ') AND A.t3ver_oid=B.uid' .
1024 t3lib_BEfunc
::deleteClause($table,'A').
1025 t3lib_BEfunc
::deleteClause($table,'B')
1027 while (FALSE !== ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
1028 $pageIdList[] = $row[0];
1030 // Note: cannot use t3lib_BEfunc::getRecordWSOL()
1031 // here because it does not accept workspace id!
1032 $rec = t3lib_BEfunc
::getRecord('pages', $row[0]);
1033 t3lib_BEfunc
::workspaceOL('pages', $rec, $workspaceId);
1034 if ($rec['_ORIG_uid']) {
1035 $elementList['pages'][$row[0]] = $rec['_ORIG_uid'];
1038 $GLOBALS['TYPO3_DB']->sql_free_result($res);
1039 // The line below is necessary even with DISTINCT
1040 // because several elements can be passed by caller
1041 $pageIdList = array_unique($pageIdList);
1047 * Finds real page IDs for state change.
1049 * @param array $idList List of page UIDs, possibly versioned
1052 protected function findRealPageIds(&$idList) {
1053 foreach ($idList as $key => $id) {
1054 $rec = t3lib_BEfunc
::getRecord('pages', $id, 't3ver_oid');
1055 if ($rec['t3ver_oid'] > 0) {
1056 $idList[$key] = $rec['t3ver_oid'];