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) {
76 // custom command "version"
77 if ($command == 'version') {
78 $commandWasProcessed = TRUE;
79 $action = (string) $value['action'];
83 // check if page / branch versioning is needed,
84 // or if "element" version can be used
86 if (isset($value['treeLevels'])) {
87 $versionizeTree = t3lib_div
::intInRange($value['treeLevels'], -1, 100);
89 if ($table == 'pages' && $versionizeTree >= 0) {
90 $this->versionizePages($id, $value['label'], $versionizeTree, $tcemainObj);
92 $tcemainObj->versionizeRecord($table, $id, $value['label']);
97 $swapMode = $tcemainObj->BE_USER
->getTSConfigVal('options.workspaces.swapMode');
98 $elementList = array();
99 if ($swapMode == 'any' ||
($swapMode == 'page' && $table == 'pages')) {
100 // check if we are allowed to do synchronios publish.
101 // We must have a single element in the cmdmap to be allowed
102 if (count($tcemainObj->cmdmap
) == 1 && count($tcemainObj->cmdmap
[$table]) == 1) {
103 $elementList = $this->findPageElementsForVersionSwap($table, $id, $value['swapWith']);
106 if (count($elementList) == 0) {
107 $elementList[$table][] = array($id, $value['swapWith']);
109 foreach ($elementList as $tbl => $idList) {
110 foreach ($idList as $idKey => $idSet) {
111 $this->version_swap($tbl, $idSet[0], $idSet[1], $value['swapIntoWS'], $tcemainObj);
117 $this->version_clearWSID($table, $id, FALSE, $tcemainObj);
121 $this->version_clearWSID($table, $id, TRUE, $tcemainObj);
125 $elementList = array();
126 $idList = $elementList[$table] = t3lib_div
::trimExplode(',', $id, 1);
127 $setStageMode = $tcemainObj->BE_USER
->getTSConfigVal('options.workspaces.changeStageMode');
128 if ($setStageMode == 'any' ||
$setStageMode == 'page') {
129 if (count($idList) == 1) {
130 $rec = t3lib_BEfunc
::getRecord($table, $idList[0], 't3ver_wsid');
131 $workspaceId = $rec['t3ver_wsid'];
133 $workspaceId = $tcemainObj->BE_USER
->workspace
;
135 if ($table !== 'pages') {
136 if ($setStageMode == 'any') {
137 // (1) Find page to change stage and (2)
138 // find other elements from the same ws to change stage
139 $pageIdList = array();
140 $this->findPageIdsForVersionStateChange($table, $idList, $workspaceId, $pageIdList, $elementList);
141 $this->findPageElementsForVersionStageChange($pageIdList, $workspaceId, $elementList);
144 // Find all elements from the same ws to change stage
145 $this->findRealPageIds($idList);
146 $this->findPageElementsForVersionStageChange($idList, $workspaceId, $elementList);
150 foreach ($elementList as $tbl => $elementIdList) {
151 foreach ($elementIdList as $elementId) {
152 $this->version_setStage($tbl, $elementId, $value['stageId'], ($value['comment'] ?
$value['comment'] : $this->generalComment
), TRUE, $tcemainObj);
161 * hook that is called AFTER all commands of the commandmap was
163 * @param $tcemainObj reference to the main tcemain object
166 public function processCmdmap_afterFinish(&$tcemainObj) {
167 // Empty accumulation array:
168 foreach ($this->notificationEmailInfo
as $notifItem) {
169 $this->notifyStageChange($notifItem['shared'][0], $notifItem['shared'][1], implode(', ', $notifItem['elements']), 0, $notifItem['shared'][2], $tcemainObj);
172 // Reset notification array
173 $this->notificationEmailInfo
= array();
178 * hook that is called AFTER all commands of the commandmap was
180 * @param $tcemainObj reference to the main tcemain object
183 public function processCmdmap_deleteAction($table, $id, $record, &$recordWasDeleted, &$tcemainObj) {
184 // only process the hook if it wasn't processed
185 // by someone else before
186 if (!$recordWasDeleted) {
187 $recordWasDeleted = TRUE;
188 $id = $record['uid'];
190 // For Live version, try if there is a workspace version because if so, rather "delete" that instead
191 // Look, if record is an offline version, then delete directly:
192 if ($record['pid'] != -1) {
193 if ($wsVersion = t3lib_BEfunc
::getWorkspaceVersionOfRecord($tcemainObj->BE_USER
->workspace
, $table, $id)) {
194 $record = $wsVersion;
195 $id = $record['uid'];
199 // Look, if record is an offline version, then delete directly:
200 if ($record['pid'] == -1) {
201 if ($TCA[$table]['ctrl']['versioningWS']) {
202 // In Live workspace, delete any. In other workspaces there must be match.
203 if ($tcemainObj->BE_USER
->workspace
== 0 ||
(int) $record['t3ver_wsid'] == $tcemainObj->BE_USER
->workspace
) {
204 $liveRec = t3lib_BEfunc
::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state');
206 // Delete those in WS 0 + if their live records state was not "Placeholder".
207 if ($record['t3ver_wsid']==0 ||
(int) $liveRec['t3ver_state'] <= 0) {
208 $tcemainObj->deleteEl($table, $id);
210 // If live record was placeholder (new/deleted), rather clear
211 // it from workspace (because it clears both version and placeholder).
212 $this->version_clearWSID($table, $id, FALSE, $tcemainObj);
214 } else $tcemainObj->newlog('Tried to delete record from another workspace',1);
215 } else $tcemainObj->newlog('Versioning not enabled for record with PID = -1!',2);
216 } elseif ($res = $tcemainObj->BE_USER
->workspaceAllowLiveRecordsInPID($record['pid'], $table)) {
217 // Look, if record is "online" or in a versionized branch, then delete directly.
219 $tcemainObj->deleteEl($table, $id);
221 $tcemainObj->newlog('Stage of root point did not allow for deletion',1);
223 } elseif ((int)$record['t3ver_state']===3) {
224 // Placeholders for moving operations are deletable directly.
226 // Get record which its a placeholder for and reset the t3ver_state of that:
227 if ($wsRec = t3lib_BEfunc
::getWorkspaceVersionOfRecord($record['t3ver_wsid'], $table, $record['t3ver_move_id'], 'uid')) {
228 // Clear the state flag of the workspace version of the record
229 // Setting placeholder state value for version (so it can know it is currently a new version...)
230 $updateFields = array(
233 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($wsRec['uid']), $updateFields);
235 $tcemainObj->deleteEl($table, $id);
237 // Otherwise, try to delete by versioning:
238 $tcemainObj->versionizeRecord($table, $id, 'DELETED!', TRUE);
239 $tcemainObj->deleteL10nOverlayRecords($table, $id);
245 /****************************
246 ***** Notifications ******
247 ****************************/
250 * Send an email notification to users in workspace
252 * @param array Workspace access array (from t3lib_userauthgroup::checkWorkspace())
253 * @param integer New Stage number: 0 = editing, 1= just ready for review, 10 = ready for publication, -1 = rejected!
254 * @param string Table name of element (or list of element names if $id is zero)
255 * @param integer Record uid of element (if zero, then $table is used as reference to element(s) alone)
256 * @param string User comment sent along with action
259 protected function notifyStageChange($stat, $stageId, $table, $id, $comment, $tcemainObj) {
260 $workspaceRec = t3lib_BEfunc
::getRecord('sys_workspace', $stat['uid']);
261 // So, if $id is not set, then $table is taken to be the complete element name!
262 $elementName = $id ?
$table . ':' . $id : $table;
264 if (is_array($workspaceRec)) {
267 switch ((int)$stageId) {
269 $newStage = 'Ready for review';
272 $newStage = 'Ready for publishing';
275 $newStage = 'Element was rejected!';
278 $newStage = 'Rejected element was noticed and edited';
281 $newStage = 'Unknown state change!?';
285 // Compile list of recipients:
287 switch((int)$stat['stagechg_notification']) {
289 switch((int)$stageId) {
291 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']);
294 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], TRUE);
297 # $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']);
298 # $emails = array_merge($emails,$this->getEmailsForStageChangeNotification($workspaceRec['members']));
300 // List of elements to reject:
301 $allElements = explode(',', $elementName);
302 // Traverse them, and find the history of each
303 foreach ($allElements as $elRef) {
304 list($eTable, $eUid) = explode(':', $elRef);
306 $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
307 'log_data,tstamp,userid',
309 'action=6 and details_nr=30
310 AND tablename=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($eTable, 'sys_log') . '
311 AND recuid=' . intval($eUid),
315 // Find all implicated since the last stage-raise from editing to review:
316 foreach ($rows as $dat) {
317 $data = unserialize($dat['log_data']);
319 $emails = array_merge($emails, $this->getEmailsForStageChangeNotification($dat['userid'], TRUE));
321 if ($data['stage'] == 1) {
329 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']);
333 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], TRUE);
339 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], TRUE);
340 $emails = array_merge($emails, $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']));
341 $emails = array_merge($emails, $this->getEmailsForStageChangeNotification($workspaceRec['members']));
344 $emails = array_unique($emails);
346 // Path to record is found:
347 list($eTable,$eUid) = explode(':', $elementName);
348 $eUid = intval($eUid);
349 $rr = t3lib_BEfunc
::getRecord($eTable, $eUid);
350 $recTitle = t3lib_BEfunc
::getRecordTitle($eTable, $rr);
351 if ($eTable != 'pages') {
352 t3lib_BEfunc
::fixVersioningPid($eTable, $rr);
355 $path = t3lib_BEfunc
::getRecordPath($eUid, '', 20);
357 // ALternative messages:
358 $TSConfig = $tcemainObj->getTCEMAIN_TSconfig($eUid);
359 $body = trim($TSConfig['notificationEmail_body']) ?
trim($TSConfig['notificationEmail_body']) : '
360 At the TYPO3 site "%s" (%s)
361 in workspace "%s" (#%s)
362 the stage has changed for the element(s) "%11$s" (%s) at location "%10$s" in the page tree:
369 State was change by %s (username: %s)
371 $subject = trim($TSConfig['notificationEmail_subject']) ?
trim($TSConfig['notificationEmail_subject']) : 'TYPO3 Workspace Note: Stage Change for %s';
374 if (count($emails)) {
375 $message = sprintf($body,
376 $GLOBALS['TYPO3_CONF_VARS']['SYS']['sitename'],
377 t3lib_div
::getIndpEnv('TYPO3_SITE_URL').TYPO3_mainDir
,
378 $workspaceRec['title'],
379 $workspaceRec['uid'],
383 $this->BE_USER
->user
['realName'],
384 $this->BE_USER
->user
['username'],
388 t3lib_div
::plainMailEncoded(
389 implode(',', $emails),
390 sprintf($subject, $elementName),
394 $tcemainObj->newlog2('Notification email for stage change was sent to "' . implode(', ', $emails) . '"', $table, $id);
400 * Return emails addresses of be_users from input list.
401 * previously called notifyStageChange_getEmails() in tcemain
403 * @param string List of backend users, on the form "be_users_10,be_users_2" or "10,2" in case noTablePrefix is set.
404 * @param boolean If set, the input list are integers and not strings.
405 * @return array Array of emails
407 protected function getEmailsForStageChangeNotification($listOfUsers, $noTablePrefix = FALSE) {
408 $users = t3lib_div
::trimExplode(',', $listOfUsers, 1);
410 foreach ($users as $userIdent) {
411 if ($noTablePrefix) {
412 $id = intval($userIdent);
414 list($table, $id) = t3lib_div
::revExplode('_', $userIdent, 2);
416 if ($table === 'be_users' ||
$noTablePrefix) {
417 if ($userRecord = t3lib_BEfunc
::getRecord('be_users', $id, 'email')) {
418 if (strlen(trim($userRecord['email']))) {
419 $emails[$id] = $userRecord['email'];
429 /****************************
430 ***** Stage Changes ******
431 ****************************/
434 * Setting stage of record
436 * @param string Table name
437 * @param integer Record UID
438 * @param integer Stage ID to set
439 * @param string Comment that goes into log
440 * @param boolean Accumulate state changes in memory for compiled notification email?
443 protected function version_setStage($table, $id, $stageId, $comment = '', $notificationEmailInfo = FALSE, $tcemainObj) {
444 if ($errorCode = $tcemainObj->BE_USER
->workspaceCannotEditOfflineVersion($table, $id)) {
445 $tcemainObj->newlog('Attempt to set stage for record failed: ' . $errorCode, 1);
446 } elseif ($tcemainObj->checkRecordUpdateAccess($table, $id)) {
447 $record = t3lib_BEfunc
::getRecord($table, $id);
448 $stat = $tcemainObj->BE_USER
->checkWorkspace($record['t3ver_wsid']);
450 if (t3lib_div
::inList('admin,online,offline,reviewer,owner', $stat['_ACCESS']) ||
($stageId <= 1 && $stat['_ACCESS'] === 'member')) {
452 // Set stage of record:
454 't3ver_stage' => $stageId
456 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($id), $updateData);
457 $tcemainObj->newlog2('Stage for record was changed to ' . $stageId . '. Comment was: "' . substr($comment, 0, 100) . '"', $table, $id);
459 // TEMPORARY, except 6-30 as action/detail number which is observed elsewhere!
460 $tcemainObj->log($table, $id, 6, 0, 0, 'Stage raised...', 30, array('comment' => $comment, 'stage' => $stageId));
462 if ((int)$stat['stagechg_notification'] > 0) {
463 if ($notificationEmailInfo) {
464 $this->notificationEmailInfo
[$stat['uid'] . ':' . $stageId . ':' . $comment]['shared'] = array($stat, $stageId, $comment);
465 $this->notificationEmailInfo
[$stat['uid'] . ':' . $stageId . ':' . $comment]['elements'][] = $table . ':' . $id;
467 $this->notifyStageChange($stat, $stageId, $table, $id, $comment, $tcemainObj);
470 } else $tcemainObj->newlog('The member user tried to set a stage value "' . $stageId . '" that was not allowed', 1);
471 } else $tcemainObj->newlog('Attempt to set stage for record failed because you do not have edit access', 1);
476 /*****************************
477 ***** CMD versioning ******
478 *****************************/
481 * Creates a new version of a page including content and possible subpages.
483 * @param integer Page uid to create new version of.
484 * @param string Version label
485 * @param integer Indicating "treeLevel" - "page" (0) or "branch" (>=1) ["element" type must call versionizeRecord() directly]
489 protected function versionizePages($uid, $label, $versionizeTree, &$tcemainObj) {
493 // returns the branch
494 $brExist = $tcemainObj->doesBranchExist('', $uid, $tcemainObj->pMap
['show'], 1);
496 // Checks if we had permissions
497 if ($brExist != -1) {
499 // Make list of tables that should come along with a new version of the page:
500 $verTablesArray = array();
501 $allTables = array_keys($TCA);
502 foreach ($allTables as $tableName) {
503 if ($tableName != 'pages' && ($versionizeTree > 0 ||
$TCA[$tableName]['ctrl']['versioning_followPages'])) {
504 $verTablesArray[] = $tableName;
508 // Begin to copy pages if we're allowed to:
509 if ($tcemainObj->BE_USER
->workspaceVersioningTypeAccess($versionizeTree)) {
511 // Versionize this page:
512 $theNewRootID = $tcemainObj->versionizeRecord('pages', $uid, $label, FALSE, $versionizeTree);
514 $tcemainObj->rawCopyPageContent($uid, $theNewRootID, $verTablesArray, $tcemainObj);
516 // If we're going to copy recursively...:
517 if ($versionizeTree > 0) {
519 // Get ALL subpages to copy (read permissions respected - they should NOT be...):
520 $CPtable = $tcemainObj->int_pageTreeInfo(array(), $uid, intval($versionizeTree), $theNewRootID);
522 // Now copying the subpages
523 foreach ($CPtable as $thePageUid => $thePagePid) {
524 $newPid = $tcemainObj->copyMappingArray
['pages'][$thePagePid];
525 if (isset($newPid)) {
526 $theNewRootID = $tcemainObj->copyRecord_raw('pages', $thePageUid, $newPid);
527 $tcemainObj->rawCopyPageContent($thePageUid, $theNewRootID, $verTablesArray, $tcemainObj);
529 $tcemainObj->newlog('Something went wrong during copying branch (for versioning)', 1);
533 } // else the page was not copied. Too bad...
534 } else $tcemainObj->newlog('The root version could not be created!',1);
535 } else $tcemainObj->newlog('Versioning type "'.$versionizeTree.'" was not allowed in workspace',1);
536 } else $tcemainObj->newlog('Could not read all subpages to versionize.',1);
541 * Swapping versions of a record
542 * 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
544 * @param string Table name
545 * @param integer UID of the online record to swap
546 * @param integer UID of the archived version to swap with!
547 * @param boolean If set, swaps online into workspace instead of publishing out of workspace.
550 protected function version_swap($table, $id, $swapWith, $swapIntoWS=0, &$tcemainObj) {
553 // First, check if we may actually edit the online record
554 if ($tcemainObj->checkRecordUpdateAccess($table, $id)) {
556 // Select the two versions:
557 $curVersion = t3lib_BEfunc
::getRecord($table, $id, '*');
558 $swapVersion = t3lib_BEfunc
::getRecord($table, $swapWith, '*');
562 if (is_array($curVersion) && is_array($swapVersion)) {
563 if ($tcemainObj->BE_USER
->workspacePublishAccess($swapVersion['t3ver_wsid'])) {
564 $wsAccess = $tcemainObj->BE_USER
->checkWorkspace($swapVersion['t3ver_wsid']);
565 if ($swapVersion['t3ver_wsid'] <= 0 ||
!($wsAccess['publish_access'] & 1) ||
(int)$swapVersion['t3ver_stage'] === 10) {
566 if ($tcemainObj->doesRecordExist($table,$swapWith,'show') && $tcemainObj->checkRecordUpdateAccess($table,$swapWith)) {
567 if (!$swapIntoWS ||
$tcemainObj->BE_USER
->workspaceSwapAccess()) {
569 // Check if the swapWith record really IS a version of the original!
570 if ((int)$swapVersion['pid'] == -1 && (int)$curVersion['pid'] >= 0 && !strcmp($swapVersion['t3ver_oid'], $id)) {
573 $lockFileName = PATH_site
.'typo3temp/swap_locking/' . $table . ':' . $id . '.ser';
575 if (!@is_file
($lockFileName)) {
578 t3lib_div
::writeFileToTypo3tempDir($lockFileName, serialize(array(
579 'tstamp' => $GLOBALS['EXEC_TIME'],
580 'user' => $tcemainObj->BE_USER
->user
['username'],
581 'curVersion' => $curVersion,
582 'swapVersion' => $swapVersion
585 // Find fields to keep
586 $keepFields = $tcemainObj->getUniqueFields($table);
587 if ($TCA[$table]['ctrl']['sortby']) {
588 $keepFields[] = $TCA[$table]['ctrl']['sortby'];
590 // l10n-fields must be kept otherwise the localization
591 // will be lost during the publishing
592 if (!isset($TCA[$table]['ctrl']['transOrigPointerTable']) && $TCA[$table]['ctrl']['transOrigPointerField']) {
593 $keepFields[] = $TCA[$table]['ctrl']['transOrigPointerField'];
597 foreach ($keepFields as $fN) {
598 $tmp = $swapVersion[$fN];
599 $swapVersion[$fN] = $curVersion[$fN];
600 $curVersion[$fN] = $tmp;
604 $t3ver_state = array();
605 $t3ver_state['swapVersion'] = $swapVersion['t3ver_state'];
606 $t3ver_state['curVersion'] = $curVersion['t3ver_state'];
608 // Modify offline version to become online:
609 $tmp_wsid = $swapVersion['t3ver_wsid'];
610 // Set pid for ONLINE
611 $swapVersion['pid'] = intval($curVersion['pid']);
612 // We clear this because t3ver_oid only make sense for offline versions
613 // and we want to prevent unintentional misuse of this
614 // value for online records.
615 $swapVersion['t3ver_oid'] = 0;
616 // In case of swapping and the offline record has a state
617 // (like 2 or 4 for deleting or move-pointer) we set the
618 // current workspace ID so the record is not deselected
619 // in the interface by t3lib_BEfunc::versioningPlaceholderClause()
620 $swapVersion['t3ver_wsid'] = 0;
622 if ($t3ver_state['swapVersion'] > 0) {
623 $swapVersion['t3ver_wsid'] = $tcemainObj->BE_USER
->workspace
;
625 $swapVersion['t3ver_wsid'] = intval($curVersion['t3ver_wsid']);
628 $swapVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
629 $swapVersion['t3ver_stage'] = 0;
631 $swapVersion['t3ver_state'] = 0;
635 if ((int)$TCA[$table]['ctrl']['versioningWS']>=2) { // && $t3ver_state['swapVersion']==4 // Maybe we don't need this?
636 if ($plhRec = t3lib_BEfunc
::getMovePlaceholder($table, $id, 't3ver_state,pid,uid' . ($TCA[$table]['ctrl']['sortby'] ?
',' . $TCA[$table]['ctrl']['sortby'] : ''))) {
637 $movePlhID = $plhRec['uid'];
638 $movePlh['pid'] = $swapVersion['pid'];
639 $swapVersion['pid'] = intval($plhRec['pid']);
641 $curVersion['t3ver_state'] = intval($swapVersion['t3ver_state']);
642 $swapVersion['t3ver_state'] = 0;
644 if ($TCA[$table]['ctrl']['sortby']) {
645 // sortby is a "keepFields" which is why this will work...
646 $movePlh[$TCA[$table]['ctrl']['sortby']] = $swapVersion[$TCA[$table]['ctrl']['sortby']];
647 $swapVersion[$TCA[$table]['ctrl']['sortby']] = $plhRec[$TCA[$table]['ctrl']['sortby']];
652 // Take care of relations in each field (e.g. IRRE):
653 if (is_array($GLOBALS['TCA'][$table]['columns'])) {
654 foreach ($GLOBALS['TCA'][$table]['columns'] as $field => $fieldConf) {
655 $this->version_swap_procBasedOnFieldType(
656 $table, $field, $fieldConf['config'], $curVersion, $swapVersion, $tcemainObj
660 unset($swapVersion['uid']);
662 // Modify online version to become offline:
663 unset($curVersion['uid']);
664 // Set pid for OFFLINE
665 $curVersion['pid'] = -1;
666 $curVersion['t3ver_oid'] = intval($id);
667 $curVersion['t3ver_wsid'] = ($swapIntoWS ?
intval($tmp_wsid) : 0);
668 $curVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
669 $curVersion['t3ver_count'] = $curVersion['t3ver_count']+
1; // Increment lifecycle counter
670 $curVersion['t3ver_stage'] = 0;
672 $curVersion['t3ver_state'] = 0;
675 // Keeping the swapmode state
676 if ($table === 'pages') {
677 $curVersion['t3ver_swapmode'] = $swapVersion['t3ver_swapmode'];
680 // Registering and swapping MM relations in current and swap records:
681 $tcemainObj->version_remapMMForVersionSwap($table, $id, $swapWith);
683 // Generating proper history data to prepare logging
684 $tcemainObj->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion);
685 $tcemainObj->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion);
688 $sqlErrors = array();
689 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($id), $swapVersion);
690 if ($GLOBALS['TYPO3_DB']->sql_error()) {
691 $sqlErrors[] = $GLOBALS['TYPO3_DB']->sql_error();
693 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($swapWith), $curVersion);
694 if ($GLOBALS['TYPO3_DB']->sql_error()) {
695 $sqlErrors[] = $GLOBALS['TYPO3_DB']->sql_error();
697 unlink($lockFileName);
701 if (!count($sqlErrors)) {
703 // If a moving operation took place...:
705 // Remove, if normal publishing:
707 // For delete + completely delete!
708 $tcemainObj->deleteEl($table, $movePlhID, TRUE, TRUE);
710 // Otherwise update the movePlaceholder:
711 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($movePlhID), $movePlh);
712 $tcemainObj->updateRefIndex($table, $movePlhID);
716 // Checking for delete:
717 // Delete only if new/deleted placeholders are there.
718 if (!$swapIntoWS && ((int)$t3ver_state['swapVersion'] === 1 ||
(int)$t3ver_state['swapVersion'] === 2)) {
720 $tcemainObj->deleteEl($table, $id, TRUE);
723 $tcemainObj->newlog2(($swapIntoWS ?
'Swapping' : 'Publishing') . ' successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, $table, $id, $swapVersion['pid']);
725 // Update reference index of the live record:
726 $tcemainObj->updateRefIndex($table, $id);
728 // Set log entry for live record:
729 $propArr = $tcemainObj->getRecordPropertiesFromRow($table, $swapVersion);
730 if ($propArr['_ORIG_pid'] == -1) {
731 $label = $GLOBALS['LANG']->sL ('LLL:EXT:lang/locallang_tcemain.xml:version_swap.offline_record_updated');
733 $label = $GLOBALS['LANG']->sL ('LLL:EXT:lang/locallang_tcemain.xml:version_swap.online_record_updated');
735 $theLogId = $tcemainObj->log($table, $id, 2, $propArr['pid'], 0, $label , 10, array($propArr['header'], $table . ':' . $id), $propArr['event_pid']);
736 $tcemainObj->setHistory($table, $id, $theLogId);
738 // Update reference index of the offline record:
739 $tcemainObj->updateRefIndex($table, $swapWith);
740 // Set log entry for offline record:
741 $propArr = $tcemainObj->getRecordPropertiesFromRow($table, $curVersion);
742 if ($propArr['_ORIG_pid'] == -1) {
743 $label = $GLOBALS['LANG']->sL ('LLL:EXT:lang/locallang_tcemain.xml:version_swap.offline_record_updated');
745 $label = $GLOBALS['LANG']->sL ('LLL:EXT:lang/locallang_tcemain.xml:version_swap.online_record_updated');
747 $theLogId = $tcemainObj->log($table, $swapWith, 2, $propArr['pid'], 0, $label, 10, array($propArr['header'], $table . ':' . $swapWith), $propArr['event_pid']);
748 $tcemainObj->setHistory($table, $swapWith, $theLogId);
750 // SWAPPING pids for subrecords:
751 if ($table=='pages' && $swapVersion['t3ver_swapmode'] >= 0) {
753 // Collect table names that should be copied along with the tables:
754 foreach ($TCA as $tN => $tCfg) {
755 // For "Branch" publishing swap ALL,
756 // otherwise for "page" publishing, swap only "versioning_followPages" tables
757 if ($swapVersion['t3ver_swapmode'] > 0 ||
$TCA[$tN]['ctrl']['versioning_followPages']) {
758 $temporaryPid = -($id+
1000000);
760 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($tN, 'pid=' . intval($id), array('pid' => $temporaryPid));
761 if ($GLOBALS['TYPO3_DB']->sql_error()) {
762 $sqlErrors[] = $GLOBALS['TYPO3_DB']->sql_error();
765 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($tN, 'pid=' . intval($swapWith), array('pid' => $id));
766 if ($GLOBALS['TYPO3_DB']->sql_error()) {
767 $sqlErrors[] = $GLOBALS['TYPO3_DB']->sql_error();
770 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($tN, 'pid=' . intval($temporaryPid), array('pid' => $swapWith));
771 if ($GLOBALS['TYPO3_DB']->sql_error()) {
772 $sqlErrors[] = $GLOBALS['TYPO3_DB']->sql_error();
775 if (count($sqlErrors)) {
776 $tcemainObj->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), 2);
782 $tcemainObj->clear_cache($table, $id);
784 // Checking for "new-placeholder" and if found, delete it (BUT FIRST after swapping!):
785 if (!$swapIntoWS && $t3ver_state['curVersion']>0) {
786 // For delete + completely delete!
787 $tcemainObj->deleteEl($table, $swapWith, TRUE, TRUE);
789 } else $tcemainObj->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), 2);
790 } 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);
791 } 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);
792 } else $tcemainObj->newlog('Workspace #' . $swapVersion['t3ver_wsid'] . ' does not support swapping.', 1);
793 } else $tcemainObj->newlog('You cannot publish a record you do not have edit and show permissions for', 1);
794 } else $tcemainObj->newlog('Records in workspace #' . $swapVersion['t3ver_wsid'] . ' can only be published when in "Publish" stage.', 1);
795 } else $tcemainObj->newlog('User could not publish records from workspace #' . $swapVersion['t3ver_wsid'], 1);
796 } else $tcemainObj->newlog('Error: Either online or swap version could not be selected!', 2);
797 } else $tcemainObj->newlog('Error: You cannot swap versions for a record you do not have access to edit!', 1);
802 * Update relations on version/workspace swapping.
804 * @param string $table: Record Table
805 * @param string $field: Record field
806 * @param array $conf: TCA configuration of current field
807 * @param string $curVersion: Reference to the current (original) record
808 * @param string $swapVersion: Reference to the record (workspace/versionized) to publish in or swap with
811 protected function version_swap_procBasedOnFieldType($table, $field, $conf, &$curVersion, &$swapVersion, $tcemainObj) {
812 $inlineType = $tcemainObj->getInlineFieldType($conf);
814 // Process pointer fields on normalized database:
815 if ($inlineType == 'field') {
816 // Read relations that point to the current record (e.g. live record):
817 $dbAnalysisCur = t3lib_div
::makeInstance('t3lib_loadDBGroup');
818 $dbAnalysisCur->start('', $conf['foreign_table'], '', $curVersion['uid'], $table, $conf);
819 // Read relations that point to the record to be swapped with e.g. draft record):
820 $dbAnalysisSwap = t3lib_div
::makeInstance('t3lib_loadDBGroup');
821 $dbAnalysisSwap->start('', $conf['foreign_table'], '', $swapVersion['uid'], $table, $conf);
822 // Update relations for both (workspace/versioning) sites:
823 $dbAnalysisCur->writeForeignField($conf, $curVersion['uid'], $swapVersion['uid']);
824 $dbAnalysisSwap->writeForeignField($conf, $swapVersion['uid'], $curVersion['uid']);
826 // Swap field values (CSV):
827 // BUT: These values will be swapped back in the next steps, when the *CHILD RECORD ITSELF* is swapped!
828 } elseif ($inlineType == 'list') {
829 $tempValue = $curVersion[$field];
830 $curVersion[$field] = $swapVersion[$field];
831 $swapVersion[$field] = $tempValue;
838 * Release version from this workspace (and into "Live" workspace but as an offline version).
840 * @param string Table name
841 * @param integer Record UID
842 * @param boolean If set, will completely delete element
845 protected function version_clearWSID($table, $id, $flush = FALSE, &$tcemainObj) {
848 if ($errorCode = $tcemainObj->BE_USER
->workspaceCannotEditOfflineVersion($table, $id)) {
849 $tcemainObj->newlog('Attempt to reset workspace for record failed: ' . $errorCode, 1);
850 } elseif ($tcemainObj->checkRecordUpdateAccess($table, $id)) {
851 if ($liveRec = t3lib_BEfunc
::getLiveVersionOfRecord($table, $id, 'uid,t3ver_state')) {
852 // Clear workspace ID:
856 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($id), $updateData);
858 // Clear workspace ID for live version AND DELETE IT as well because it is a new record!
859 if ((int) $liveRec['t3ver_state'] == 1 ||
(int) $liveRec['t3ver_state'] == 2) {
860 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table,'uid=' . intval($liveRec['uid']), $updateData);
861 // THIS assumes that the record was placeholder ONLY for ONE record (namely $id)
862 $tcemainObj->deleteEl($table, $liveRec['uid'], TRUE);
865 // If "deleted" flag is set for the version that got released
866 // it doesn't make sense to keep that "placeholder" anymore and we delete it completly.
867 $wsRec = t3lib_BEfunc
::getRecord($table, $id);
868 if ($flush ||
((int) $wsRec['t3ver_state'] == 1 ||
(int) $wsRec['t3ver_state'] == 2)) {
869 $tcemainObj->deleteEl($table, $id, TRUE, TRUE);
872 // Remove the move-placeholder if found for live record.
873 if ((int)$TCA[$table]['ctrl']['versioningWS'] >= 2) {
874 if ($plhRec = t3lib_BEfunc
::getMovePlaceholder($table, $liveRec['uid'], 'uid')) {
875 $tcemainObj->deleteEl($table, $plhRec['uid'], TRUE, TRUE);
879 } else $tcemainObj->newlog('Attempt to reset workspace for record failed because you do not have edit access',1);
883 /*******************************
884 ***** helper functions ******
885 *******************************/
889 * Copies all records from tables in $copyTablesArray from page with $old_pid to page with $new_pid
890 * Uses raw-copy for the operation (meant for versioning!)
892 * @param integer Current page id.
893 * @param integer New page id
894 * @param array Array of tables from which to copy
896 * @see versionizePages()
898 protected function rawCopyPageContent($oldPageId, $newPageId, $copyTablesArray, &$tcemainObj) {
902 foreach ($copyTablesArray as $table) {
903 // all records under the page is copied.
904 if ($table && is_array($TCA[$table]) && $table != 'pages') {
905 $mres = $GLOBALS['TYPO3_DB']->exec_SELECTquery(
908 'pid=' . intval($oldPageId) . $tcemainObj->deleteClause($table)
910 while ($row = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($mres)) {
911 // Check, if this record has already been copied by a parent record as relation:
912 if (!$this->copyMappingArray
[$table][$row['uid']]) {
913 // Copying each of the underlying records (method RAW)
914 $tcemainObj->copyRecord_raw($table, $row['uid'], $newPageId);
917 $GLOBALS['TYPO3_DB']->sql_free_result($mres);
925 * Finds all elements for swapping versions in workspace
927 * @param string $table Table name of the original element to swap
928 * @param int $id UID of the original element to swap (online)
929 * @param int $offlineId As above but offline
930 * @return array Element data. Key is table name, values are array with first element as online UID, second - offline UID
932 protected function findPageElementsForVersionSwap($table, $id, $offlineId) {
935 $rec = t3lib_BEfunc
::getRecord($table, $offlineId, 't3ver_wsid');
936 $workspaceId = $rec['t3ver_wsid'];
938 $elementData = array();
939 if ($workspaceId != 0) {
940 // Get page UID for LIVE and workspace
941 if ($table != 'pages') {
942 $rec = t3lib_BEfunc
::getRecord($table, $id, 'pid');
943 $pageId = $rec['pid'];
944 $rec = t3lib_BEfunc
::getRecord('pages', $pageId);
945 t3lib_BEfunc
::workspaceOL('pages', $rec, $workspaceId);
946 $offlinePageId = $rec['_ORIG_uid'];
949 $offlinePageId = $offlineId;
952 // Traversing all tables supporting versioning:
953 foreach ($TCA as $table => $cfg) {
954 if ($TCA[$table]['ctrl']['versioningWS'] && $table != 'pages') {
955 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('A.uid AS offlineUid, B.uid AS uid',
956 $table . ' A,' . $table . ' B',
957 'A.pid=-1 AND B.pid=' . $pageId . ' AND A.t3ver_wsid=' . $workspaceId .
958 ' AND B.uid=A.t3ver_oid' .
959 t3lib_BEfunc
::deleteClause($table, 'A') . t3lib_BEfunc
::deleteClause($table, 'B'));
960 while (FALSE != ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
961 $elementData[$table][] = array($row[1], $row[0]);
963 $GLOBALS['TYPO3_DB']->sql_free_result($res);
966 if ($offlinePageId && $offlinePageId != $pageId) {
967 $elementData['pages'][] = array($pageId, $offlinePageId);
974 * Searches for all elements from all tables on the given pages in the same workspace.
976 * @param array $pageIdList List of PIDs to search
977 * @param int $workspaceId Workspace ID
978 * @param array $elementList List of found elements. Key is table name, value is array of element UIDs
981 protected function findPageElementsForVersionStageChange($pageIdList, $workspaceId, &$elementList) {
984 if ($workspaceId != 0) {
985 // Traversing all tables supporting versioning:
986 foreach ($TCA as $table => $cfg) {
987 if ($TCA[$table]['ctrl']['versioningWS'] && $table != 'pages') {
988 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('DISTINCT A.uid',
989 $table . ' A,' . $table . ' B',
990 'A.pid=-1' . // Offline version
991 ' AND A.t3ver_wsid=' . $workspaceId .
992 ' AND B.pid IN (' . implode(',', $pageIdList) . ') AND A.t3ver_oid=B.uid' .
993 t3lib_BEfunc
::deleteClause($table,'A').
994 t3lib_BEfunc
::deleteClause($table,'B')
996 while (FALSE !== ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
997 $elementList[$table][] = $row[0];
999 $GLOBALS['TYPO3_DB']->sql_free_result($res);
1000 if (is_array($elementList[$table])) {
1001 // Yes, it is possible to get non-unique array even with DISTINCT above!
1002 // It happens because several UIDs are passed in the array already.
1003 $elementList[$table] = array_unique($elementList[$table]);
1012 * Finds page UIDs for the element from table <code>$table</code> with UIDs from <code>$idList</code>
1014 * @param array $table Table to search
1015 * @param array $idList List of records' UIDs
1016 * @param int $workspaceId Workspace ID. We need this parameter because user can be in LIVE but he still can publisg DRAFT from ws module!
1017 * @param array $pageIdList List of found page UIDs
1018 * @param array $elementList List of found element UIDs. Key is table name, value is list of UIDs
1021 protected function findPageIdsForVersionStateChange($table, $idList, $workspaceId, &$pageIdList, &$elementList) {
1022 if ($workspaceId != 0) {
1023 $res = $GLOBALS['TYPO3_DB']->exec_SELECTquery('DISTINCT B.pid',
1024 $table . ' A,' . $table . ' B',
1025 'A.pid=-1' . // Offline version
1026 ' AND A.t3ver_wsid=' . $workspaceId .
1027 ' AND A.uid IN (' . implode(',', $idList) . ') AND A.t3ver_oid=B.uid' .
1028 t3lib_BEfunc
::deleteClause($table,'A').
1029 t3lib_BEfunc
::deleteClause($table,'B')
1031 while (FALSE !== ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
1032 $pageIdList[] = $row[0];
1034 // Note: cannot use t3lib_BEfunc::getRecordWSOL()
1035 // here because it does not accept workspace id!
1036 $rec = t3lib_BEfunc
::getRecord('pages', $row[0]);
1037 t3lib_BEfunc
::workspaceOL('pages', $rec, $workspaceId);
1038 if ($rec['_ORIG_uid']) {
1039 $elementList['pages'][$row[0]] = $rec['_ORIG_uid'];
1042 $GLOBALS['TYPO3_DB']->sql_free_result($res);
1043 // The line below is necessary even with DISTINCT
1044 // because several elements can be passed by caller
1045 $pageIdList = array_unique($pageIdList);
1051 * Finds real page IDs for state change.
1053 * @param array $idList List of page UIDs, possibly versioned
1056 protected function findRealPageIds(&$idList) {
1057 foreach ($idList as $key => $id) {
1058 $rec = t3lib_BEfunc
::getRecord('pages', $id, 't3ver_oid');
1059 if ($rec['t3ver_oid'] > 0) {
1060 $idList[$key] = $rec['t3ver_oid'];