Fixed bug #15740: Move versioning code from TCEmain into "version" extension, adding...
[Packages/TYPO3.CMS.git] / typo3 / sysext / version / class.tx_version_tcemain.php
1 <?php
2 /***************************************************************
3 * Copyright notice
4 *
5 * (c) 1999-2010 Kasper Skårhøj (kasperYYYY@typo3.com)
6 * (c) 2010 Benjamin Mack (benni@typo3.org)
7 *
8 * All rights reserved
9 *
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.
15 *
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.
20 *
21 *
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.
26 *
27 * This copyright notice MUST APPEAR in all copies of the script!
28 ***************************************************************/
29 /**
30 *
31 * Contains some parts for staging, versioning and workspaces
32 * to interact with the TYPO3 Core Engine
33 *
34 */
35 class tx_version_tcemain {
36
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();
41
42 // general comment, eg. for staging in workspaces.
43 protected $generalComment = '';
44
45
46 /****************************
47 ***** Cmdmap Hooks ******
48 ****************************/
49
50 /**
51 * hook that is called before any cmd of the commandmap is
52 * executed
53 * @param $tcemainObj reference to the main tcemain object
54 * @return void
55 */
56 public function processCmdmap_beforeStart(&$tcemainObj) {
57 // Reset notification array
58 $this->notificationEmailInfo = array();
59 }
60
61
62 /**
63 * hook that is called when no prepared command was found
64 *
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
72 * @return void
73 */
74 public function processCmdmap($command, $table, $id, $value, &$commandIsProcessed, &$tcemainObj) {
75
76
77 // custom command "version"
78 if ($command == 'version') {
79 $commandWasProcessed = TRUE;
80 $action = (string) $value['action'];
81 switch ($action) {
82
83 case 'new':
84 // check if page / branch versioning is needed,
85 // or if "element" version can be used
86 $versionizeTree = -1;
87 if (isset($value['treeLevels'])) {
88 $versionizeTree = t3lib_div::intInRange($value['treeLevels'], -1, 100);
89 }
90 if ($table == 'pages' && $versionizeTree >= 0) {
91 $this->versionizePages($id, $value['label'], $versionizeTree, $tcemainObj);
92 } else {
93 $tcemainObj->versionizeRecord($table, $id, $value['label']);
94 }
95 break;
96
97 case 'swap':
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']);
105 }
106 }
107 if (count($elementList) == 0) {
108 $elementList[$table][] = array($id, $value['swapWith']);
109 }
110 foreach ($elementList as $tbl => $idList) {
111 foreach ($idList as $idKey => $idSet) {
112 $this->version_swap($tbl, $idSet[0], $idSet[1], $value['swapIntoWS'], $tcemainObj);
113 }
114 }
115 break;
116
117 case 'clearWSID':
118 $this->version_clearWSID($table, $id, FALSE, $tcemainObj);
119 break;
120
121 case 'flush':
122 $this->version_clearWSID($table, $id, TRUE, $tcemainObj);
123 break;
124
125 case 'setStage':
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'];
133 } else {
134 $workspaceId = $tcemainObj->BE_USER->workspace;
135 }
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);
143 }
144 } else {
145 // Find all elements from the same ws to change stage
146 $this->findRealPageIds($idList);
147 $this->findPageElementsForVersionStageChange($idList, $workspaceId, $elementList);
148 }
149 }
150
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);
154 }
155 }
156 break;
157 }
158 }
159 }
160
161 /**
162 * hook that is called AFTER all commands of the commandmap was
163 * executed
164 * @param $tcemainObj reference to the main tcemain object
165 * @return void
166 */
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);
171 }
172
173 // Reset notification array
174 $this->notificationEmailInfo = array();
175 }
176
177
178 /**
179 * hook that is called AFTER all commands of the commandmap was
180 * executed
181 * @param $tcemainObj reference to the main tcemain object
182 * @return void
183 */
184 public function processCmdmap_deleteAction($table, $id, $record, &$recordWasDeleted, &$tcemainObj) {
185 $id = $record['uid'];
186
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'];
193 }
194 }
195
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');
202
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);
206 } else {
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);
210 }
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.
215 if ($res>0) {
216 $tcemainObj->deleteEl($table, $id);
217 } else {
218 $tcemainObj->newlog('Stage of root point did not allow for deletion',1);
219 }
220 } elseif ((int)$record['t3ver_state']===3) {
221 // Placeholders for moving operations are deletable directly.
222
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(
228 't3ver_state' => 0
229 );
230 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($wsRec['uid']), $updateFields);
231 }
232 $tcemainObj->deleteEl($table, $id);
233 } else {
234 // Otherwise, try to delete by versioning:
235 $tcemainObj->versionizeRecord($table, $id, 'DELETED!', TRUE);
236 $tcemainObj->deleteL10nOverlayRecords($table, $id);
237 }
238 }
239
240
241 /****************************
242 ***** Notifications ******
243 ****************************/
244
245 /**
246 * Send an email notification to users in workspace
247 *
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
253 * @return void
254 */
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;
259
260 if (is_array($workspaceRec)) {
261
262 // Compile label:
263 switch ((int)$stageId) {
264 case 1:
265 $newStage = 'Ready for review';
266 break;
267 case 10:
268 $newStage = 'Ready for publishing';
269 break;
270 case -1:
271 $newStage = 'Element was rejected!';
272 break;
273 case 0:
274 $newStage = 'Rejected element was noticed and edited';
275 break;
276 default:
277 $newStage = 'Unknown state change!?';
278 break;
279 }
280
281 // Compile list of recipients:
282 $emails = array();
283 switch((int)$stat['stagechg_notification']) {
284 case 1:
285 switch((int)$stageId) {
286 case 1:
287 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']);
288 break;
289 case 10:
290 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], TRUE);
291 break;
292 case -1:
293 # $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']);
294 # $emails = array_merge($emails,$this->getEmailsForStageChangeNotification($workspaceRec['members']));
295
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);
301
302 $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
303 'log_data,tstamp,userid',
304 'sys_log',
305 'action=6 and details_nr=30
306 AND tablename=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($eTable, 'sys_log') . '
307 AND recuid=' . intval($eUid),
308 '',
309 'uid DESC'
310 );
311 // Find all implicated since the last stage-raise from editing to review:
312 foreach ($rows as $dat) {
313 $data = unserialize($dat['log_data']);
314
315 $emails = array_merge($emails, $this->getEmailsForStageChangeNotification($dat['userid'], TRUE));
316
317 if ($data['stage'] == 1) {
318 break;
319 }
320 }
321 }
322 break;
323
324 case 0:
325 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']);
326 break;
327
328 default:
329 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], TRUE);
330 break;
331 }
332 break;
333
334 case 10:
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']));
338 break;
339 }
340 $emails = array_unique($emails);
341
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);
349 $eUid = $rr['pid'];
350 }
351 $path = t3lib_BEfunc::getRecordPath($eUid, '', 20);
352
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:
359
360 ==> %s
361
362 User Comment:
363 "%s"
364
365 State was change by %s (username: %s)
366 ';
367 $subject = trim($TSConfig['notificationEmail_subject']) ? trim($TSConfig['notificationEmail_subject']) : 'TYPO3 Workspace Note: Stage Change for %s';
368
369 // Send email:
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'],
376 $elementName,
377 $newStage,
378 $comment,
379 $this->BE_USER->user['realName'],
380 $this->BE_USER->user['username'],
381 $path,
382 $recTitle);
383
384 t3lib_div::plainMailEncoded(
385 implode(',', $emails),
386 sprintf($subject, $elementName),
387 trim($message)
388 );
389
390 $tcemainObj->newlog2('Notification email for stage change was sent to "' . implode(', ', $emails) . '"', $table, $id);
391 }
392 }
393 }
394
395 /**
396 * Return emails addresses of be_users from input list.
397 * previously called notifyStageChange_getEmails() in tcemain
398 *
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
402 */
403 protected function getEmailsForStageChangeNotification($listOfUsers, $noTablePrefix = FALSE) {
404 $users = t3lib_div::trimExplode(',', $listOfUsers, 1);
405 $emails = array();
406 foreach ($users as $userIdent) {
407 if ($noTablePrefix) {
408 $id = intval($userIdent);
409 } else {
410 list($table, $id) = t3lib_div::revExplode('_', $userIdent, 2);
411 }
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'];
416 }
417 }
418 }
419 }
420 return $emails;
421 }
422
423
424
425 /****************************
426 ***** Stage Changes ******
427 ****************************/
428
429 /**
430 * Setting stage of record
431 *
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?
437 * @return void
438 */
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']);
445
446 if (t3lib_div::inList('admin,online,offline,reviewer,owner', $stat['_ACCESS']) || ($stageId <= 1 && $stat['_ACCESS'] === 'member')) {
447
448 // Set stage of record:
449 $updateData = array(
450 't3ver_stage' => $stageId
451 );
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);
454
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));
457
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;
462 } else {
463 $this->notifyStageChange($stat, $stageId, $table, $id, $comment, $tcemainObj);
464 }
465 }
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);
468 }
469
470
471
472 /*****************************
473 ***** CMD versioning ******
474 *****************************/
475
476 /**
477 * Creates a new version of a page including content and possible subpages.
478 *
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]
482 * @return void
483 * @see copyPages()
484 */
485 protected function versionizePages($uid, $label, $versionizeTree, &$tcemainObj) {
486 global $TCA;
487
488 $uid = intval($uid);
489 // returns the branch
490 $brExist = $tcemainObj->doesBranchExist('', $uid, $tcemainObj->pMap['show'], 1);
491
492 // Checks if we had permissions
493 if ($brExist != -1) {
494
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;
501 }
502 }
503
504 // Begin to copy pages if we're allowed to:
505 if ($tcemainObj->BE_USER->workspaceVersioningTypeAccess($versionizeTree)) {
506
507 // Versionize this page:
508 $theNewRootID = $tcemainObj->versionizeRecord('pages', $uid, $label, FALSE, $versionizeTree);
509 if ($theNewRootID) {
510 $tcemainObj->rawCopyPageContent($uid, $theNewRootID, $verTablesArray, $tcemainObj);
511
512 // If we're going to copy recursively...:
513 if ($versionizeTree > 0) {
514
515 // Get ALL subpages to copy (read permissions respected - they should NOT be...):
516 $CPtable = $tcemainObj->int_pageTreeInfo(array(), $uid, intval($versionizeTree), $theNewRootID);
517
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);
524 } else {
525 $tcemainObj->newlog('Something went wrong during copying branch (for versioning)', 1);
526 break;
527 }
528 }
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);
533 }
534
535
536 /**
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
539 *
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.
544 * @return void
545 */
546 protected function version_swap($table, $id, $swapWith, $swapIntoWS=0, &$tcemainObj) {
547 global $TCA;
548
549 // First, check if we may actually edit the online record
550 if ($tcemainObj->checkRecordUpdateAccess($table, $id)) {
551
552 // Select the two versions:
553 $curVersion = t3lib_BEfunc::getRecord($table, $id, '*');
554 $swapVersion = t3lib_BEfunc::getRecord($table, $swapWith, '*');
555 $movePlh = array();
556 $movePlhID = 0;
557
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()) {
564
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)) {
567
568 // Lock file name:
569 $lockFileName = PATH_site.'typo3temp/swap_locking/' . $table . ':' . $id . '.ser';
570
571 if (!@is_file($lockFileName)) {
572
573 // Write lock-file:
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
579 )));
580
581 // Find fields to keep
582 $keepFields = $tcemainObj->getUniqueFields($table);
583 if ($TCA[$table]['ctrl']['sortby']) {
584 $keepFields[] = $TCA[$table]['ctrl']['sortby'];
585 }
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'];
590 }
591
592 // Swap "keepfields"
593 foreach ($keepFields as $fN) {
594 $tmp = $swapVersion[$fN];
595 $swapVersion[$fN] = $curVersion[$fN];
596 $curVersion[$fN] = $tmp;
597 }
598
599 // Preserve states:
600 $t3ver_state = array();
601 $t3ver_state['swapVersion'] = $swapVersion['t3ver_state'];
602 $t3ver_state['curVersion'] = $curVersion['t3ver_state'];
603
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;
617 if ($swapIntoWS) {
618 if ($t3ver_state['swapVersion'] > 0) {
619 $swapVersion['t3ver_wsid'] = $tcemainObj->BE_USER->workspace;
620 } else {
621 $swapVersion['t3ver_wsid'] = intval($curVersion['t3ver_wsid']);
622 }
623 }
624 $swapVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
625 $swapVersion['t3ver_stage'] = 0;
626 if (!$swapIntoWS) {
627 $swapVersion['t3ver_state'] = 0;
628 }
629
630 // Moving element.
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']);
636
637 $curVersion['t3ver_state'] = intval($swapVersion['t3ver_state']);
638 $swapVersion['t3ver_state'] = 0;
639
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']];
644 }
645 }
646 }
647
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
653 );
654 }
655 }
656 unset($swapVersion['uid']);
657
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;
667 if (!$swapIntoWS) {
668 $curVersion['t3ver_state'] = 0;
669 }
670
671 // Keeping the swapmode state
672 if ($table === 'pages') {
673 $curVersion['t3ver_swapmode'] = $swapVersion['t3ver_swapmode'];
674 }
675
676 // Registering and swapping MM relations in current and swap records:
677 $tcemainObj->version_remapMMForVersionSwap($table, $id, $swapWith);
678
679 // Generating proper history data to prepare logging
680 $tcemainObj->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion);
681 $tcemainObj->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion);
682
683 // Execute swapping:
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();
688 } else {
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();
692 } else {
693 unlink($lockFileName);
694 }
695 }
696
697 if (!count($sqlErrors)) {
698
699 // If a moving operation took place...:
700 if ($movePlhID) {
701 // Remove, if normal publishing:
702 if (!$swapIntoWS) {
703 // For delete + completely delete!
704 $tcemainObj->deleteEl($table, $movePlhID, TRUE, TRUE);
705 } else {
706 // Otherwise update the movePlaceholder:
707 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($movePlhID), $movePlh);
708 $tcemainObj->updateRefIndex($table, $movePlhID);
709 }
710 }
711
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)) {
715 // Force delete
716 $tcemainObj->deleteEl($table, $id, TRUE);
717 }
718
719 $tcemainObj->newlog2(($swapIntoWS ? 'Swapping' : 'Publishing') . ' successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, $table, $id, $swapVersion['pid']);
720
721 // Update reference index of the live record:
722 $tcemainObj->updateRefIndex($table, $id);
723
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');
728 } else {
729 $label = $GLOBALS['LANG']->sL ('LLL:EXT:lang/locallang_tcemain.xml:version_swap.online_record_updated');
730 }
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);
733
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');
740 } else {
741 $label = $GLOBALS['LANG']->sL ('LLL:EXT:lang/locallang_tcemain.xml:version_swap.online_record_updated');
742 }
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);
745
746 // SWAPPING pids for subrecords:
747 if ($table=='pages' && $swapVersion['t3ver_swapmode'] >= 0) {
748
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);
755
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();
759 }
760
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();
764 }
765
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();
769 }
770
771 if (count($sqlErrors)) {
772 $tcemainObj->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), 2);
773 }
774 }
775 }
776 }
777 // Clear cache:
778 $tcemainObj->clear_cache($table, $id);
779
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);
784 }
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);
794 }
795
796
797 /**
798 * Update relations on version/workspace swapping.
799 *
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
805 * @return void
806 */
807 protected function version_swap_procBasedOnFieldType($table, $field, $conf, &$curVersion, &$swapVersion, $tcemainObj) {
808 $inlineType = $tcemainObj->getInlineFieldType($conf);
809
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']);
821
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;
828 }
829 }
830
831
832
833 /**
834 * Release version from this workspace (and into "Live" workspace but as an offline version).
835 *
836 * @param string Table name
837 * @param integer Record UID
838 * @param boolean If set, will completely delete element
839 * @return void
840 */
841 protected function version_clearWSID($table, $id, $flush = FALSE, &$tcemainObj) {
842 global $TCA;
843
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:
849 $updateData = array(
850 't3ver_wsid' => 0
851 );
852 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($id), $updateData);
853
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);
859 }
860
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);
866 }
867
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);
872 }
873 }
874 }
875 } else $tcemainObj->newlog('Attempt to reset workspace for record failed because you do not have edit access',1);
876 }
877
878
879 /*******************************
880 ***** helper functions ******
881 *******************************/
882
883
884 /**
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!)
887 *
888 * @param integer Current page id.
889 * @param integer New page id
890 * @param array Array of tables from which to copy
891 * @return void
892 * @see versionizePages()
893 */
894 protected function rawCopyPageContent($oldPageId, $newPageId, $copyTablesArray, &$tcemainObj) {
895 global $TCA;
896
897 if ($newPageId) {
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(
902 'uid',
903 $table,
904 'pid=' . intval($oldPageId) . $tcemainObj->deleteClause($table)
905 );
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);
911 }
912 }
913 $GLOBALS['TYPO3_DB']->sql_free_result($mres);
914 }
915 }
916 }
917 }
918
919
920 /**
921 * Finds all elements for swapping versions in workspace
922 *
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
927 */
928 protected function findPageElementsForVersionSwap($table, $id, $offlineId) {
929 global $TCA;
930
931 $rec = t3lib_BEfunc::getRecord($table, $offlineId, 't3ver_wsid');
932 $workspaceId = $rec['t3ver_wsid'];
933
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'];
943 } else {
944 $pageId = $id;
945 $offlinePageId = $offlineId;
946 }
947
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]);
958 }
959 $GLOBALS['TYPO3_DB']->sql_free_result($res);
960 }
961 }
962 if ($offlinePageId && $offlinePageId != $pageId) {
963 $elementData['pages'][] = array($pageId, $offlinePageId);
964 }
965 }
966 return $elementData;
967 }
968
969 /**
970 * Searches for all elements from all tables on the given pages in the same workspace.
971 *
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
975 * @return void
976 */
977 protected function findPageElementsForVersionStageChange($pageIdList, $workspaceId, &$elementList) {
978 global $TCA;
979
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')
991 );
992 while (FALSE !== ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
993 $elementList[$table][] = $row[0];
994 }
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]);
1000 }
1001 }
1002 }
1003 }
1004 }
1005
1006
1007 /**
1008 * Finds page UIDs for the element from table <code>$table</code> with UIDs from <code>$idList</code>
1009 *
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
1015 * @return void
1016 */
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')
1026 );
1027 while (FALSE !== ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
1028 $pageIdList[] = $row[0];
1029 // Find ws version
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'];
1036 }
1037 }
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);
1042 }
1043 }
1044
1045
1046 /**
1047 * Finds real page IDs for state change.
1048 *
1049 * @param array $idList List of page UIDs, possibly versioned
1050 * @return void
1051 */
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'];
1057 }
1058 }
1059 }
1060 }
1061
1062 ?>