2 namespace TYPO3\CMS\Version\Controller
;
5 * This file is part of the TYPO3 CMS project.
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
14 * The TYPO3 project - inspiring people to share!
17 use TYPO3\CMS\Core\Utility\GeneralUtility
;
18 use TYPO3\CMS\Backend\Utility\BackendUtility
;
19 use TYPO3\CMS\Backend\Utility\IconUtility
;
22 * Versioning module, including workspace management
24 * @author Kasper Skårhøj <kasperYYYY@typo3.com>
26 class VersionModuleController
extends \TYPO3\CMS\Backend\Module\BaseScriptClass
{
29 * Module configuration
33 public $MCONF = array();
40 public $MOD_MENU = array();
43 * Module session settings
47 public $MOD_SETTINGS = array();
50 * document template object
52 * @var \TYPO3\CMS\Backend\Template\DocumentTemplate
66 public $showWorkspaceCol = 0;
71 public $formatWorkspace_cache = array();
76 public $formatCount_cache = array();
81 public $targets = array();
84 * Accumulation of online targets.
88 public $pageModule = '';
95 public $publishAccess = FALSE;
100 public $stageIndex = array();
105 public $recIndex = array();
108 * Initialize language files
110 public function __construct() {
111 $GLOBALS['LANG']->includeLLFile('EXT:version/locallang.xlf');
115 * Initialize menu configuration
119 public function menuConfig() {
121 $this->MOD_SETTINGS
= BackendUtility
::getModuleData($this->MOD_MENU
, GeneralUtility
::_GP('SET'), $this->MCONF
['name'], 'ses');
125 * Main function of the module. Write the content to $this->content
129 public function main() {
137 // Setting module configuration:
138 $this->MCONF
= $GLOBALS['MCONF'];
139 $this->REQUEST_URI
= str_replace('&sendToReview=1', '', GeneralUtility
::getIndpEnv('REQUEST_URI'));
141 $this->doc
= GeneralUtility
::makeInstance(\TYPO3\CMS\Backend\Template\DocumentTemplate
::class);
142 $this->doc
->backPath
= $GLOBALS['BACK_PATH'];
143 $this->doc
->setModuleTemplate('EXT:version/Resources/Private/Templates/version.html');
145 $this->doc
->inDocStylesArray
[$GLOBALS['MCONF']['name']] = '
146 .version-diff-1 { background-color: green; }
147 .version-diff-2 { background-color: red; }
149 // Setting up the context sensitive menu:
150 $this->doc
->getContextMenuCode();
151 // Getting input data:
152 $this->id
= (int)GeneralUtility
::_GP('id');
154 // Record uid. Goes with table name to indicate specific record
155 $this->uid
= (int)GeneralUtility
::_GP('uid');
156 // // Record table. Goes with uid to indicate specific record
157 $this->table
= GeneralUtility
::_GP('table');
159 $this->details
= GeneralUtility
::_GP('details');
160 // Page id. If set, indicates activation from Web>Versioning module
161 $this->diffOnly
= GeneralUtility
::_GP('diffOnly');
162 // Flag. If set, shows only the offline version and with diff-view
163 // Force this setting:
164 $this->MOD_SETTINGS
['expandSubElements'] = TRUE;
165 $this->MOD_SETTINGS
['diff'] = $this->details ||
$this->MOD_SETTINGS
['diff'] ?
1 : 0;
166 // Reading the record:
167 $record = BackendUtility
::getRecord($this->table
, $this->uid
);
168 if ($record['pid'] == -1) {
169 $record = BackendUtility
::getRecord($this->table
, $record['t3ver_oid']);
171 $this->recordFound
= is_array($record);
172 $pidValue = $this->table
=== 'pages' ?
$this->uid
: $record['pid'];
173 // Checking access etc.
174 if ($this->recordFound
&& $GLOBALS['TCA'][$this->table
]['ctrl']['versioningWS'] && !$this->id
) {
175 $this->doc
->form
= '<form action="" method="post">';
176 $this->uid
= $record['uid'];
177 // Might have changed if new live record was found!
179 // The page will show only if there is a valid page and if this page may be viewed by the user
180 $this->pageinfo
= BackendUtility
::readPageAccess($pidValue, $this->perms_clause
);
181 $access = is_array($this->pageinfo
) ?
1 : 0;
182 if ($pidValue && $access ||
$GLOBALS['BE_USER']->user
['admin'] && !$pidValue) {
184 $this->doc
->JScode
.= $this->doc
->wrapScriptTags('
186 function hlSubelements(origId, verId, over, diffLayer) { //
188 document.getElementById(\'orig_\'+origId).attributes.getNamedItem("class").nodeValue = \'typo3-ver-hl\';
189 document.getElementById(\'ver_\'+verId).attributes.getNamedItem("class").nodeValue = \'typo3-ver-hl\';
191 document.getElementById(\'diff_\'+verId).style.visibility = \'visible\';
194 document.getElementById(\'orig_\'+origId).attributes.getNamedItem("class").nodeValue = \'typo3-ver\';
195 document.getElementById(\'ver_\'+verId).attributes.getNamedItem("class").nodeValue = \'typo3-ver\';
197 document.getElementById(\'diff_\'+verId).style.visibility = \'hidden\';
202 // If another page module was specified, replace the default Page module with the new one
203 $newPageModule = trim($GLOBALS['BE_USER']->getTSConfigVal('options.overridePageModule'));
204 $this->pageModule
= BackendUtility
::isModuleSetInTBE_MODULES($newPageModule) ?
$newPageModule : 'web_layout';
205 // Setting publish access permission for workspace:
206 $this->publishAccess
= $GLOBALS['BE_USER']->workspacePublishAccess($GLOBALS['BE_USER']->workspace
);
207 $this->versioningMgm();
209 $this->content
.= $this->doc
->spacer(10);
210 // Setting up the buttons and markers for docheader
211 $docHeaderButtons = $this->getButtons();
212 $markers['CSH'] = $docHeaderButtons['csh'];
213 $markers['FUNC_MENU'] = BackendUtility
::getFuncMenu($this->id
, 'SET[function]', $this->MOD_SETTINGS
['function'], $this->MOD_MENU
['function']);
214 $markers['CONTENT'] = $this->content
;
216 // If no access or id value, create empty document
217 $this->content
= $this->doc
->section($GLOBALS['LANG']->getLL('clickAPage_header'), $GLOBALS['LANG']->getLL('clickAPage_content'), 0, 1);
218 // Setting up the buttons and markers for docheader
219 $docHeaderButtons = $this->getButtons();
220 $markers['CONTENT'] = $this->content
;
222 // Build the <body> for the module
223 $this->content
= $this->doc
->startPage($GLOBALS['LANG']->getLL('title'));
224 $this->content
.= $this->doc
->moduleBody($this->pageinfo
, $docHeaderButtons, $markers);
225 $this->content
.= $this->doc
->endPage();
226 $this->content
= $this->doc
->insertStylesAndJS($this->content
);
230 * Outputs accumulated module content to browser.
234 public function printContent() {
239 * Create the panel of buttons for submitting the form or otherwise perform operations.
241 * @return array All available buttons as an assoc. array
243 protected function getButtons() {
251 if ($this->recordFound
&& $GLOBALS['TCA'][$this->table
]['ctrl']['versioningWS']) {
253 $buttons['view'] = '<a href="#" onclick="' . htmlspecialchars(BackendUtility
::viewOnClick($this->pageinfo
['uid'], $GLOBALS['BACK_PATH'], BackendUtility
::BEgetRootLine($this->pageinfo
['uid']))) . '" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:labels.showPage', TRUE) . '">' . IconUtility
::getSpriteIcon('actions-document-view') . '</a>';
255 if ($GLOBALS['BE_USER']->mayMakeShortcut()) {
256 $buttons['shortcut'] = $this->doc
->makeShortcutIcon('id, edit_record, pointer, new_unique_uid, search_field, search_levels, showLimit', implode(',', array_keys($this->MOD_MENU
)), $this->MCONF
['name']);
258 // If access to Web>List for user, then link to that module.
259 $buttons['record_list'] = BackendUtility
::getListViewLink(array(
260 'id' => $this->pageinfo
['uid'],
261 'returnUrl' => GeneralUtility
::getIndpEnv('REQUEST_URI')
262 ), '', $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:labels.showList'));
267 /******************************
269 * Versioning management
271 ******************************/
273 * Management of versions for record
277 public function versioningMgm() {
279 $diff_1 = GeneralUtility
::_POST('diff_1');
280 $diff_2 = GeneralUtility
::_POST('diff_2');
281 if (GeneralUtility
::_POST('do_diff')) {
283 $content .= '<h3>' . $GLOBALS['LANG']->getLL('diffing') . ':</h3>';
284 if ($diff_1 && $diff_2) {
285 $diff_1_record = BackendUtility
::getRecord($this->table
, $diff_1);
286 $diff_2_record = BackendUtility
::getRecord($this->table
, $diff_2);
287 if (is_array($diff_1_record) && is_array($diff_2_record)) {
288 $t3lib_diff_Obj = GeneralUtility
::makeInstance(\TYPO3\CMS\Core\Utility\DiffUtility
::class);
291 <tr class="bgColor5 tableheader">
292 <td>' . $GLOBALS['LANG']->getLL('fieldname') . '</td>
293 <td width="98%">' . $GLOBALS['LANG']->getLL('coloredDiffView') . ':</td>
296 foreach ($diff_1_record as $fN => $fV) {
297 if ($GLOBALS['TCA'][$this->table
]['columns'][$fN] && $GLOBALS['TCA'][$this->table
]['columns'][$fN]['config']['type'] !== 'passthrough' && !GeneralUtility
::inList('t3ver_label', $fN)) {
298 if ((string)$diff_1_record[$fN] !== (string)$diff_2_record[$fN]) {
299 $diffres = $t3lib_diff_Obj->makeDiffDisplay(BackendUtility
::getProcessedValue($this->table
, $fN, $diff_2_record[$fN], 0, 1), BackendUtility
::getProcessedValue($this->table
, $fN, $diff_1_record[$fN], 0, 1));
301 <tr class="bgColor4">
303 <td width="98%">' . $diffres . '</td>
309 if (count($tRows) > 1) {
310 $content .= '<table border="0" cellpadding="1" cellspacing="1" width="100%">' . implode('', $tRows) . '</table><br /><br />';
312 $content .= $GLOBALS['LANG']->getLL('recordsMatchesCompletely');
315 $content .= $GLOBALS['LANG']->getLL('errorRecordsNotFound');
318 $content .= $GLOBALS['LANG']->getLL('errorDiffSources');
322 $record = BackendUtility
::getRecord($this->table
, $this->uid
);
323 $recordIcon = IconUtility
::getSpriteIconForRecord($this->table
, $record);
324 $recTitle = BackendUtility
::getRecordTitle($this->table
, $record, TRUE);
327 ' . $recordIcon . $recTitle . '
328 <form name="theform" action="' . str_replace('&sendToReview=1', '', $this->REQUEST_URI
) . '" method="post">
329 <table border="0" cellspacing="1" cellpadding="1">';
331 <tr class="bgColor5 tableheader">
334 <td title="' . $GLOBALS['LANG']->getLL('tblHeaderDesc_title') . '">' . $GLOBALS['LANG']->getLL('tblHeader_title') . '</td>
335 <td title="' . $GLOBALS['LANG']->getLL('tblHeaderDesc_uid') . '">' . $GLOBALS['LANG']->getLL('tblHeader_uid') . '</td>
336 <td title="' . $GLOBALS['LANG']->getLL('tblHeaderDesc_t3ver_oid') . '">' . $GLOBALS['LANG']->getLL('tblHeader_t3ver_oid') . '</td>
337 <td title="' . $GLOBALS['LANG']->getLL('tblHeaderDesc_t3ver_id') . '">' . $GLOBALS['LANG']->getLL('tblHeader_t3ver_id') . '</td>
338 <td title="' . $GLOBALS['LANG']->getLL('tblHeaderDesc_t3ver_wsid') . '">' . $GLOBALS['LANG']->getLL('tblHeader_t3ver_wsid') . '</td>
339 <td title="' . $GLOBALS['LANG']->getLL('tblHeaderDesc_t3ver_state') . '">' . $GLOBALS['LANG']->getLL('tblHeader_t3ver_state') . '</td>
340 <td title="' . $GLOBALS['LANG']->getLL('tblHeaderDesc_t3ver_stage') . '">' . $GLOBALS['LANG']->getLL('tblHeader_t3ver_stage') . '</td>
341 <td title="' . $GLOBALS['LANG']->getLL('tblHeaderDesc_t3ver_count') . '">' . $GLOBALS['LANG']->getLL('tblHeader_t3ver_count') . '</td>
342 <td title="' . $GLOBALS['LANG']->getLL('tblHeaderDesc_pid') . '">' . $GLOBALS['LANG']->getLL('tblHeader_pid') . '</td>
343 <td title="' . $GLOBALS['LANG']->getLL('tblHeaderDesc_t3ver_label') . '">' . $GLOBALS['LANG']->getLL('tblHeader_t3ver_label') . '</td>
344 <td colspan="2"><input type="submit" name="do_diff" value="' . $GLOBALS['LANG']->getLL('diff') . '" /></td>
346 $versions = BackendUtility
::selectVersionsOfRecord($this->table
, $this->uid
, '*', $GLOBALS['BE_USER']->workspace
);
347 foreach ($versions as $row) {
348 $adminLinks = $this->adminLinks($this->table
, $row);
350 <tr class="' . ($row['uid'] != $this->uid ?
'bgColor4' : 'bgColor2 tableheader') . '">
351 <td>' . ($row['uid'] != $this->uid ?
352 '<a href="' . $this->doc
->issueCommand(('&cmd[' . $this->table
. '][' . $this->uid
. '][version][swapWith]=' . $row['uid'] . '&cmd[' . $this->table
. '][' . $this->uid
. '][version][action]=swap')) . '" title="' . $GLOBALS['LANG']->getLL('swapWithCurrent', TRUE) . '">' . IconUtility
::getSpriteIcon('actions-version-swap-version') . '</a>' :
353 IconUtility
::getSpriteIcon('status-status-current', array('title' => $GLOBALS['LANG']->getLL('currentOnlineVersion', TRUE)))) . '</td>
354 <td nowrap="nowrap">' . $adminLinks . '</td>
355 <td nowrap="nowrap">' . BackendUtility
::getRecordTitle($this->table
, $row, TRUE) . '</td>
356 <td>' . $row['uid'] . '</td>
357 <td>' . $row['t3ver_oid'] . '</td>
358 <td>' . $row['t3ver_id'] . '</td>
359 <td>' . $row['t3ver_wsid'] . '</td>
360 <td>' . $row['t3ver_state'] . '</td>
361 <td>' . $row['t3ver_stage'] . '</td>
362 <td>' . $row['t3ver_count'] . '</td>
363 <td>' . $row['pid'] . '</td>
364 <td nowrap="nowrap"><a href="#" onclick="' . htmlspecialchars(BackendUtility
::editOnClick(('&edit[' . $this->table
. '][' . $row['uid'] . ']=edit&columnsOnly=t3ver_label'), $this->doc
->backPath
)) . '" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:cm.edit', TRUE) . '">' . IconUtility
::getSpriteIcon('actions-document-open') . '</a>' . htmlspecialchars($row['t3ver_label']) . '</td>
365 <td class="version-diff-1"><input type="radio" name="diff_1" value="' . $row['uid'] . '"' . ($diff_1 == $row['uid'] ?
' checked="checked"' : '') . '/></td>
366 <td class="version-diff-2"><input type="radio" name="diff_2" value="' . $row['uid'] . '"' . ($diff_2 == $row['uid'] ?
' checked="checked"' : '') . '/></td>
368 // Show sub-content if the table is pages AND it is not the online branch (because that will mostly render the WHOLE tree below - not smart;)
369 if ($this->table
== 'pages' && $row['uid'] != $this->uid
) {
370 $sub = $this->pageSubContent($row['uid']);
376 <td colspan="10">' . $sub . '</td>
377 <td colspan="2"></td>
382 $content .= '</table></form>';
383 $this->content
.= $this->doc
->section($GLOBALS['LANG']->getLL('title'), $content, 0, 1);
387 <form action="' . $this->doc
->backPath
. 'tce_db.php" method="post">
388 ' . $GLOBALS['LANG']->getLL('tblHeader_t3ver_label') . ': <input type="text" name="cmd[' . $this->table
. '][' . $this->uid
. '][version][label]" /><br />
389 <br /><input type="hidden" name="cmd[' . $this->table
. '][' . $this->uid
. '][version][action]" value="new" />
390 <input type="hidden" name="prErr" value="1" />
391 <input type="hidden" name="redirect" value="' . htmlspecialchars($this->REQUEST_URI
) . '" />
392 <input type="submit" name="_" value="' . $GLOBALS['LANG']->getLL('createNewVersion') . '" />
393 ' . \TYPO3\CMS\Backend\Form\FormEngine
::getHiddenTokenField('tceAction') . '
397 $this->content
.= $this->doc
->spacer(15);
398 $this->content
.= $this->doc
->section($GLOBALS['LANG']->getLL('createNewVersion'), $content, 0, 1);
402 * Recursively look for children for page version with $pid
404 * @param int $pid UID of page record for which to look up sub-elements following that version
405 * @param int $c Counter, do not set (limits to 100 levels)
406 * @return string Table with content if any
408 public function pageSubContent($pid, $c = 0) {
409 $tableNames = GeneralUtility
::removeArrayEntryByValue(array_keys($GLOBALS['TCA']), 'pages');
410 $tableNames[] = 'pages';
412 foreach ($tableNames as $tN) {
413 // Basically list ALL tables - not only those being copied might be found!
414 $mres = $GLOBALS['TYPO3_DB']->exec_SELECTquery('*', $tN, 'pid=' . (int)$pid . BackendUtility
::deleteClause($tN), '', $GLOBALS['TCA'][$tN]['ctrl']['sortby'] ?
$GLOBALS['TCA'][$tN]['ctrl']['sortby'] : '');
415 if ($GLOBALS['TYPO3_DB']->sql_num_rows($mres)) {
418 <td colspan="4" class="' . ($GLOBALS['TCA'][$tN]['ctrl']['versioning_followPages'] ?
'bgColor6' : ($tN == 'pages' ?
'bgColor5' : 'bgColor-10')) . '"' . (!$GLOBALS['TCA'][$tN]['ctrl']['versioning_followPages'] && $tN !== 'pages' ?
' style="color: #666666; font-style:italic;"' : '') . '>' . $tN . '</td>
420 while ($subrow = $GLOBALS['TYPO3_DB']->sql_fetch_assoc($mres)) {
421 $ownVer = $this->lookForOwnVersions($tN, $subrow['uid']);
424 <td>' . $this->adminLinks($tN, $subrow) . '</td>
425 <td>' . $subrow['uid'] . '</td>
426 ' . ($ownVer > 1 ?
'<td style="font-weight: bold; background-color: yellow;"><a href="' .
427 htmlspecialchars(BackendUtility
::getModuleUrl('web_txversionM1', array('table' => $tN, 'uid' => $subrow['uid']))) .
428 '">' . ($ownVer - 1) . '</a></td>' : '<td></td>') . '
429 <td width="98%">' . BackendUtility
::getRecordTitle($tN, $subrow, TRUE) . '</td>
431 if ($tN == 'pages' && $c < 100) {
432 $sub = $this->pageSubContent($subrow['uid'], $c +
1);
439 <td width="98%">' . $sub . '</td>
445 $GLOBALS['TYPO3_DB']->sql_free_result($mres);
447 return $content ?
'<table border="1" cellpadding="1" cellspacing="0" width="100%">' . $content . '</table>' : '';
451 * Look for number of versions of a record
453 * @param string $table Table name
454 * @param int $uid Record uid
455 * @return int Number of versions for record, FALSE if none.
457 public function lookForOwnVersions($table, $uid) {
458 $versions = BackendUtility
::selectVersionsOfRecord($table, $uid, 'uid');
459 if (is_array($versions)) {
460 return count($versions);
466 * Administrative links for a table / record
468 * @param string $table Table name
469 * @param array $row Record for which administrative links are generated.
470 * @return string HTML link tags.
472 public function adminLinks($table, $row) {
474 $adminLink = '<a href="#" onclick="' . htmlspecialchars(BackendUtility
::editOnClick(('&edit[' . $table . '][' . $row['uid'] . ']=edit'), $this->doc
->backPath
)) . '" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:cm.edit', TRUE) . '">' . IconUtility
::getSpriteIcon('actions-document-open') . '</a>';
476 $adminLink .= '<a href="' . htmlspecialchars($this->doc
->issueCommand(('&cmd[' . $table . '][' . $row['uid'] . '][delete]=1'))) . '" title="' . $GLOBALS['LANG']->sL('LLL:EXT:lang/locallang_core.xlf:cm.delete', TRUE) . '">' . IconUtility
::getSpriteIcon('actions-edit-delete') . '</a>';
477 if ($table === 'pages') {
478 // If another page module was specified, replace the default Page module with the new one
479 $newPageModule = trim($GLOBALS['BE_USER']->getTSConfigVal('options.overridePageModule'));
480 $pageModule = BackendUtility
::isModuleSetInTBE_MODULES($newPageModule) ?
$newPageModule : 'web_layout';
481 // Perform some acccess checks:
482 $a_wl = $GLOBALS['BE_USER']->check('modules', 'web_list');
483 $a_wp = $GLOBALS['BE_USER']->check('modules', $pageModule);
484 $adminLink .= '<a href="#" onclick="top.loadEditId(' . $row['uid'] . ');top.goToModule(\'' . $pageModule . '\'); return false;">' . IconUtility
::getSpriteIcon('actions-page-open') . '</a>';
485 $adminLink .= '<a href="#" onclick="top.loadEditId(' . $row['uid'] . ');top.goToModule(\'web_list\'); return false;">' . IconUtility
::getSpriteIcon('actions-system-list-open') . '</a>';
486 // "View page" icon is added:
487 $adminLink .= '<a href="#" onclick="' . htmlspecialchars(BackendUtility
::viewOnClick($row['uid'], $this->doc
->backPath
, BackendUtility
::BEgetRootLine($row['uid']))) . '">' . IconUtility
::getSpriteIcon('actions-document-view') . '</a>';
489 if ($row['pid'] == -1) {
490 $getVars = '&ADMCMD_vPrev[' . rawurlencode(($table . ':' . $row['t3ver_oid'])) . ']=' . $row['uid'];
491 // "View page" icon is added:
492 $adminLink .= '<a href="#" onclick="' . htmlspecialchars(BackendUtility
::viewOnClick($row['_REAL_PID'], $this->doc
->backPath
, BackendUtility
::BEgetRootLine($row['_REAL_PID']), '', '', $getVars)) . '">' . IconUtility
::getSpriteIcon('actions-document-view') . '</a>';