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