[BUGFIX] Show frontend user image migration wizard again
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / Updates / FrontendUserImageUpdateWizard.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 use Doctrine\DBAL\DBALException;
17 use TYPO3\CMS\Core\Database\ConnectionPool;
18 use TYPO3\CMS\Core\Database\Query\Expression\ExpressionBuilder;
19 use TYPO3\CMS\Core\Database\Query\Restriction\DeletedRestriction;
20 use TYPO3\CMS\Core\Log\Logger;
21 use TYPO3\CMS\Core\Log\LogManager;
22 use TYPO3\CMS\Core\Registry;
23 use TYPO3\CMS\Core\Resource\File;
24 use TYPO3\CMS\Core\Resource\ResourceStorage;
25 use TYPO3\CMS\Core\Resource\StorageRepository;
26 use TYPO3\CMS\Core\Utility\GeneralUtility;
27
28 /**
29 * Upgrade wizard which goes through all files referenced in fe_users::image
30 * and creates sys_file records as well as sys_file_reference records for each hit.
31 */
32 class FrontendUserImageUpdateWizard extends AbstractUpdate
33 {
34
35 /**
36 * Number of records fetched per database query
37 * Used to prevent memory overflows for huge databases
38 */
39 const RECORDS_PER_QUERY = 1000;
40
41 /**
42 * @var string
43 */
44 protected $title = 'Migrate all file relations from fe_users.image to sys_file_references';
45
46 /**
47 * @var ResourceStorage
48 */
49 protected $storage;
50
51 /**
52 * @var Logger
53 */
54 protected $logger;
55
56 /**
57 * Table to migrate records from
58 *
59 * @var string
60 */
61 protected $table = 'fe_users';
62
63 /**
64 * Table field holding the migration to be
65 *
66 * @var string
67 */
68 protected $fieldToMigrate = 'image';
69
70 /**
71 * the source file resides here
72 *
73 * @var string
74 */
75 protected $sourcePath = 'uploads/pics/';
76
77 /**
78 * target folder after migration
79 * Relative to fileadmin
80 *
81 * @var string
82 */
83 protected $targetPath = '_migrated/frontend_users/';
84
85 /**
86 * @var Registry
87 */
88 protected $registry;
89
90 /**
91 * @var string
92 */
93 protected $registryNamespace = 'FrontendUserImageUpdateWizard';
94
95 /**
96 * @var array|null
97 */
98 protected $recordOffset;
99
100 /**
101 * Constructor
102 */
103 public function __construct()
104 {
105 $this->logger = GeneralUtility::makeInstance(LogManager::class)->getLogger(__CLASS__);
106 }
107
108 /**
109 * Initialize the storage repository.
110 */
111 public function init()
112 {
113 $storages = GeneralUtility::makeInstance(StorageRepository::class)->findAll();
114 $this->storage = $storages[0];
115 $this->registry = GeneralUtility::makeInstance(Registry::class);
116 $this->recordOffset = $this->registry->get($this->registryNamespace, 'recordOffset');
117 }
118
119 /**
120 * Checks if an update is needed
121 *
122 * @param string &$description The description for the update
123 *
124 * @return bool TRUE if an update is needed, FALSE otherwise
125 */
126 public function checkForUpdate(&$description)
127 {
128 if ($this->isWizardDone()) {
129 return false;
130 }
131
132 $description = 'This update wizard goes through all files that are referenced in the fe_users.image field'
133 . ' and adds the files to the FAL File Index.<br />'
134 . 'It also moves the files from uploads/ to the fileadmin/_migrated/ path.';
135
136 $this->init();
137
138 return $this->recordOffset !== [];
139 }
140
141 /**
142 * Performs the database update.
143 *
144 * @param array &$dbQueries Queries done in this update
145 * @param string &$customMessage Custom message
146 * @return bool TRUE on success, FALSE on error
147 */
148 public function performUpdate(array &$dbQueries, &$customMessage)
149 {
150 $customMessage = '';
151 try {
152 $this->init();
153
154 if (!isset($this->recordOffset[$this->table])) {
155 $this->recordOffset[$this->table] = 0;
156 }
157
158 do {
159 $limit = $this->recordOffset[$this->table] . ',' . self::RECORDS_PER_QUERY;
160 $records = $this->getRecordsFromTable($limit, $dbQueries);
161 foreach ($records as $record) {
162 $this->migrateField($record, $customMessage, $dbQueries);
163 }
164 $this->registry->set($this->registryNamespace, 'recordOffset', $this->recordOffset);
165 } while (count($records) === self::RECORDS_PER_QUERY);
166
167 $this->markWizardAsDone();
168 $this->registry->remove($this->registryNamespace, 'recordOffset');
169 } catch (\Exception $e) {
170 $customMessage .= PHP_EOL . $e->getMessage();
171 }
172
173 return empty($customMessage);
174 }
175
176 /**
177 * Get records from table where the field to migrate is not empty (NOT NULL and != '')
178 * and also not numeric (which means that it is migrated)
179 *
180 * @param int $limit Maximum number records to select
181 * @param array $dbQueries
182 *
183 * @return array
184 * @throws \RuntimeException
185 */
186 protected function getRecordsFromTable($limit, &$dbQueries)
187 {
188 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
189 $queryBuilder = $connectionPool->getQueryBuilderForTable($this->table);
190
191 $queryBuilder->getRestrictions()
192 ->removeAll()
193 ->add(GeneralUtility::makeInstance(DeletedRestriction::class));
194
195 try {
196 $result = $queryBuilder
197 ->select('uid', 'pid', $this->fieldToMigrate)
198 ->from($this->table)
199 ->where(
200 $queryBuilder->expr()->isNotNull($this->fieldToMigrate),
201 $queryBuilder->expr()->neq(
202 $this->fieldToMigrate,
203 $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)
204 ),
205 $queryBuilder->expr()->comparison(
206 'CAST(CAST(' . $queryBuilder->quoteIdentifier($this->fieldToMigrate) . ' AS DECIMAL) AS CHAR)',
207 ExpressionBuilder::NEQ,
208 'CAST(' . $queryBuilder->quoteIdentifier($this->fieldToMigrate) . ' AS CHAR)'
209 )
210 )
211 ->orderBy('uid')
212 ->setFirstResult($limit)
213 ->execute();
214
215 $dbQueries[] = $queryBuilder->getSQL();
216
217 return $result->fetchAll();
218 } catch (DBALException $e) {
219 throw new \RuntimeException(
220 'Database query failed. Error was: ' . $e->getPrevious()->getMessage(),
221 1476050084
222 );
223 }
224 }
225
226 /**
227 * Migrates a single field.
228 *
229 * @param array $row
230 * @param string $customMessage
231 * @param array $dbQueries
232 *
233 * @throws \Exception
234 */
235 protected function migrateField($row, &$customMessage, &$dbQueries)
236 {
237 $fieldItems = GeneralUtility::trimExplode(',', $row[$this->fieldToMigrate], true);
238 if (empty($fieldItems) || is_numeric($row[$this->fieldToMigrate])) {
239 return;
240 }
241 $fileadminDirectory = rtrim($GLOBALS['TYPO3_CONF_VARS']['BE']['fileadminDir'], '/') . '/';
242 $i = 0;
243
244 if (!PATH_site) {
245 throw new \Exception('PATH_site was undefined.', 1476107387);
246 }
247
248 $storageUid = (int)$this->storage->getUid();
249
250 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
251
252 foreach ($fieldItems as $item) {
253 $fileUid = null;
254 $sourcePath = PATH_site . $this->sourcePath . $item;
255 $targetDirectory = PATH_site . $fileadminDirectory . $this->targetPath;
256 $targetPath = $targetDirectory . basename($item);
257
258 // maybe the file was already moved, so check if the original file still exists
259 if (file_exists($sourcePath)) {
260 if (!is_dir($targetDirectory)) {
261 GeneralUtility::mkdir_deep($targetDirectory);
262 }
263
264 // see if the file already exists in the storage
265 $fileSha1 = sha1_file($sourcePath);
266
267 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file');
268 $queryBuilder->getRestrictions()->removeAll();
269 $existingFileRecord = $queryBuilder->select('uid')->from('sys_file')->where(
270 $queryBuilder->expr()->eq(
271 'sha1',
272 $queryBuilder->createNamedParameter($fileSha1, \PDO::PARAM_STR)
273 ),
274 $queryBuilder->expr()->eq(
275 'storage',
276 $queryBuilder->createNamedParameter($storageUid, \PDO::PARAM_INT)
277 )
278 )->execute()->fetch();
279
280 // the file exists, the file does not have to be moved again
281 if (is_array($existingFileRecord)) {
282 $fileUid = $existingFileRecord['uid'];
283 } else {
284 // just move the file (no duplicate)
285 rename($sourcePath, $targetPath);
286 }
287 }
288
289 if ($fileUid === null) {
290 // get the File object if it hasn't been fetched before
291 try {
292 // if the source file does not exist, we should just continue, but leave a message in the docs;
293 // ideally, the user would be informed after the update as well.
294 /** @var File $file */
295 $file = $this->storage->getFile($this->targetPath . $item);
296 $fileUid = $file->getUid();
297 } catch (\InvalidArgumentException $e) {
298
299 // no file found, no reference can be set
300 $this->logger->notice(
301 'File ' . $this->sourcePath . $item . ' does not exist. Reference was not migrated.',
302 [
303 'table' => $this->table,
304 'record' => $row,
305 'field' => $this->fieldToMigrate,
306 ]
307 );
308
309 $format = 'File \'%s\' does not exist. Referencing field: %s.%d.%s. The reference was not migrated.';
310 $message = sprintf(
311 $format,
312 $this->sourcePath . $item,
313 $this->table,
314 $row['uid'],
315 $this->fieldToMigrate
316 );
317 $customMessage .= PHP_EOL . $message;
318 continue;
319 }
320 }
321
322 if ($fileUid > 0) {
323 $fields = [
324 'fieldname' => $this->fieldToMigrate,
325 'table_local' => 'sys_file',
326 'pid' => ($this->table === 'pages' ? $row['uid'] : $row['pid']),
327 'uid_foreign' => $row['uid'],
328 'uid_local' => $fileUid,
329 'tablenames' => $this->table,
330 'crdate' => time(),
331 'tstamp' => time(),
332 'sorting' => ($i + 256),
333 'sorting_foreign' => $i,
334 ];
335
336 $queryBuilder = $connectionPool->getQueryBuilderForTable('sys_file_reference');
337 $queryBuilder->insert('sys_file_reference')->values($fields)->execute();
338 $dbQueries[] = str_replace(LF, ' ', $queryBuilder->getSQL());
339 ++$i;
340 }
341 }
342
343 // Update referencing table's original field to now contain the count of references,
344 // but only if all new references could be set
345 if ($i === count($fieldItems)) {
346 $queryBuilder = $connectionPool->getQueryBuilderForTable($this->table);
347 $queryBuilder->update($this->table)->where(
348 $queryBuilder->expr()->eq(
349 'uid',
350 $queryBuilder->createNamedParameter($row['uid'], \PDO::PARAM_INT)
351 )
352 )->set($this->fieldToMigrate, $i)->execute();
353 $dbQueries[] = str_replace(LF, ' ', $queryBuilder->getSQL());
354 } else {
355 $this->recordOffset[$this->table]++;
356 }
357 }
358 }