[BUGFIX] Improve install tool temporary assets removal
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Resource / ProcessedFileRepository.php
1 <?php
2 namespace TYPO3\CMS\Core\Resource;
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 Psr\Log\LoggerAwareInterface;
18 use Psr\Log\LoggerAwareTrait;
19 use TYPO3\CMS\Core\Database\ConnectionPool;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21
22 /**
23 * Repository for accessing files
24 * it also serves as the public API for the indexing part of files in general
25 */
26 class ProcessedFileRepository extends AbstractRepository implements LoggerAwareInterface
27 {
28 use LoggerAwareTrait;
29
30 /**
31 * The main object type of this class. In some cases (fileReference) this
32 * repository can also return FileReference objects, implementing the
33 * common FileInterface.
34 *
35 * @var string
36 */
37 protected $objectType = ProcessedFile::class;
38
39 /**
40 * Main File object storage table. Note that this repository also works on
41 * the sys_file_reference table when returning FileReference objects.
42 *
43 * @var string
44 */
45 protected $table = 'sys_file_processedfile';
46
47 /**
48 * As determining the table columns is a costly operation this is done only once during runtime and cached then
49 *
50 * @var array
51 * @see cleanUnavailableColumns()
52 */
53 protected $tableColumns = [];
54
55 /**
56 * Creates this object.
57 */
58 public function __construct()
59 {
60 parent::__construct();
61 }
62
63 /**
64 * Creates a ProcessedFile object from a file object and a processing configuration
65 *
66 * @param FileInterface $originalFile
67 * @param string $taskType
68 * @param array $configuration
69 * @return ProcessedFile
70 */
71 public function createNewProcessedFileObject(FileInterface $originalFile, $taskType, array $configuration)
72 {
73 return GeneralUtility::makeInstance(
74 $this->objectType,
75 $originalFile,
76 $taskType,
77 $configuration
78 );
79 }
80
81 /**
82 * @param array $databaseRow
83 * @return ProcessedFile
84 */
85 protected function createDomainObject(array $databaseRow)
86 {
87 $originalFile = $this->factory->getFileObject((int)$databaseRow['original']);
88 $originalFile->setStorage($this->factory->getStorageObject($originalFile->getProperty('storage')));
89 $taskType = $databaseRow['task_type'];
90 $configuration = unserialize($databaseRow['configuration']);
91
92 return GeneralUtility::makeInstance(
93 $this->objectType,
94 $originalFile,
95 $taskType,
96 $configuration,
97 $databaseRow
98 );
99 }
100
101 /**
102 * @param ResourceStorage $storage
103 * @param string $identifier
104 *
105 * @return ProcessedFile|null
106 */
107 public function findByStorageAndIdentifier(ResourceStorage $storage, $identifier)
108 {
109 $processedFileObject = null;
110 if ($storage->hasFile($identifier)) {
111 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
112 $databaseRow = $queryBuilder
113 ->select('*')
114 ->from($this->table)
115 ->where(
116 $queryBuilder->expr()->eq(
117 'storage',
118 $queryBuilder->createNamedParameter($storage->getUid(), \PDO::PARAM_INT)
119 ),
120 $queryBuilder->expr()->eq(
121 'identifier',
122 $queryBuilder->createNamedParameter($identifier, \PDO::PARAM_STR)
123 )
124 )
125 ->execute()
126 ->fetch();
127
128 if ($databaseRow) {
129 $processedFileObject = $this->createDomainObject($databaseRow);
130 }
131 }
132 return $processedFileObject;
133 }
134
135 /**
136 * Count processed files by storage. This is used in the install tool
137 * to render statistics of processed files.
138 *
139 * @param ResourceStorage $storage
140 * @return int
141 */
142 public function countByStorage(ResourceStorage $storage): int
143 {
144 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
145 ->getQueryBuilderForTable($this->table);
146 return (int)$queryBuilder
147 ->count('uid')
148 ->from($this->table)
149 ->where(
150 $queryBuilder->expr()->eq(
151 'storage',
152 $queryBuilder->createNamedParameter($storage->getUid(), \PDO::PARAM_INT)
153 )
154 )
155 ->execute()
156 ->fetchColumn(0);
157 }
158
159 /**
160 * Adds a processedfile object in the database
161 *
162 * @param ProcessedFile $processedFile
163 */
164 public function add($processedFile)
165 {
166 if ($processedFile->isPersisted()) {
167 $this->update($processedFile);
168 } else {
169 $insertFields = $processedFile->toArray();
170 $insertFields['crdate'] = $insertFields['tstamp'] = time();
171 $insertFields = $this->cleanUnavailableColumns($insertFields);
172
173 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
174
175 $connection->insert(
176 $this->table,
177 $insertFields
178 );
179
180 $uid = $connection->lastInsertId($this->table);
181 $processedFile->updateProperties(['uid' => $uid]);
182 }
183 }
184
185 /**
186 * Updates an existing file object in the database
187 *
188 * @param ProcessedFile $processedFile
189 */
190 public function update($processedFile)
191 {
192 if ($processedFile->isPersisted()) {
193 $uid = (int)$processedFile->getUid();
194 $updateFields = $this->cleanUnavailableColumns($processedFile->toArray());
195 $updateFields['tstamp'] = time();
196
197 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
198 $connection->update(
199 $this->table,
200 $updateFields,
201 [
202 'uid' => (int)$uid
203 ]
204 );
205 }
206 }
207
208 /**
209 * @param \TYPO3\CMS\Core\Resource\File|\TYPO3\CMS\Core\Resource\FileInterface $file
210 * @param string $taskType The task that should be executed on the file
211 * @param array $configuration
212 *
213 * @return ProcessedFile
214 */
215 public function findOneByOriginalFileAndTaskTypeAndConfiguration(FileInterface $file, $taskType, array $configuration)
216 {
217 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
218
219 $databaseRow = $queryBuilder
220 ->select('*')
221 ->from($this->table)
222 ->where(
223 $queryBuilder->expr()->eq(
224 'original',
225 $queryBuilder->createNamedParameter($file->getUid(), \PDO::PARAM_INT)
226 ),
227 $queryBuilder->expr()->eq('task_type', $queryBuilder->createNamedParameter($taskType, \PDO::PARAM_STR)),
228 $queryBuilder->expr()->eq(
229 'configurationsha1',
230 $queryBuilder->createNamedParameter(sha1(serialize($configuration)), \PDO::PARAM_STR)
231 )
232 )
233 ->execute()
234 ->fetch();
235
236 if (is_array($databaseRow)) {
237 $processedFile = $this->createDomainObject($databaseRow);
238 } else {
239 $processedFile = $this->createNewProcessedFileObject($file, $taskType, $configuration);
240 }
241 return $processedFile;
242 }
243
244 /**
245 * @param FileInterface $file
246 * @return ProcessedFile[]
247 * @throws \InvalidArgumentException
248 */
249 public function findAllByOriginalFile(FileInterface $file)
250 {
251 if (!$file instanceof File) {
252 throw new \InvalidArgumentException('Parameter is no File object but got type "'
253 . (is_object($file) ? get_class($file) : gettype($file)) . '"', 1382006142);
254 }
255
256 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
257 $result = $queryBuilder
258 ->select('*')
259 ->from($this->table)
260 ->where(
261 $queryBuilder->expr()->eq(
262 'original',
263 $queryBuilder->createNamedParameter($file->getUid(), \PDO::PARAM_INT)
264 )
265 )
266 ->execute();
267
268 $itemList = [];
269 while ($row = $result->fetch()) {
270 $itemList[] = $this->createDomainObject($row);
271 }
272 return $itemList;
273 }
274
275 /**
276 * Removes all processed files and also deletes the associated physical files.
277 * If a storageUid is given, only db entries and files of this storage are removed.
278 *
279 * @param int|null $storageUid If not NULL, only the processed files of the given storage are removed
280 * @return int Number of failed deletions
281 */
282 public function removeAll($storageUid = null)
283 {
284 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
285 ->getQueryBuilderForTable($this->table);
286 $where = [
287 $queryBuilder->expr()->neq('identifier', $queryBuilder->createNamedParameter('', \PDO::PARAM_STR))
288 ];
289 if ($storageUid !== null) {
290 $where[] = $queryBuilder->expr()->eq(
291 'storage',
292 $queryBuilder->createNamedParameter($storageUid, \PDO::PARAM_INT)
293 );
294 }
295 $result = $queryBuilder
296 ->select('*')
297 ->from($this->table)
298 ->where(...$where)
299 ->execute();
300
301 $errorCount = 0;
302
303 while ($row = $result->fetch()) {
304 if ($storageUid && (int)$storageUid !== (int)$row['storage']) {
305 continue;
306 }
307 try {
308 $file = $this->createDomainObject($row);
309 $file->getStorage()->setEvaluatePermissions(false);
310 $file->delete(true);
311 } catch (\Exception $e) {
312 $this->logger->error(
313 'Failed to delete file "' . $row['identifier'] . '" in storage uid ' . $row['storage'] . '.',
314 [
315 'exception' => $e
316 ]
317 );
318 ++$errorCount;
319 }
320 }
321
322 if ($storageUid === null) {
323 // Truncate entire table if not restricted to specific storage
324 GeneralUtility::makeInstance(ConnectionPool::class)
325 ->getConnectionForTable($this->table)
326 ->truncate($this->table);
327 } else {
328 // else remove db rows of this storage only
329 GeneralUtility::makeInstance(ConnectionPool::class)
330 ->getConnectionForTable($this->table)
331 ->delete($this->table, ['storage' => $storageUid], [\PDO::PARAM_INT]);
332 }
333
334 return $errorCount;
335 }
336
337 /**
338 * Removes all array keys which cannot be persisted
339 *
340 * @param array $data
341 *
342 * @return array
343 */
344 protected function cleanUnavailableColumns(array $data)
345 {
346 // As determining the table columns is a costly operation this is done only once during runtime and cached then
347 if (empty($this->tableColumns[$this->table])) {
348 $this->tableColumns[$this->table] = GeneralUtility::makeInstance(ConnectionPool::class)
349 ->getConnectionForTable($this->table)
350 ->getSchemaManager()
351 ->listTableColumns($this->table);
352 }
353
354 return array_intersect_key($data, $this->tableColumns[$this->table]);
355 }
356 }