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