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