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