Followup to #15740: Move versioning code from TCEmain into "version" extension -...
[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 // custom command "version"
77 if ($command == 'version') {
78 $commandWasProcessed = TRUE;
79 $action = (string) $value['action'];
80 switch ($action) {
81
82 case 'new':
83 // check if page / branch versioning is needed,
84 // or if "element" version can be used
85 $versionizeTree = -1;
86 if (isset($value['treeLevels'])) {
87 $versionizeTree = t3lib_div::intInRange($value['treeLevels'], -1, 100);
88 }
89 if ($table == 'pages' && $versionizeTree >= 0) {
90 $this->versionizePages($id, $value['label'], $versionizeTree, $tcemainObj);
91 } else {
92 $tcemainObj->versionizeRecord($table, $id, $value['label']);
93 }
94 break;
95
96 case 'swap':
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']);
104 }
105 }
106 if (count($elementList) == 0) {
107 $elementList[$table][] = array($id, $value['swapWith']);
108 }
109 foreach ($elementList as $tbl => $idList) {
110 foreach ($idList as $idKey => $idSet) {
111 $this->version_swap($tbl, $idSet[0], $idSet[1], $value['swapIntoWS'], $tcemainObj);
112 }
113 }
114 break;
115
116 case 'clearWSID':
117 $this->version_clearWSID($table, $id, FALSE, $tcemainObj);
118 break;
119
120 case 'flush':
121 $this->version_clearWSID($table, $id, TRUE, $tcemainObj);
122 break;
123
124 case 'setStage':
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'];
132 } else {
133 $workspaceId = $tcemainObj->BE_USER->workspace;
134 }
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);
142 }
143 } else {
144 // Find all elements from the same ws to change stage
145 $this->findRealPageIds($idList);
146 $this->findPageElementsForVersionStageChange($idList, $workspaceId, $elementList);
147 }
148 }
149
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);
153 }
154 }
155 break;
156 }
157 }
158 }
159
160 /**
161 * hook that is called AFTER all commands of the commandmap was
162 * executed
163 * @param $tcemainObj reference to the main tcemain object
164 * @return void
165 */
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);
170 }
171
172 // Reset notification array
173 $this->notificationEmailInfo = array();
174 }
175
176
177 /**
178 * hook that is called AFTER all commands of the commandmap was
179 * executed
180 * @param $tcemainObj reference to the main tcemain object
181 * @return void
182 */
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'];
189
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'];
196 }
197 }
198
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');
205
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);
209 } else {
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);
213 }
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.
218 if ($res>0) {
219 $tcemainObj->deleteEl($table, $id);
220 } else {
221 $tcemainObj->newlog('Stage of root point did not allow for deletion',1);
222 }
223 } elseif ((int)$record['t3ver_state']===3) {
224 // Placeholders for moving operations are deletable directly.
225
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(
231 't3ver_state' => 0
232 );
233 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($wsRec['uid']), $updateFields);
234 }
235 $tcemainObj->deleteEl($table, $id);
236 } else {
237 // Otherwise, try to delete by versioning:
238 $tcemainObj->versionizeRecord($table, $id, 'DELETED!', TRUE);
239 $tcemainObj->deleteL10nOverlayRecords($table, $id);
240 }
241 }
242 }
243
244
245 /****************************
246 ***** Notifications ******
247 ****************************/
248
249 /**
250 * Send an email notification to users in workspace
251 *
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
257 * @return void
258 */
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;
263
264 if (is_array($workspaceRec)) {
265
266 // Compile label:
267 switch ((int)$stageId) {
268 case 1:
269 $newStage = 'Ready for review';
270 break;
271 case 10:
272 $newStage = 'Ready for publishing';
273 break;
274 case -1:
275 $newStage = 'Element was rejected!';
276 break;
277 case 0:
278 $newStage = 'Rejected element was noticed and edited';
279 break;
280 default:
281 $newStage = 'Unknown state change!?';
282 break;
283 }
284
285 // Compile list of recipients:
286 $emails = array();
287 switch((int)$stat['stagechg_notification']) {
288 case 1:
289 switch((int)$stageId) {
290 case 1:
291 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']);
292 break;
293 case 10:
294 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], TRUE);
295 break;
296 case -1:
297 # $emails = $this->getEmailsForStageChangeNotification($workspaceRec['reviewers']);
298 # $emails = array_merge($emails,$this->getEmailsForStageChangeNotification($workspaceRec['members']));
299
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);
305
306 $rows = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows(
307 'log_data,tstamp,userid',
308 'sys_log',
309 'action=6 and details_nr=30
310 AND tablename=' . $GLOBALS['TYPO3_DB']->fullQuoteStr($eTable, 'sys_log') . '
311 AND recuid=' . intval($eUid),
312 '',
313 'uid DESC'
314 );
315 // Find all implicated since the last stage-raise from editing to review:
316 foreach ($rows as $dat) {
317 $data = unserialize($dat['log_data']);
318
319 $emails = array_merge($emails, $this->getEmailsForStageChangeNotification($dat['userid'], TRUE));
320
321 if ($data['stage'] == 1) {
322 break;
323 }
324 }
325 }
326 break;
327
328 case 0:
329 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['members']);
330 break;
331
332 default:
333 $emails = $this->getEmailsForStageChangeNotification($workspaceRec['adminusers'], TRUE);
334 break;
335 }
336 break;
337
338 case 10:
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']));
342 break;
343 }
344 $emails = array_unique($emails);
345
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);
353 $eUid = $rr['pid'];
354 }
355 $path = t3lib_BEfunc::getRecordPath($eUid, '', 20);
356
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:
363
364 ==> %s
365
366 User Comment:
367 "%s"
368
369 State was change by %s (username: %s)
370 ';
371 $subject = trim($TSConfig['notificationEmail_subject']) ? trim($TSConfig['notificationEmail_subject']) : 'TYPO3 Workspace Note: Stage Change for %s';
372
373 // Send email:
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'],
380 $elementName,
381 $newStage,
382 $comment,
383 $this->BE_USER->user['realName'],
384 $this->BE_USER->user['username'],
385 $path,
386 $recTitle);
387
388 t3lib_div::plainMailEncoded(
389 implode(',', $emails),
390 sprintf($subject, $elementName),
391 trim($message)
392 );
393
394 $tcemainObj->newlog2('Notification email for stage change was sent to "' . implode(', ', $emails) . '"', $table, $id);
395 }
396 }
397 }
398
399 /**
400 * Return emails addresses of be_users from input list.
401 * previously called notifyStageChange_getEmails() in tcemain
402 *
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
406 */
407 protected function getEmailsForStageChangeNotification($listOfUsers, $noTablePrefix = FALSE) {
408 $users = t3lib_div::trimExplode(',', $listOfUsers, 1);
409 $emails = array();
410 foreach ($users as $userIdent) {
411 if ($noTablePrefix) {
412 $id = intval($userIdent);
413 } else {
414 list($table, $id) = t3lib_div::revExplode('_', $userIdent, 2);
415 }
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'];
420 }
421 }
422 }
423 }
424 return $emails;
425 }
426
427
428
429 /****************************
430 ***** Stage Changes ******
431 ****************************/
432
433 /**
434 * Setting stage of record
435 *
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?
441 * @return void
442 */
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']);
449
450 if (t3lib_div::inList('admin,online,offline,reviewer,owner', $stat['_ACCESS']) || ($stageId <= 1 && $stat['_ACCESS'] === 'member')) {
451
452 // Set stage of record:
453 $updateData = array(
454 't3ver_stage' => $stageId
455 );
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);
458
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));
461
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;
466 } else {
467 $this->notifyStageChange($stat, $stageId, $table, $id, $comment, $tcemainObj);
468 }
469 }
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);
472 }
473
474
475
476 /*****************************
477 ***** CMD versioning ******
478 *****************************/
479
480 /**
481 * Creates a new version of a page including content and possible subpages.
482 *
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]
486 * @return void
487 * @see copyPages()
488 */
489 protected function versionizePages($uid, $label, $versionizeTree, &$tcemainObj) {
490 global $TCA;
491
492 $uid = intval($uid);
493 // returns the branch
494 $brExist = $tcemainObj->doesBranchExist('', $uid, $tcemainObj->pMap['show'], 1);
495
496 // Checks if we had permissions
497 if ($brExist != -1) {
498
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;
505 }
506 }
507
508 // Begin to copy pages if we're allowed to:
509 if ($tcemainObj->BE_USER->workspaceVersioningTypeAccess($versionizeTree)) {
510
511 // Versionize this page:
512 $theNewRootID = $tcemainObj->versionizeRecord('pages', $uid, $label, FALSE, $versionizeTree);
513 if ($theNewRootID) {
514 $tcemainObj->rawCopyPageContent($uid, $theNewRootID, $verTablesArray, $tcemainObj);
515
516 // If we're going to copy recursively...:
517 if ($versionizeTree > 0) {
518
519 // Get ALL subpages to copy (read permissions respected - they should NOT be...):
520 $CPtable = $tcemainObj->int_pageTreeInfo(array(), $uid, intval($versionizeTree), $theNewRootID);
521
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);
528 } else {
529 $tcemainObj->newlog('Something went wrong during copying branch (for versioning)', 1);
530 break;
531 }
532 }
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);
537 }
538
539
540 /**
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
543 *
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.
548 * @return void
549 */
550 protected function version_swap($table, $id, $swapWith, $swapIntoWS=0, &$tcemainObj) {
551 global $TCA;
552
553 // First, check if we may actually edit the online record
554 if ($tcemainObj->checkRecordUpdateAccess($table, $id)) {
555
556 // Select the two versions:
557 $curVersion = t3lib_BEfunc::getRecord($table, $id, '*');
558 $swapVersion = t3lib_BEfunc::getRecord($table, $swapWith, '*');
559 $movePlh = array();
560 $movePlhID = 0;
561
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()) {
568
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)) {
571
572 // Lock file name:
573 $lockFileName = PATH_site.'typo3temp/swap_locking/' . $table . ':' . $id . '.ser';
574
575 if (!@is_file($lockFileName)) {
576
577 // Write lock-file:
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
583 )));
584
585 // Find fields to keep
586 $keepFields = $tcemainObj->getUniqueFields($table);
587 if ($TCA[$table]['ctrl']['sortby']) {
588 $keepFields[] = $TCA[$table]['ctrl']['sortby'];
589 }
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'];
594 }
595
596 // Swap "keepfields"
597 foreach ($keepFields as $fN) {
598 $tmp = $swapVersion[$fN];
599 $swapVersion[$fN] = $curVersion[$fN];
600 $curVersion[$fN] = $tmp;
601 }
602
603 // Preserve states:
604 $t3ver_state = array();
605 $t3ver_state['swapVersion'] = $swapVersion['t3ver_state'];
606 $t3ver_state['curVersion'] = $curVersion['t3ver_state'];
607
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;
621 if ($swapIntoWS) {
622 if ($t3ver_state['swapVersion'] > 0) {
623 $swapVersion['t3ver_wsid'] = $tcemainObj->BE_USER->workspace;
624 } else {
625 $swapVersion['t3ver_wsid'] = intval($curVersion['t3ver_wsid']);
626 }
627 }
628 $swapVersion['t3ver_tstamp'] = $GLOBALS['EXEC_TIME'];
629 $swapVersion['t3ver_stage'] = 0;
630 if (!$swapIntoWS) {
631 $swapVersion['t3ver_state'] = 0;
632 }
633
634 // Moving element.
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']);
640
641 $curVersion['t3ver_state'] = intval($swapVersion['t3ver_state']);
642 $swapVersion['t3ver_state'] = 0;
643
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']];
648 }
649 }
650 }
651
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
657 );
658 }
659 }
660 unset($swapVersion['uid']);
661
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;
671 if (!$swapIntoWS) {
672 $curVersion['t3ver_state'] = 0;
673 }
674
675 // Keeping the swapmode state
676 if ($table === 'pages') {
677 $curVersion['t3ver_swapmode'] = $swapVersion['t3ver_swapmode'];
678 }
679
680 // Registering and swapping MM relations in current and swap records:
681 $tcemainObj->version_remapMMForVersionSwap($table, $id, $swapWith);
682
683 // Generating proper history data to prepare logging
684 $tcemainObj->compareFieldArrayWithCurrentAndUnset($table, $id, $swapVersion);
685 $tcemainObj->compareFieldArrayWithCurrentAndUnset($table, $swapWith, $curVersion);
686
687 // Execute swapping:
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();
692 } else {
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();
696 } else {
697 unlink($lockFileName);
698 }
699 }
700
701 if (!count($sqlErrors)) {
702
703 // If a moving operation took place...:
704 if ($movePlhID) {
705 // Remove, if normal publishing:
706 if (!$swapIntoWS) {
707 // For delete + completely delete!
708 $tcemainObj->deleteEl($table, $movePlhID, TRUE, TRUE);
709 } else {
710 // Otherwise update the movePlaceholder:
711 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($movePlhID), $movePlh);
712 $tcemainObj->updateRefIndex($table, $movePlhID);
713 }
714 }
715
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)) {
719 // Force delete
720 $tcemainObj->deleteEl($table, $id, TRUE);
721 }
722
723 $tcemainObj->newlog2(($swapIntoWS ? 'Swapping' : 'Publishing') . ' successful for table "' . $table . '" uid ' . $id . '=>' . $swapWith, $table, $id, $swapVersion['pid']);
724
725 // Update reference index of the live record:
726 $tcemainObj->updateRefIndex($table, $id);
727
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');
732 } else {
733 $label = $GLOBALS['LANG']->sL ('LLL:EXT:lang/locallang_tcemain.xml:version_swap.online_record_updated');
734 }
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);
737
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');
744 } else {
745 $label = $GLOBALS['LANG']->sL ('LLL:EXT:lang/locallang_tcemain.xml:version_swap.online_record_updated');
746 }
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);
749
750 // SWAPPING pids for subrecords:
751 if ($table=='pages' && $swapVersion['t3ver_swapmode'] >= 0) {
752
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);
759
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();
763 }
764
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();
768 }
769
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();
773 }
774
775 if (count($sqlErrors)) {
776 $tcemainObj->newlog('During Swapping: SQL errors happened: ' . implode('; ', $sqlErrors), 2);
777 }
778 }
779 }
780 }
781 // Clear cache:
782 $tcemainObj->clear_cache($table, $id);
783
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);
788 }
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);
798 }
799
800
801 /**
802 * Update relations on version/workspace swapping.
803 *
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
809 * @return void
810 */
811 protected function version_swap_procBasedOnFieldType($table, $field, $conf, &$curVersion, &$swapVersion, $tcemainObj) {
812 $inlineType = $tcemainObj->getInlineFieldType($conf);
813
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']);
825
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;
832 }
833 }
834
835
836
837 /**
838 * Release version from this workspace (and into "Live" workspace but as an offline version).
839 *
840 * @param string Table name
841 * @param integer Record UID
842 * @param boolean If set, will completely delete element
843 * @return void
844 */
845 protected function version_clearWSID($table, $id, $flush = FALSE, &$tcemainObj) {
846 global $TCA;
847
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:
853 $updateData = array(
854 't3ver_wsid' => 0
855 );
856 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, 'uid=' . intval($id), $updateData);
857
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);
863 }
864
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);
870 }
871
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);
876 }
877 }
878 }
879 } else $tcemainObj->newlog('Attempt to reset workspace for record failed because you do not have edit access',1);
880 }
881
882
883 /*******************************
884 ***** helper functions ******
885 *******************************/
886
887
888 /**
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!)
891 *
892 * @param integer Current page id.
893 * @param integer New page id
894 * @param array Array of tables from which to copy
895 * @return void
896 * @see versionizePages()
897 */
898 protected function rawCopyPageContent($oldPageId, $newPageId, $copyTablesArray, &$tcemainObj) {
899 global $TCA;
900
901 if ($newPageId) {
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(
906 'uid',
907 $table,
908 'pid=' . intval($oldPageId) . $tcemainObj->deleteClause($table)
909 );
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);
915 }
916 }
917 $GLOBALS['TYPO3_DB']->sql_free_result($mres);
918 }
919 }
920 }
921 }
922
923
924 /**
925 * Finds all elements for swapping versions in workspace
926 *
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
931 */
932 protected function findPageElementsForVersionSwap($table, $id, $offlineId) {
933 global $TCA;
934
935 $rec = t3lib_BEfunc::getRecord($table, $offlineId, 't3ver_wsid');
936 $workspaceId = $rec['t3ver_wsid'];
937
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'];
947 } else {
948 $pageId = $id;
949 $offlinePageId = $offlineId;
950 }
951
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]);
962 }
963 $GLOBALS['TYPO3_DB']->sql_free_result($res);
964 }
965 }
966 if ($offlinePageId && $offlinePageId != $pageId) {
967 $elementData['pages'][] = array($pageId, $offlinePageId);
968 }
969 }
970 return $elementData;
971 }
972
973 /**
974 * Searches for all elements from all tables on the given pages in the same workspace.
975 *
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
979 * @return void
980 */
981 protected function findPageElementsForVersionStageChange($pageIdList, $workspaceId, &$elementList) {
982 global $TCA;
983
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')
995 );
996 while (FALSE !== ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
997 $elementList[$table][] = $row[0];
998 }
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]);
1004 }
1005 }
1006 }
1007 }
1008 }
1009
1010
1011 /**
1012 * Finds page UIDs for the element from table <code>$table</code> with UIDs from <code>$idList</code>
1013 *
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
1019 * @return void
1020 */
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')
1030 );
1031 while (FALSE !== ($row = $GLOBALS['TYPO3_DB']->sql_fetch_row($res))) {
1032 $pageIdList[] = $row[0];
1033 // Find ws version
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'];
1040 }
1041 }
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);
1046 }
1047 }
1048
1049
1050 /**
1051 * Finds real page IDs for state change.
1052 *
1053 * @param array $idList List of page UIDs, possibly versioned
1054 * @return void
1055 */
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'];
1061 }
1062 }
1063 }
1064 }
1065
1066 ?>