[BUGFIX] Report migration errors in TceformsUpdateWizard
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Updates / TceformsUpdateWizard.php
1 <?php
2 namespace TYPO3\CMS\Install\Updates;
3
4 /**
5 * This file is part of the TYPO3 CMS project.
6 *
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.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 use TYPO3\CMS\Core\Database\DatabaseConnection;
18 use TYPO3\CMS\Core\Utility\GeneralUtility;
19 use TYPO3\CMS\Extbase\Domain\Model\File;
20
21 /**
22 * Upgrade wizard which goes through all files referenced in the tt_content.image filed
23 * and creates sys_file records as well as sys_file_reference records for the individual usages.
24 *
25 * @author Ingmar Schlecht <ingmar@typo3.org>
26 */
27 class TceformsUpdateWizard extends AbstractUpdate {
28
29 /**
30 * Number of records fetched per database query
31 * Used to prevent memory overflows for huge databases
32 */
33 const RECORDS_PER_QUERY = 1000;
34
35 /**
36 * @var string
37 */
38 protected $title = 'Migrate all file relations from tt_content.image and pages.media';
39
40 /**
41 * @var \TYPO3\CMS\Core\Resource\ResourceStorage
42 */
43 protected $storage;
44
45 /**
46 * @var \TYPO3\CMS\Core\Log\Logger
47 */
48 protected $logger;
49
50 /**
51 * @var DatabaseConnection
52 */
53 protected $database;
54
55 /**
56 * Table fields to migrate
57 * @var array
58 */
59 protected $tables = array(
60 'tt_content' => array(
61 'image' => array(
62 'sourcePath' => 'uploads/pics/',
63 // Relative to fileadmin
64 'targetPath' => '_migrated/pics/',
65 'titleTexts' => 'titleText',
66 'captions' => 'imagecaption',
67 'links' => 'image_link',
68 'alternativeTexts' => 'altText'
69 )
70 ),
71 'pages' => array(
72 'media' => array(
73 'sourcePath' => 'uploads/media/',
74 // Relative to fileadmin
75 'targetPath' => '_migrated/media/'
76 )
77 ),
78 'pages_language_overlay' => array(
79 'media' => array(
80 'sourcePath' => 'uploads/media/',
81 // Relative to fileadmin
82 'targetPath' => '_migrated/media/'
83 )
84 )
85 );
86
87 /**
88 * Constructor
89 */
90 public function __construct() {
91 /** @var $logManager \TYPO3\CMS\Core\Log\LogManager */
92 $logManager = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Log\\LogManager');
93 $this->logger = $logManager->getLogger(__CLASS__);
94 $this->database = $GLOBALS['TYPO3_DB'];
95 }
96
97 /**
98 * Initialize the storage repository.
99 */
100 public function init() {
101 /** @var $storageRepository \TYPO3\CMS\Core\Resource\StorageRepository */
102 $storageRepository = GeneralUtility::makeInstance('TYPO3\\CMS\\Core\\Resource\\StorageRepository');
103 $storages = $storageRepository->findAll();
104 $this->storage = $storages[0];
105 }
106
107 /**
108 * Checks if an update is needed
109 *
110 * @param string &$description The description for the update
111 * @return boolean TRUE if an update is needed, FALSE otherwise
112 */
113 public function checkForUpdate(&$description) {
114 $description = 'This update wizard goes through all files that are referenced in the tt_content.image and '
115 . 'pages.media / pages_language_overlay.media field and adds the files to the new File Index.<br />'
116 . 'It also moves the files from uploads/ to the fileadmin/_migrated/ path.<br /><br />'
117 . 'This update wizard can be called multiple times in case it didn\'t finish after running once.';
118
119 if ($this->versionNumber < 6000000) {
120 // Nothing to do
121 return FALSE;
122 }
123
124 $finishedFields = $this->getFinishedFields();
125 if (count($finishedFields) === 0) {
126 // Nothing done yet, so there's plenty of work left
127 return TRUE;
128 }
129
130 $numberOfFieldsToMigrate = 0;
131 foreach ($this->tables as $table => $tableConfiguration) {
132 // find all additional fields we should get from the database
133 foreach (array_keys($tableConfiguration) as $fieldToMigrate) {
134 $fieldKey = $table . ':' . $fieldToMigrate;
135 if (!in_array($fieldKey, $finishedFields)) {
136 $numberOfFieldsToMigrate++;
137 }
138 }
139 }
140 return $numberOfFieldsToMigrate > 0;
141 }
142
143 /**
144 * Performs the database update.
145 *
146 * @param array &$dbQueries Queries done in this update
147 * @param mixed &$customMessages Custom messages
148 * @return boolean TRUE on success, FALSE on error
149 */
150 public function performUpdate(array &$dbQueries, &$customMessages) {
151 if ($this->versionNumber < 6000000) {
152 // Nothing to do
153 return TRUE;
154 }
155 try {
156 $this->init();
157 $finishedFields = $this->getFinishedFields();
158 foreach ($this->tables as $table => $tableConfiguration) {
159 // find all additional fields we should get from the database
160 foreach ($tableConfiguration as $fieldToMigrate => $fieldConfiguration) {
161 $fieldKey = $table . ':' . $fieldToMigrate;
162 if (in_array($fieldKey, $finishedFields)) {
163 // this field was already migrated
164 continue;
165 }
166 $fieldsToGet = array($fieldToMigrate);
167 if (isset($fieldConfiguration['titleTexts'])) {
168 $fieldsToGet[] = $fieldConfiguration['titleTexts'];
169 }
170 if (isset($fieldConfiguration['alternativeTexts'])) {
171 $fieldsToGet[] = $fieldConfiguration['alternativeTexts'];
172 }
173 if (isset($fieldConfiguration['captions'])) {
174 $fieldsToGet[] = $fieldConfiguration['captions'];
175 }
176 if (isset($fieldConfiguration['links'])) {
177 $fieldsToGet[] = $fieldConfiguration['links'];
178 }
179
180 do {
181 $records = $this->getRecordsFromTable($table, $fieldToMigrate, $fieldsToGet, self::RECORDS_PER_QUERY);
182 foreach ($records as $record) {
183 $this->migrateField($table, $record, $fieldToMigrate, $fieldConfiguration, $customMessages);
184 }
185 } while (count($records) === self::RECORDS_PER_QUERY);
186
187 // add the field to the "finished fields" if things didn't fail above
188 if (is_array($records)) {
189 $finishedFields[] = $fieldKey;
190 }
191 }
192 }
193 $this->markWizardAsDone(implode(',', $finishedFields));
194 } catch (\Exception $e) {
195 $customMessages .= PHP_EOL . $e->getMessage();
196 }
197 return empty($customMessages);
198 }
199
200 /**
201 * We write down the fields that were migrated. Like this: tt_content:media
202 * so you can check whether a field was already migrated
203 *
204 * @return array
205 */
206 protected function getFinishedFields() {
207 $className = 'TYPO3\\CMS\\Install\\Updates\\TceformsUpdateWizard';
208 return isset($GLOBALS['TYPO3_CONF_VARS']['INSTALL']['wizardDone'][$className])
209 ? explode(',', $GLOBALS['TYPO3_CONF_VARS']['INSTALL']['wizardDone'][$className])
210 : array();
211 }
212
213 /**
214 * Get records from table where the field to migrate is not empty (NOT NULL and != '')
215 * and also not numeric (which means that it is migrated)
216 *
217 * @param string $table
218 * @param string $fieldToMigrate
219 * @param array $relationFields
220 * @param int $limit Maximum number records to select
221 * @throws \RuntimeException
222 * @return array
223 */
224 protected function getRecordsFromTable($table, $fieldToMigrate, $relationFields, $limit) {
225 $fields = implode(',', array_merge($relationFields, array('uid', 'pid')));
226 $deletedCheck = isset($GLOBALS['TCA'][$table]['ctrl']['delete'])
227 ? ' AND ' . $GLOBALS['TCA'][$table]['ctrl']['delete'] . '=0'
228 : '';
229 $where = $fieldToMigrate . ' IS NOT NULL'
230 . ' AND ' . $fieldToMigrate . ' != \'\''
231 . ' AND CAST(CAST(' . $fieldToMigrate . ' AS DECIMAL) AS CHAR) <> ' . $fieldToMigrate
232 . $deletedCheck;
233 $result = $this->database->exec_SELECTgetRows($fields, $table, $where, '', '', $limit);
234 if ($result === NULL) {
235 throw new \RuntimeException('Database query failed. Error was: ' . $this->database->sql_error());
236 }
237 return $result;
238 }
239
240 /**
241 * Migrates a single field.
242 *
243 * @param string $table
244 * @param array $row
245 * @param string $fieldname
246 * @param array $fieldConfiguration
247 * @param string $customMessages
248 * @return array A list of performed database queries
249 * @throws \Exception
250 */
251 protected function migrateField($table, $row, $fieldname, $fieldConfiguration, &$customMessages) {
252 $titleTextContents = array();
253 $alternativeTextContents = array();
254 $captionContents = array();
255 $linkContents = array();
256
257 $fieldItems = GeneralUtility::trimExplode(',', $row[$fieldname], TRUE);
258 if (empty($fieldItems) || is_numeric($row[$fieldname])) {
259 return array();
260 }
261 if (isset($fieldConfiguration['titleTexts'])) {
262 $titleTextField = $fieldConfiguration['titleTexts'];
263 $titleTextContents = explode(LF, $row[$titleTextField]);
264 }
265
266 if (isset($fieldConfiguration['alternativeTexts'])) {
267 $alternativeTextField = $fieldConfiguration['alternativeTexts'];
268 $alternativeTextContents = explode(LF, $row[$alternativeTextField]);
269 }
270 if (isset($fieldConfiguration['captions'])) {
271 $captionField = $fieldConfiguration['captions'];
272 $captionContents = explode(LF, $row[$captionField]);
273 }
274 if (isset($fieldConfiguration['links'])) {
275 $linkField = $fieldConfiguration['links'];
276 $linkContents = explode(LF, $row[$linkField]);
277 }
278 $fileadminDirectory = rtrim($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'], '/') . '/';
279 $queries = array();
280 $i = 0;
281
282 if (!PATH_site) {
283 throw new \Exception('PATH_site was undefined.');
284 }
285
286 $storageUid = (int)$this->storage->getUid();
287
288 foreach ($fieldItems as $item) {
289 $fileUid = NULL;
290 $sourcePath = PATH_site . $fieldConfiguration['sourcePath'] . $item;
291 $targetDirectory = PATH_site . $fileadminDirectory . $fieldConfiguration['targetPath'];
292 $targetPath = $targetDirectory . $item;
293
294 // maybe the file was already moved, so check if the original file still exists
295 if (file_exists($sourcePath)) {
296 if (!is_dir($targetDirectory)) {
297 GeneralUtility::mkdir_deep($targetDirectory);
298 }
299
300 // see if the file already exists in the storage
301 $fileSha1 = sha1_file($sourcePath);
302
303 $existingFileRecord = $this->database->exec_SELECTgetSingleRow(
304 'uid',
305 'sys_file',
306 'sha1=' . $this->database->fullQuoteStr($fileSha1, 'sys_file') . ' AND storage=' . $storageUid
307 );
308 // the file exists, the file does not have to be moved again
309 if (is_array($existingFileRecord)) {
310 $fileUid = $existingFileRecord['uid'];
311 } else {
312 // just move the file (no duplicate)
313 rename($sourcePath, $targetPath);
314 }
315 }
316
317 if ($fileUid === NULL) {
318 // get the File object if it hasn't been fetched before
319 try {
320 // if the source file does not exist, we should just continue, but leave a message in the docs;
321 // ideally, the user would be informed after the update as well.
322 /** @var File $file */
323 $file = $this->storage->getFile($fieldConfiguration['targetPath'] . $item);
324 $fileUid = $file->getUid();
325
326 } catch (\InvalidArgumentException $e) {
327
328 // no file found, no reference can be set
329 $this->logger->notice(
330 'File ' . $fieldConfiguration['sourcePath'] . $item . ' does not exist. Reference was not migrated.',
331 array('table' => $table, 'record' => $row, 'field' => $fieldname)
332 );
333
334 $format = 'File \'%s\' does not exist. Referencing field: %s.%d.%s. The reference was not migrated.';
335 $message = sprintf($format, $fieldConfiguration['sourcePath'] . $item, $table, $row['uid'], $fieldname);
336 $customMessages .= PHP_EOL . $message;
337
338 continue;
339 }
340 }
341
342 if ($fileUid > 0) {
343 $fields = array(
344 // TODO add sorting/sorting_foreign
345 'fieldname' => $fieldname,
346 'table_local' => 'sys_file',
347 // the sys_file_reference record should always placed on the same page
348 // as the record to link to, see issue #46497
349 'pid' => ($table === 'pages' ? $row['uid'] : $row['pid']),
350 'uid_foreign' => $row['uid'],
351 'uid_local' => $fileUid,
352 'tablenames' => $table,
353 'crdate' => time(),
354 'tstamp' => time()
355 );
356 if (isset($titleTextField)) {
357 $fields['title'] = trim($titleTextContents[$i]);
358 }
359 if (isset($alternativeTextField)) {
360 $fields['alternative'] = trim($alternativeTextContents[$i]);
361 }
362 if (isset($captionField)) {
363 $fields['description'] = trim($captionContents[$i]);
364 }
365 if (isset($linkField)) {
366 $fields['link'] = trim($linkContents[$i]);
367 }
368 $this->database->exec_INSERTquery('sys_file_reference', $fields);
369 $queries[] = str_replace(LF, ' ', $this->database->debug_lastBuiltQuery);
370 ++$i;
371 }
372 }
373
374 // Update referencing table's original field to now contain the count of references,
375 // but only if all new references could be set
376 if ($i === count($fieldItems)) {
377 $this->database->exec_UPDATEquery($table, 'uid=' . $row['uid'], array($fieldname => $i));
378 $queries[] = str_replace(LF, ' ', $this->database->debug_lastBuiltQuery);
379 }
380 return $queries;
381 }
382 }