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