29256f71ce599267fed2d1b64270cd623293a3b1
[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 DataHandler
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 $nameParts = str_getcsv($fileName, ' ');
272 foreach ($nameParts as $part) {
273 $part = trim($part);
274 if ($part !== '') {
275 $queryBuilder->andWhere(
276 $queryBuilder->expr()->like(
277 'name',
278 $queryBuilder->createNamedParameter(
279 '%' . $queryBuilder->escapeLikeWildcards($part) . '%',
280 \PDO::PARAM_STR
281 )
282 )
283 );
284 }
285 }
286 }
287
288 if (!$includeMissing) {
289 $queryBuilder->andWhere($queryBuilder->expr()->eq('missing', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)));
290 }
291
292 $result = $queryBuilder->execute();
293
294 $fileRecords = [];
295 while ($fileRecord = $result->fetch()) {
296 $fileRecords[$fileRecord['identifier']] = $fileRecord;
297 }
298
299 return $fileRecords;
300 }
301
302 /**
303 * Adds a file to the index
304 *
305 * @param File $file
306 */
307 public function add(File $file)
308 {
309 if ($this->hasIndexRecord($file)) {
310 $this->update($file);
311 if ($file->_getPropertyRaw('uid') === null) {
312 $file->updateProperties($this->findOneByFileObject($file));
313 }
314 } else {
315 $file->updateProperties(['uid' => $this->insertRecord($file->getProperties())]);
316 }
317 }
318
319 /**
320 * Add data from record (at indexing time)
321 *
322 * @param array $data
323 * @return array
324 */
325 public function addRaw(array $data)
326 {
327 $data['uid'] = $this->insertRecord($data);
328 return $data;
329 }
330
331 /**
332 * Helper to reduce code duplication
333 *
334 * @param array $data
335 *
336 * @return int
337 */
338 protected function insertRecord(array $data)
339 {
340 $data = array_intersect_key($data, array_flip($this->fields));
341 $data['tstamp'] = time();
342 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
343 $connection->insert(
344 $this->table,
345 $data
346 );
347 $data['uid'] = $connection->lastInsertId($this->table);
348 $this->updateRefIndex($data['uid']);
349 $this->emitRecordCreatedSignal($data);
350 return $data['uid'];
351 }
352
353 /**
354 * Checks if a file is indexed
355 *
356 * @param File $file
357 * @return bool
358 */
359 public function hasIndexRecord(File $file)
360 {
361 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
362
363 if ((int)$file->_getPropertyRaw('uid') > 0) {
364 $constraints = [
365 $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($file->getUid(), \PDO::PARAM_INT))
366 ];
367 } else {
368 $constraints = [
369 $queryBuilder->expr()->eq(
370 'storage',
371 $queryBuilder->createNamedParameter($file->getStorage()->getUid(), \PDO::PARAM_INT)
372 ),
373 $queryBuilder->expr()->eq(
374 'identifier',
375 $queryBuilder->createNamedParameter($file->_getPropertyRaw('identifier'), \PDO::PARAM_STR)
376 )
377 ];
378 }
379
380 $count = $queryBuilder
381 ->count('uid')
382 ->from($this->table)
383 ->where(...$constraints)
384 ->execute()
385 ->fetchColumn(0);
386
387 return (bool)$count;
388 }
389
390 /**
391 * Updates the index record in the database
392 *
393 * @param File $file
394 */
395 public function update(File $file)
396 {
397 $updatedProperties = array_intersect($this->fields, $file->getUpdatedProperties());
398 $updateRow = [];
399 foreach ($updatedProperties as $key) {
400 $updateRow[$key] = $file->getProperty($key);
401 }
402 if (!empty($updateRow)) {
403 if ((int)$file->_getPropertyRaw('uid') > 0) {
404 $constraints = ['uid' => (int)$file->getUid()];
405 } else {
406 $constraints = [
407 'storage' => (int)$file->getStorage()->getUid(),
408 'identifier' => $file->_getPropertyRaw('identifier')
409 ];
410 }
411
412 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
413 $updateRow['tstamp'] = time();
414
415 $connection->update(
416 $this->table,
417 $updateRow,
418 $constraints
419 );
420
421 $this->updateRefIndex($file->getUid());
422 $this->emitRecordUpdatedSignal(array_intersect_key($file->getProperties(), array_flip($this->fields)));
423 }
424 }
425
426 /**
427 * Finds the files needed for second indexer step
428 *
429 * @param ResourceStorage $storage
430 * @param int $limit
431 * @return array
432 */
433 public function findInStorageWithIndexOutstanding(ResourceStorage $storage, $limit = -1)
434 {
435 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
436
437 if ((int)$limit > 0) {
438 $queryBuilder->setMaxResults((int)$limit);
439 }
440
441 $rows = $queryBuilder
442 ->select(...$this->fields)
443 ->from($this->table)
444 ->where(
445 $queryBuilder->expr()->gt('tstamp', $queryBuilder->quoteIdentifier('last_indexed')),
446 $queryBuilder->expr()->eq('storage', $queryBuilder->createNamedParameter($storage->getUid(), \PDO::PARAM_INT))
447 )
448 ->orderBy('tstamp', 'ASC')
449 ->execute()
450 ->fetchAll();
451
452 return $rows;
453 }
454
455 /**
456 * Helper function for the Indexer to detect missing files
457 *
458 * @param ResourceStorage $storage
459 * @param array $uidList
460 * @return array
461 */
462 public function findInStorageAndNotInUidList(ResourceStorage $storage, array $uidList)
463 {
464 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
465
466 $queryBuilder
467 ->select(...$this->fields)
468 ->from($this->table)
469 ->where(
470 $queryBuilder->expr()->eq(
471 'storage',
472 $queryBuilder->createNamedParameter($storage->getUid(), \PDO::PARAM_INT)
473 )
474 );
475
476 if (!empty($uidList)) {
477 $queryBuilder->andWhere(
478 $queryBuilder->expr()->notIn(
479 'uid',
480 $queryBuilder->createNamedParameter($uidList, Connection::PARAM_INT_ARRAY)
481 )
482 );
483 }
484
485 $rows = $queryBuilder->execute()->fetchAll();
486
487 return $rows;
488 }
489
490 /**
491 * Updates the timestamp when the file indexer extracted metadata
492 *
493 * @param int $fileUid
494 */
495 public function updateIndexingTime($fileUid)
496 {
497 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
498 $connection->update(
499 $this->table,
500 [
501 'last_indexed' => time()
502 ],
503 [
504 'uid' => (int)$fileUid
505 ]
506 );
507 }
508
509 /**
510 * Marks given file as missing in sys_file
511 *
512 * @param int $fileUid
513 */
514 public function markFileAsMissing($fileUid)
515 {
516 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
517 $connection->update(
518 $this->table,
519 [
520 'missing' => 1
521 ],
522 [
523 'uid' => (int)$fileUid
524 ]
525 );
526 $this->emitRecordMarkedAsMissingSignal($fileUid);
527 }
528
529 /**
530 * Remove a sys_file record from the database
531 *
532 * @param int $fileUid
533 */
534 public function remove($fileUid)
535 {
536 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
537 $connection->delete(
538 $this->table,
539 [
540 'uid' => (int)$fileUid
541 ]
542 );
543 $this->updateRefIndex($fileUid);
544 $this->emitRecordDeletedSignal($fileUid);
545 }
546
547 /**
548 * Update Reference Index (sys_refindex) for a file
549 *
550 * @param int $id Record UID
551 */
552 public function updateRefIndex($id)
553 {
554 /** @var $refIndexObj ReferenceIndex */
555 $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
556 $refIndexObj->enableRuntimeCache();
557 $refIndexObj->updateRefIndexTable($this->table, $id);
558 }
559
560 /*
561 * Get the SignalSlot dispatcher
562 *
563 * @return Dispatcher
564 */
565 protected function getSignalSlotDispatcher()
566 {
567 return $this->getObjectManager()->get(Dispatcher::class);
568 }
569
570 /**
571 * Get the ObjectManager
572 *
573 * @return ObjectManager
574 */
575 protected function getObjectManager()
576 {
577 return GeneralUtility::makeInstance(ObjectManager::class);
578 }
579
580 /**
581 * Signal that is called after an IndexRecord is updated
582 *
583 * @param array $data
584 */
585 protected function emitRecordUpdatedSignal(array $data)
586 {
587 $this->getSignalSlotDispatcher()->dispatch(self::class, 'recordUpdated', [$data]);
588 }
589
590 /**
591 * Signal that is called after an IndexRecord is created
592 *
593 * @param array $data
594 */
595 protected function emitRecordCreatedSignal(array $data)
596 {
597 $this->getSignalSlotDispatcher()->dispatch(self::class, 'recordCreated', [$data]);
598 }
599
600 /**
601 * Signal that is called after an IndexRecord is deleted
602 *
603 * @param int $fileUid
604 */
605 protected function emitRecordDeletedSignal($fileUid)
606 {
607 $this->getSignalSlotDispatcher()->dispatch(self::class, 'recordDeleted', [$fileUid]);
608 }
609
610 /**
611 * Signal that is called after an IndexRecord is marked as missing
612 *
613 * @param int $fileUid
614 */
615 protected function emitRecordMarkedAsMissingSignal($fileUid)
616 {
617 $this->getSignalSlotDispatcher()->dispatch(self::class, 'recordMarkedAsMissing', [$fileUid]);
618 }
619 }