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