[BUGFIX] Doctrine: Provide table name to lastInsertId()
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Resource / Index / FileIndexRepository.php
1 <?php
2 namespace TYPO3\CMS\Core\Resource\Index;
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 TYPO3\CMS\Core\Database\ConnectionPool;
18 use TYPO3\CMS\Core\Database\ReferenceIndex;
19 use TYPO3\CMS\Core\Resource\File;
20 use TYPO3\CMS\Core\Resource\FileInterface;
21 use TYPO3\CMS\Core\Resource\Folder;
22 use TYPO3\CMS\Core\Resource\ResourceFactory;
23 use TYPO3\CMS\Core\Resource\ResourceStorage;
24 use TYPO3\CMS\Core\SingletonInterface;
25 use TYPO3\CMS\Core\Utility\GeneralUtility;
26 use TYPO3\CMS\Extbase\Object\ObjectManager;
27 use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
28
29 /**
30 * Repository Class as an abstraction layer to sys_file
31 *
32 * Every access to table sys_file_metadata which is not handled by TCEmain
33 * has to use this Repository class.
34 *
35 * This is meant for FAL internal use only!.
36 */
37 class FileIndexRepository implements SingletonInterface
38 {
39 /**
40 * @var string
41 */
42 protected $table = 'sys_file';
43
44 /**
45 * A list of properties which are to be persisted
46 *
47 * @var array
48 */
49 protected $fields = [
50 'uid', 'pid', 'missing', 'type', 'storage', 'identifier', 'identifier_hash', 'extension',
51 'mime_type', 'name', 'sha1', 'size', 'creation_date', 'modification_date', 'folder_hash'
52 ];
53
54 /**
55 * Gets the Resource Factory
56 *
57 * @return ResourceFactory
58 */
59 protected function getResourceFactory()
60 {
61 return ResourceFactory::getInstance();
62 }
63
64 /**
65 * Returns an Instance of the Repository
66 *
67 * @return FileIndexRepository
68 */
69 public static function getInstance()
70 {
71 return GeneralUtility::makeInstance(self::class);
72 }
73
74 /**
75 * Retrieves Index record for a given $combinedIdentifier
76 *
77 * @param string $combinedIdentifier
78 * @return array|bool
79 */
80 public function findOneByCombinedIdentifier($combinedIdentifier)
81 {
82 list($storageUid, $identifier) = GeneralUtility::trimExplode(':', $combinedIdentifier, false, 2);
83 return $this->findOneByStorageUidAndIdentifier($storageUid, $identifier);
84 }
85
86 /**
87 * Retrieves Index record for a given $fileUid
88 *
89 * @param int $fileUid
90 * @return array|bool
91 */
92 public function findOneByUid($fileUid)
93 {
94 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
95 ->getQueryBuilderForTable($this->table);
96
97 $row = $queryBuilder
98 ->select(...$this->fields)
99 ->from($this->table)
100 ->where(
101 $queryBuilder->expr()->eq('uid', (int)$fileUid)
102 )
103 ->execute()
104 ->fetch();
105
106 return is_array($row) ? $row : false;
107 }
108
109 /**
110 * Retrieves Index record for a given $storageUid and $identifier
111 *
112 * @param int $storageUid
113 * @param string $identifier
114 * @return array|bool
115 *
116 * @internal only for use from FileRepository
117 */
118 public function findOneByStorageUidAndIdentifier($storageUid, $identifier)
119 {
120 $identifierHash = $this->getResourceFactory()->getStorageObject($storageUid)->hashFileIdentifier($identifier);
121 return $this->findOneByStorageUidAndIdentifierHash($storageUid, $identifierHash);
122 }
123
124 /**
125 * Retrieves Index record for a given $storageUid and $identifier
126 *
127 * @param int $storageUid
128 * @param string $identifierHash
129 * @return array|bool
130 *
131 * @internal only for use from FileRepository
132 */
133 public function findOneByStorageUidAndIdentifierHash($storageUid, $identifierHash)
134 {
135 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
136 ->getQueryBuilderForTable($this->table);
137
138 $row = $queryBuilder
139 ->select(...$this->fields)
140 ->from($this->table)
141 ->where(
142 $queryBuilder->expr()->eq('storage', (int)$storageUid),
143 $queryBuilder->expr()->eq('identifier_hash', $queryBuilder->createNamedParameter($identifierHash))
144 )
145 ->execute()
146 ->fetch();
147
148 return is_array($row) ? $row : false;
149 }
150
151 /**
152 * Retrieves Index record for a given $fileObject
153 *
154 * @param FileInterface $fileObject
155 * @return array|bool
156 *
157 * @internal only for use from FileRepository
158 */
159 public function findOneByFileObject(FileInterface $fileObject)
160 {
161 $storageUid = $fileObject->getStorage()->getUid();
162 $identifierHash = $fileObject->getHashedIdentifier();
163 return $this->findOneByStorageUidAndIdentifierHash($storageUid, $identifierHash);
164 }
165
166 /**
167 * Returns all indexed files which match the content hash
168 * Used by the indexer to detect already present files
169 *
170 * @param string $hash
171 * @return mixed
172 */
173 public function findByContentHash($hash)
174 {
175 if (!preg_match('/^[0-9a-f]{40}$/i', $hash)) {
176 return [];
177 }
178
179 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
180 ->getQueryBuilderForTable($this->table);
181
182 $resultRows = $queryBuilder
183 ->select(...$this->fields)
184 ->from($this->table)
185 ->where(
186 $queryBuilder->expr()->eq('sha1', $queryBuilder->createNamedParameter($hash))
187 )
188 ->execute()
189 ->fetchAll();
190
191 return $resultRows;
192 }
193
194 /**
195 * Find all records for files in a Folder
196 *
197 * @param Folder $folder
198 * @return array|NULL
199 */
200 public function findByFolder(Folder $folder)
201 {
202 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
203 ->getQueryBuilderForTable($this->table);
204
205 $result = $queryBuilder
206 ->select(...$this->fields)
207 ->from($this->table)
208 ->where(
209 $queryBuilder->expr()->eq(
210 'folder_hash',
211 $queryBuilder->createNamedParameter($folder->getHashedIdentifier())
212 ),
213 $queryBuilder->expr()->eq('storage', (int)$folder->getStorage()->getUid())
214 )
215 ->execute();
216
217 $resultRows = [];
218 while ($row = $result->fetch()) {
219 $resultRows[$row['identifier']] = $row;
220 }
221
222 return $resultRows;
223 }
224
225 /**
226 * Find all records for files in an array of Folders
227 *
228 * @param Folder[] $folders
229 * @param bool $includeMissing
230 * @param string $fileName
231 * @return array|NULL
232 */
233 public function findByFolders(array $folders, $includeMissing = true, $fileName = null)
234 {
235 $storageUids = [];
236 $folderIdentifiers = [];
237
238 foreach ($folders as $folder) {
239 if (!$folder instanceof Folder) {
240 continue;
241 }
242
243 $storageUids[] = (int)$folder->getStorage()->getUid();
244 $folderIdentifiers[] = $folder->getHashedIdentifier();
245 }
246
247 $storageUids = array_unique($storageUids);
248 $folderIdentifiers = array_unique($folderIdentifiers);
249
250 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
251
252 $queryBuilder
253 ->select(...$this->fields)
254 ->from($this->table)
255 ->where(
256 $queryBuilder->expr()->in(
257 'folder_hash',
258 array_map([$queryBuilder, 'createNamedParameter'], $folderIdentifiers)
259 ),
260 $queryBuilder->expr()->in('storage', $storageUids)
261 );
262
263 if (isset($fileName)) {
264 $queryBuilder->andWhere(
265 $queryBuilder->expr()->like(
266 'name',
267 $queryBuilder->createNamedParameter('%' . $queryBuilder->escapeLikeWildcards($fileName) . '%')
268 )
269 );
270 }
271
272 if (!$includeMissing) {
273 $queryBuilder->andWhere($queryBuilder->expr()->eq('missing', 0));
274 }
275
276 $result = $queryBuilder->execute();
277
278 $fileRecords = [];
279 while ($fileRecord = $result->fetch()) {
280 $fileRecords[$fileRecord['identifier']] = $fileRecord;
281 }
282
283 return $fileRecords;
284 }
285
286 /**
287 * Adds a file to the index
288 *
289 * @param File $file
290 * @return void
291 */
292 public function add(File $file)
293 {
294 if ($this->hasIndexRecord($file)) {
295 $this->update($file);
296 if ($file->_getPropertyRaw('uid') === null) {
297 $file->updateProperties($this->findOneByFileObject($file));
298 }
299 } else {
300 $file->updateProperties(['uid' => $this->insertRecord($file->getProperties())]);
301 }
302 }
303
304 /**
305 * Add data from record (at indexing time)
306 *
307 * @param array $data
308 * @return array
309 */
310 public function addRaw(array $data)
311 {
312 $data['uid'] = $this->insertRecord($data);
313 return $data;
314 }
315
316 /**
317 * Helper to reduce code duplication
318 *
319 * @param array $data
320 *
321 * @return int
322 */
323 protected function insertRecord(array $data)
324 {
325 $data = array_intersect_key($data, array_flip($this->fields));
326 $data['tstamp'] = time();
327 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
328 $connection->insert(
329 $this->table,
330 $data
331 );
332 $data['uid'] = $connection->lastInsertId($this->table);
333 $this->updateRefIndex($data['uid']);
334 $this->emitRecordCreatedSignal($data);
335 return $data['uid'];
336 }
337
338 /**
339 * Checks if a file is indexed
340 *
341 * @param File $file
342 * @return bool
343 */
344 public function hasIndexRecord(File $file)
345 {
346 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
347
348 if ((int)$file->_getPropertyRaw('uid') > 0) {
349 $constraints = [
350 $queryBuilder->expr()->eq('uid', (int)$file->getUid())
351 ];
352 } else {
353 $constraints = [
354 $queryBuilder->expr()->eq('storage', (int)$file->getStorage()->getUid()),
355 $queryBuilder->expr()->eq(
356 'identifier',
357 $queryBuilder->createNamedParameter($file->_getPropertyRaw('identifier'))
358 )
359 ];
360 }
361
362 $count = $queryBuilder
363 ->count('uid')
364 ->from($this->table)
365 ->where(...$constraints)
366 ->execute()
367 ->fetchColumn(0);
368
369 return (bool)$count;
370 }
371
372 /**
373 * Updates the index record in the database
374 *
375 * @param File $file
376 * @return void
377 */
378 public function update(File $file)
379 {
380 $updatedProperties = array_intersect($this->fields, $file->getUpdatedProperties());
381 $updateRow = [];
382 foreach ($updatedProperties as $key) {
383 $updateRow[$key] = $file->getProperty($key);
384 }
385 if (!empty($updateRow)) {
386 if ((int)$file->_getPropertyRaw('uid') > 0) {
387 $constraints = ['uid' => (int)$file->getUid()];
388 } else {
389 $constraints = [
390 'storage' => (int)$file->getStorage()->getUid(),
391 'identifier' => $file->_getPropertyRaw('identifier')
392 ];
393 }
394
395 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
396 $updateRow['tstamp'] = time();
397
398 $connection->update(
399 $this->table,
400 $updateRow,
401 $constraints
402 );
403
404 $this->updateRefIndex($file->getUid());
405 $this->emitRecordUpdatedSignal(array_intersect_key($file->getProperties(), array_flip($this->fields)));
406 }
407 }
408
409 /**
410 * Finds the files needed for second indexer step
411 *
412 * @param ResourceStorage $storage
413 * @param int $limit
414 * @return array
415 */
416 public function findInStorageWithIndexOutstanding(ResourceStorage $storage, $limit = -1)
417 {
418 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
419
420 if ((int)$limit > 0) {
421 $queryBuilder->setMaxResults((int)$limit);
422 }
423
424 $rows = $queryBuilder
425 ->select(...$this->fields)
426 ->from($this->table)
427 ->where(
428 $queryBuilder->expr()->gt('tstamp', $queryBuilder->quoteIdentifier('last_indexed')),
429 $queryBuilder->expr()->eq('storage', (int)$storage->getUid())
430 )
431 ->orderBy('tstamp', 'ASC')
432 ->execute()
433 ->fetchAll();
434
435 return $rows;
436 }
437
438 /**
439 * Helper function for the Indexer to detect missing files
440 *
441 * @param ResourceStorage $storage
442 * @param array $uidList
443 * @return array
444 */
445 public function findInStorageAndNotInUidList(ResourceStorage $storage, array $uidList)
446 {
447 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
448
449 $queryBuilder
450 ->select(...$this->fields)
451 ->from($this->table)
452 ->where(
453 $queryBuilder->expr()->eq('storage', (int)$storage->getUid())
454 );
455
456 if (!empty($uidList)) {
457 $queryBuilder->andWhere($queryBuilder->expr()->notIn('uid', array_map('intval', $uidList)));
458 }
459
460 $rows = $queryBuilder->execute()->fetchAll();
461
462 return $rows;
463 }
464
465 /**
466 * Updates the timestamp when the file indexer extracted metadata
467 *
468 * @param int $fileUid
469 * @return void
470 */
471 public function updateIndexingTime($fileUid)
472 {
473 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
474 $connection->update(
475 $this->table,
476 [
477 'last_indexed' => time()
478 ],
479 [
480 'uid' => (int)$fileUid
481 ]
482 );
483 }
484
485 /**
486 * Marks given file as missing in sys_file
487 *
488 * @param int $fileUid
489 * @return void
490 */
491 public function markFileAsMissing($fileUid)
492 {
493 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
494 $connection->update(
495 $this->table,
496 [
497 'missing' => 1
498 ],
499 [
500 'uid' => (int)$fileUid
501 ]
502 );
503 $this->emitRecordMarkedAsMissingSignal($fileUid);
504 }
505
506 /**
507 * Remove a sys_file record from the database
508 *
509 * @param int $fileUid
510 * @return void
511 */
512 public function remove($fileUid)
513 {
514 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
515 $connection->delete(
516 $this->table,
517 [
518 'uid' => (int)$fileUid
519 ]
520 );
521 $this->updateRefIndex($fileUid);
522 $this->emitRecordDeletedSignal($fileUid);
523 }
524
525 /**
526 * Update Reference Index (sys_refindex) for a file
527 *
528 * @param int $id Record UID
529 * @return void
530 */
531 public function updateRefIndex($id)
532 {
533 /** @var $refIndexObj ReferenceIndex */
534 $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
535 $refIndexObj->updateRefIndexTable($this->table, $id);
536 }
537
538 /*
539 * Get the SignalSlot dispatcher
540 *
541 * @return Dispatcher
542 */
543 protected function getSignalSlotDispatcher()
544 {
545 return $this->getObjectManager()->get(Dispatcher::class);
546 }
547
548 /**
549 * Get the ObjectManager
550 *
551 * @return ObjectManager
552 */
553 protected function getObjectManager()
554 {
555 return GeneralUtility::makeInstance(ObjectManager::class);
556 }
557
558 /**
559 * Signal that is called after an IndexRecord is updated
560 *
561 * @param array $data
562 * @signal
563 */
564 protected function emitRecordUpdatedSignal(array $data)
565 {
566 $this->getSignalSlotDispatcher()->dispatch(self::class, 'recordUpdated', [$data]);
567 }
568
569 /**
570 * Signal that is called after an IndexRecord is created
571 *
572 * @param array $data
573 * @signal
574 */
575 protected function emitRecordCreatedSignal(array $data)
576 {
577 $this->getSignalSlotDispatcher()->dispatch(self::class, 'recordCreated', [$data]);
578 }
579
580 /**
581 * Signal that is called after an IndexRecord is deleted
582 *
583 * @param int $fileUid
584 * @signal
585 */
586 protected function emitRecordDeletedSignal($fileUid)
587 {
588 $this->getSignalSlotDispatcher()->dispatch(self::class, 'recordDeleted', [$fileUid]);
589 }
590
591 /**
592 * Signal that is called after an IndexRecord is marked as missing
593 *
594 * @param int $fileUid
595 * @signal
596 */
597 protected function emitRecordMarkedAsMissingSignal($fileUid)
598 {
599 $this->getSignalSlotDispatcher()->dispatch(self::class, 'recordMarkedAsMissing', [$fileUid]);
600 }
601 }