[TASK] Allow multiple search words for file search
[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 $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('%' . $queryBuilder->escapeLikeWildcards($part) . '%',
279 \PDO::PARAM_STR)
280 )
281 );
282 }
283 }
284 }
285
286 if (!$includeMissing) {
287 $queryBuilder->andWhere($queryBuilder->expr()->eq('missing', $queryBuilder->createNamedParameter(0, \PDO::PARAM_INT)));
288 }
289
290 $result = $queryBuilder->execute();
291
292 $fileRecords = [];
293 while ($fileRecord = $result->fetch()) {
294 $fileRecords[$fileRecord['identifier']] = $fileRecord;
295 }
296
297 return $fileRecords;
298 }
299
300 /**
301 * Adds a file to the index
302 *
303 * @param File $file
304 * @return void
305 */
306 public function add(File $file)
307 {
308 if ($this->hasIndexRecord($file)) {
309 $this->update($file);
310 if ($file->_getPropertyRaw('uid') === null) {
311 $file->updateProperties($this->findOneByFileObject($file));
312 }
313 } else {
314 $file->updateProperties(['uid' => $this->insertRecord($file->getProperties())]);
315 }
316 }
317
318 /**
319 * Add data from record (at indexing time)
320 *
321 * @param array $data
322 * @return array
323 */
324 public function addRaw(array $data)
325 {
326 $data['uid'] = $this->insertRecord($data);
327 return $data;
328 }
329
330 /**
331 * Helper to reduce code duplication
332 *
333 * @param array $data
334 *
335 * @return int
336 */
337 protected function insertRecord(array $data)
338 {
339 $data = array_intersect_key($data, array_flip($this->fields));
340 $data['tstamp'] = time();
341 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
342 $connection->insert(
343 $this->table,
344 $data
345 );
346 $data['uid'] = $connection->lastInsertId($this->table);
347 $this->updateRefIndex($data['uid']);
348 $this->emitRecordCreatedSignal($data);
349 return $data['uid'];
350 }
351
352 /**
353 * Checks if a file is indexed
354 *
355 * @param File $file
356 * @return bool
357 */
358 public function hasIndexRecord(File $file)
359 {
360 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)->getQueryBuilderForTable($this->table);
361
362 if ((int)$file->_getPropertyRaw('uid') > 0) {
363 $constraints = [
364 $queryBuilder->expr()->eq('uid', $queryBuilder->createNamedParameter($file->getUid(), \PDO::PARAM_INT))
365 ];
366 } else {
367 $constraints = [
368 $queryBuilder->expr()->eq(
369 'storage',
370 $queryBuilder->createNamedParameter($file->getStorage()->getUid(), \PDO::PARAM_INT)
371 ),
372 $queryBuilder->expr()->eq(
373 'identifier',
374 $queryBuilder->createNamedParameter($file->_getPropertyRaw('identifier'), \PDO::PARAM_STR)
375 )
376 ];
377 }
378
379 $count = $queryBuilder
380 ->count('uid')
381 ->from($this->table)
382 ->where(...$constraints)
383 ->execute()
384 ->fetchColumn(0);
385
386 return (bool)$count;
387 }
388
389 /**
390 * Updates the index record in the database
391 *
392 * @param File $file
393 * @return void
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 * @return void
495 */
496 public function updateIndexingTime($fileUid)
497 {
498 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
499 $connection->update(
500 $this->table,
501 [
502 'last_indexed' => time()
503 ],
504 [
505 'uid' => (int)$fileUid
506 ]
507 );
508 }
509
510 /**
511 * Marks given file as missing in sys_file
512 *
513 * @param int $fileUid
514 * @return void
515 */
516 public function markFileAsMissing($fileUid)
517 {
518 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
519 $connection->update(
520 $this->table,
521 [
522 'missing' => 1
523 ],
524 [
525 'uid' => (int)$fileUid
526 ]
527 );
528 $this->emitRecordMarkedAsMissingSignal($fileUid);
529 }
530
531 /**
532 * Remove a sys_file record from the database
533 *
534 * @param int $fileUid
535 * @return void
536 */
537 public function remove($fileUid)
538 {
539 $connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($this->table);
540 $connection->delete(
541 $this->table,
542 [
543 'uid' => (int)$fileUid
544 ]
545 );
546 $this->updateRefIndex($fileUid);
547 $this->emitRecordDeletedSignal($fileUid);
548 }
549
550 /**
551 * Update Reference Index (sys_refindex) for a file
552 *
553 * @param int $id Record UID
554 * @return void
555 */
556 public function updateRefIndex($id)
557 {
558 /** @var $refIndexObj ReferenceIndex */
559 $refIndexObj = GeneralUtility::makeInstance(ReferenceIndex::class);
560 $refIndexObj->updateRefIndexTable($this->table, $id);
561 }
562
563 /*
564 * Get the SignalSlot dispatcher
565 *
566 * @return Dispatcher
567 */
568 protected function getSignalSlotDispatcher()
569 {
570 return $this->getObjectManager()->get(Dispatcher::class);
571 }
572
573 /**
574 * Get the ObjectManager
575 *
576 * @return ObjectManager
577 */
578 protected function getObjectManager()
579 {
580 return GeneralUtility::makeInstance(ObjectManager::class);
581 }
582
583 /**
584 * Signal that is called after an IndexRecord is updated
585 *
586 * @param array $data
587 * @signal
588 */
589 protected function emitRecordUpdatedSignal(array $data)
590 {
591 $this->getSignalSlotDispatcher()->dispatch(self::class, 'recordUpdated', [$data]);
592 }
593
594 /**
595 * Signal that is called after an IndexRecord is created
596 *
597 * @param array $data
598 * @signal
599 */
600 protected function emitRecordCreatedSignal(array $data)
601 {
602 $this->getSignalSlotDispatcher()->dispatch(self::class, 'recordCreated', [$data]);
603 }
604
605 /**
606 * Signal that is called after an IndexRecord is deleted
607 *
608 * @param int $fileUid
609 * @signal
610 */
611 protected function emitRecordDeletedSignal($fileUid)
612 {
613 $this->getSignalSlotDispatcher()->dispatch(self::class, 'recordDeleted', [$fileUid]);
614 }
615
616 /**
617 * Signal that is called after an IndexRecord is marked as missing
618 *
619 * @param int $fileUid
620 * @signal
621 */
622 protected function emitRecordMarkedAsMissingSignal($fileUid)
623 {
624 $this->getSignalSlotDispatcher()->dispatch(self::class, 'recordMarkedAsMissing', [$fileUid]);
625 }
626 }