[BUGFIX] Workspaces and Version update wizard isn't skippable
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Updates / MigrateWorkspacesUpdate.php
1 <?php
2 namespace TYPO3\CMS\Install\Updates;
3
4 /***************************************************************
5 * Copyright notice
6 *
7 * (c) 2010-2013 Tolleiv Nietsch <info@tolleiv.de>
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 * Migrates workspaces from TYPO3 versions below 4.5.
32 *
33 * @author Tolleiv Nietsch <info@tolleiv.de>
34 */
35 class MigrateWorkspacesUpdate extends InstallSysExtsUpdate {
36
37 /**
38 * @var string
39 */
40 protected $title = 'Versioning and Workspaces';
41
42 /**
43 * @var array
44 */
45 public $sqlQueries;
46
47 /**
48 * Checks if an update is needed
49 *
50 * @param string &$description The description for the update, which will be updated with a description of the script's purpose
51 * @return boolean Whether an update is needed (TRUE) or not (FALSE)
52 */
53 public function checkForUpdate(&$description) {
54 if ($this->isWizardDone()) {
55 return FALSE;
56 }
57 $result = FALSE;
58 $description = 'Migrates the old hardcoded draft workspace to be a real workspace record,
59 updates workspace owner fields to support either users or groups and
60 migrates the old-style workspaces with fixed workflow to a custom-stage workflow. If required
61 the extbase, fluid, version and workspaces extensions are installed.';
62 $reason = '';
63 // TYPO3 version 4.5 and above
64 if ($this->versionNumber >= 4005000) {
65 // If neither version nor workspaces is installed, we're not doing a migration
66 // Present the user with the choice of activating versioning and workspaces
67 if (
68 !\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('version')
69 && !\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('workspaces')
70 ) {
71 $result = TRUE;
72 // Override the default description
73 $description = 'Activates the usage of workspaces in your installation. Workspaces let you edit elements
74 without the changes being visible on the live web site right away. Modified elements can then go
75 through a validation process and eventually be published.<br /><br />';
76 $description .= 'This wizard will install system extensions "version" and "workspaces" (and may
77 install "fluid" and "extbase" too, as they are used by the "workspaces" extension).';
78 } else {
79 \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->loadExtensionTables(FALSE);
80 if (
81 !\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('version')
82 || !\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('workspaces')
83 ) {
84 $result = TRUE;
85 $reason .= ' Both extensions "version" and "workspaces" need to be
86 present to use the entire versioning and workflow featureset of TYPO3.';
87 }
88 $tables = array_keys($GLOBALS['TYPO3_DB']->admin_get_tables());
89 // sys_workspace table might not exists if version extension was never installed
90 if (!in_array('sys_workspace', $tables) || !in_array('sys_workspace_stage', $tables)) {
91 $result = TRUE;
92 $reason .= ' The database tables for the workspace functionality are missing.';
93 } elseif ($this->isOldStyleAdminFieldUsed() || $this->isOldStyleWorkspace()) {
94 $wsCount = $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('uid', 'sys_workspace', '');
95 $result |= $wsCount > 0;
96 $reason .= ' The existing workspaces will be checked for compatibility with the new features.';
97 }
98 $draftWorkspaceTestResult = $this->isDraftWorkspaceUsed();
99 if ($draftWorkspaceTestResult) {
100 $reason .= ' The old style draft workspace is used.
101 Related records will be moved into a full featured workspace.';
102 $result = TRUE;
103 }
104 $description .= '<br /><strong>Why do you need this wizard?</strong><br />' . $reason;
105 }
106 }
107 return $result;
108 }
109
110 /**
111 * This method requests input from the user about the upgrade process, if needed
112 *
113 * @param string $inputPrefix
114 * @return void
115 */
116 public function getUserInput($inputPrefix) {
117 $content = '';
118 if (
119 !\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('version')
120 && !\TYPO3\CMS\Core\Utility\ExtensionManagementUtility::isLoaded('workspaces')
121 ) {
122 // We need feedback only if versioning is not activated at all
123 // In such a case we want to leave the user with the choice of not activating the stuff at all
124 $content = '
125 <fieldset>
126 <ol>
127 ';
128 $content .= '
129 <li class="labelAfter">
130 <input type="checkbox" id="versioning" name="' . $inputPrefix . '[versioning]" value="1" checked="checked" />
131 <label for="versioning">Activate workspaces?</label>
132 </li>
133 ';
134 $content .= '
135 </ol>
136 </fieldset>
137 ';
138 } else {
139 // No feedback needed, just include the update flag as a hidden field
140 $content = '<input type="hidden" id="versioning" name="' . $inputPrefix . '[versioning]" value="1" />';
141 }
142 return $content;
143 }
144
145 /**
146 * Performs the database update. Changes existing workspaces to use the new custom workspaces
147 *
148 * @param array &$databaseQueries Queries done in this update
149 * @param mixed &$customMessages Custom messages
150 * @return boolean Whether it worked (TRUE) or not (FALSE)
151 */
152 public function performUpdate(array &$databaseQueries, &$customMessages) {
153 $result = TRUE;
154 // TYPO3 version below 4.5
155 if ($this->versionNumber < 4005000) {
156 return FALSE;
157 }
158 // Wizard skipped by the user
159 if (empty($this->pObj->INSTALL['update']['migrateWorkspaces']['versioning'])) {
160 $this->markWizardAsDone();
161 return TRUE;
162 }
163 \TYPO3\CMS\Core\Core\Bootstrap::getInstance()->loadExtensionTables(FALSE);
164 // install version and workspace extension (especially when updating from very old TYPO3 versions
165 $this->installExtensions(array('extbase', 'fluid', 'version', 'workspaces'));
166 // migrate all workspaces to support groups and be_users
167 if ($this->isOldStyleAdminFieldUsed()) {
168 $this->migrateAdminFieldToNewStyle();
169 }
170 // create a new dedicated "Draft" workspace and move all records to that new workspace
171 if ($this->isDraftWorkspaceUsed()) {
172 $draftWorkspaceId = $this->createWorkspace();
173 if (is_integer($draftWorkspaceId)) {
174 $this->migrateDraftWorkspaceRecordsToWorkspace($draftWorkspaceId);
175 }
176 }
177 $workspaces = $this->getWorkspacesWithoutStages();
178 $this->sqlQueries[] = $GLOBALS['TYPO3_DB']->debug_lastBuiltQuery;
179 $label = 'Review';
180 foreach ($workspaces as $workspace) {
181 // Find all workspaces and add "review" stage record
182 // Add review users and groups to the new IRRE record
183 $reviewStageId = $this->createReviewStageForWorkspace($workspace['uid'], $label, $workspace['reviewers']);
184 // Update all "review" state records in the database to point to the new state
185 $this->migrateOldRecordsToStage($workspace['uid'], 1, $reviewStageId);
186 // Update all "ready to publish" records in the database to point to the new ready to publish state
187 $this->migrateOldRecordsToStage($workspace['uid'], 10, -99);
188 }
189 if (is_array($this->sqlQueries) && is_array($databaseQueries)) {
190 $databaseQueries = array_merge($databaseQueries, $this->sqlQueries);
191 }
192 return $result;
193 }
194
195 /**
196 * Check if any table contains draft-workspace records
197 *
198 * @return boolean
199 */
200 protected function isDraftWorkspaceUsed() {
201 $foundDraftRecords = FALSE;
202 $tables = array_keys($GLOBALS['TCA']);
203 foreach ($tables as $table) {
204 if (is_array($GLOBALS['TCA'][$table])) {
205 $versioningVer = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($GLOBALS['TCA'][$table]['ctrl']['versioningWS'], 0, 2, 0);
206 if ($versioningVer > 0) {
207 if ($this->hasElementsOnWorkspace($table, -1)) {
208 $foundDraftRecords = TRUE;
209 break;
210 }
211 }
212 }
213 }
214 return $foundDraftRecords;
215 }
216
217 /**
218 * Find workspaces which have no sys_workspace_state(s) but have records using states
219 *
220 * @return boolean
221 */
222 protected function isOldStyleWorkspace() {
223 $foundOldStyleStages = FALSE;
224 $workspaces = $this->getWorkspacesWithoutStages();
225 $workspacesWithReviewers = 0;
226 $workspaceUids = array();
227 foreach ($workspaces as $workspace) {
228 $workspaceUids[] = $workspace['uid'];
229 if ($workspace['reviewers']) {
230 $workspacesWithReviewers++;
231 }
232 }
233 if (!$workspacesWithReviewers && !empty($workspaceUids)) {
234 $tables = array_keys($GLOBALS['TCA']);
235 foreach ($tables as $table) {
236 $versioningVer = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($GLOBALS['TCA'][$table]['ctrl']['versioningWS'], 0, 2, 0);
237 if ($versioningVer > 0) {
238 $count = $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('uid', $table, 't3ver_wsid IN (' . implode(',', $workspaceUids) . ') AND t3ver_stage IN (-1,1,10) AND pid = -1');
239 if ($count > 0) {
240 $foundOldStyleStages = TRUE;
241 break;
242 }
243 }
244 }
245 }
246 return $foundOldStyleStages || $workspacesWithReviewers;
247 }
248
249 /**
250 * Create a new stage for the given workspace
251 *
252 * @param integer Workspace ID
253 * @param string The label of the new stage
254 * @param string The users or groups which are authorized for that stage
255 * @return integer The id of the new stage
256 */
257 protected function createReviewStageForWorkspace($workspaceId, $stageLabel, $stageMembers) {
258 $data = array(
259 'parentid' => $workspaceId,
260 'parenttable' => 'sys_workspace',
261 'title' => $stageLabel,
262 'responsible_persons' => $stageMembers
263 );
264 $GLOBALS['TYPO3_DB']->exec_INSERTquery('sys_workspace_stage', $data);
265 $this->sqlQueries[] = $GLOBALS['TYPO3_DB']->debug_lastBuiltQuery;
266 return $GLOBALS['TYPO3_DB']->sql_insert_id();
267 }
268
269 /**
270 * Updates the stages of placeholder records within the given workspace from $oldId to $newId
271 *
272 * @param integer Workspace ID
273 * @param integer Old stage od
274 * @param integer New stage od
275 * @return void
276 */
277 protected function migrateOldRecordsToStage($workspaceId, $oldStageId, $newStageId) {
278 $tables = array_keys($GLOBALS['TCA']);
279 $where = 't3ver_wsid = ' . intval($workspaceId) . ' AND t3ver_stage = ' . intval($oldStageId) . ' AND pid = -1';
280 $values = array(
281 't3ver_stage' => intval($newStageId)
282 );
283 foreach ($tables as $table) {
284 $versioningVer = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($GLOBALS['TCA'][$table]['ctrl']['versioningWS'], 0, 2, 0);
285 if ($versioningVer > 0) {
286 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, $where, $values);
287 $this->sqlQueries[] = $GLOBALS['TYPO3_DB']->debug_lastBuiltQuery;
288 }
289 }
290 }
291
292 /**
293 * Check if there's any workspace which doesn't support the new admin-field format yet
294 *
295 * @return boolean
296 */
297 protected function isOldStyleAdminFieldUsed() {
298 $where = 'adminusers != "" AND adminusers NOT LIKE "%be_users%" AND adminusers NOT LIKE "%be_groups%" AND deleted=0';
299 $count = $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('uid', 'sys_workspace', $where);
300 $this->sqlQueries[] = $GLOBALS['TYPO3_DB']->debug_lastBuiltQuery;
301 return $count > 0;
302 }
303
304 /**
305 * Create a real workspace named "Draft"
306 *
307 * @return integer
308 */
309 protected function createWorkspace() {
310 // @todo who are the reviewers and owners for this workspace?
311 // In previous versions this was defined in be_groups/be_users with the setting "Edit in Draft"
312 $data = array(
313 'title' => 'Draft'
314 );
315 $GLOBALS['TYPO3_DB']->exec_INSERTquery('sys_workspace', $data);
316 $this->sqlQueries[] = $GLOBALS['TYPO3_DB']->debug_lastBuiltQuery;
317 return $GLOBALS['TYPO3_DB']->sql_insert_id();
318 }
319
320 /**
321 * Migrates all elements from the old draft workspace to the new one.
322 *
323 * @param integer $wsId
324 * @return void
325 */
326 protected function migrateDraftWorkspaceRecordsToWorkspace($wsId) {
327 $tables = array_keys($GLOBALS['TCA']);
328 $where = 't3ver_wsid=-1';
329 $values = array(
330 't3ver_wsid' => intval($wsId)
331 );
332 foreach ($tables as $table) {
333 $versioningVer = \TYPO3\CMS\Core\Utility\MathUtility::forceIntegerInRange($GLOBALS['TCA'][$table]['ctrl']['versioningWS'], 0, 2, 0);
334 if ($versioningVer > 0 && $this->hasElementsOnWorkspace($table, -1)) {
335 $GLOBALS['TYPO3_DB']->exec_UPDATEquery($table, $where, $values);
336 $this->sqlQueries[] = $GLOBALS['TYPO3_DB']->debug_lastBuiltQuery;
337 }
338 }
339 }
340
341 /**
342 * Migrate all workspace adminusers fields to support groups aswell,
343 * this means that the old comma separated list of uids (referring to be_users)
344 * is updated to be a list of uids with the tablename as prefix
345 *
346 * @return void
347 */
348 protected function migrateAdminFieldToNewStyle() {
349 $where = 'adminusers != "" AND adminusers NOT LIKE "%be_users%" AND adminusers NOT LIKE "%be_groups%" AND deleted=0';
350 $workspaces = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('uid, adminusers', 'sys_workspace', $where);
351 $this->sqlQueries[] = $GLOBALS['TYPO3_DB']->debug_lastBuiltQuery;
352 foreach ($workspaces as $workspace) {
353 $updateArray = array(
354 'adminusers' => 'be_users_' . implode(',be_users_', \TYPO3\CMS\Core\Utility\GeneralUtility::trimExplode(',', $workspace['adminusers'], TRUE))
355 );
356 $GLOBALS['TYPO3_DB']->exec_UPDATEquery('sys_workspace', 'uid = ' . $workspace['uid'], $updateArray);
357 $this->sqlQueries[] = $GLOBALS['TYPO3_DB']->debug_lastBuiltQuery;
358 }
359 }
360
361 /**
362 * Determines whether a table has elements in a particular workspace.
363 *
364 * @param string $table Name of the table
365 * @param integer $workspaceId Id of the workspace
366 * @return boolean
367 */
368 protected function hasElementsOnWorkspace($table, $workspaceId) {
369 $count = $GLOBALS['TYPO3_DB']->exec_SELECTcountRows('uid', $table, 't3ver_wsid=' . intval($workspaceId));
370 $this->sqlQueries[] = $GLOBALS['TYPO3_DB']->debug_lastBuiltQuery;
371 return $count > 0;
372 }
373
374 /**
375 * Returns all sys_workspace records which are not referenced by any sys_workspace_stages record
376 *
377 * @return array
378 */
379 protected function getWorkspacesWithoutStages() {
380 $stages = $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('parentid', 'sys_workspace_stage', 'parenttable=\'sys_workspace\'');
381 $wsWhitelist = array();
382 foreach ($stages as $stage) {
383 $wsWhitelist[] = $stage['parentid'];
384 }
385 $where = 'deleted=0';
386 $where .= !empty($wsWhitelist) ? ' AND uid NOT IN (' . implode(',', $wsWhitelist) . ')' : '';
387 return $GLOBALS['TYPO3_DB']->exec_SELECTgetRows('*', 'sys_workspace', $where);
388 }
389
390 /**
391 * Marks some wizard as being "seen" so that it not shown again.
392 *
393 * Writes the info in LocalConfiguration.php
394 *
395 * @param mixed $confValue The configuration is set to this value
396 * @return void
397 */
398 protected function markWizardAsDone($confValue = 1) {
399 \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Configuration\\ConfigurationManager')->setLocalConfigurationValueByPath('INSTALL/wizardDone/' . get_class($this), $confValue);
400 }
401
402 /**
403 * Checks if this wizard has been "done" before
404 *
405 * @return boolean TRUE if wizard has been done before, FALSE otherwise
406 */
407 protected function isWizardDone() {
408 $wizardClassName = get_class($this);
409 $done = FALSE;
410 if (isset($GLOBALS['TYPO3_CONF_VARS']['INSTALL']['wizardDone'][$wizardClassName]) && $GLOBALS['TYPO3_CONF_VARS']['INSTALL']['wizardDone'][$wizardClassName]) {
411 $done = TRUE;
412 }
413 return $done;
414 }
415 }
416 ?>