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