‪TYPO3CMS  10.4
FilesWithMultipleReferencesCommand.php
Go to the documentation of this file.
1 <?php
2 
3 declare(strict_types=1);
4 
5 /*
6  * This file is part of the TYPO3 CMS project.
7  *
8  * It is free software; you can redistribute it and/or modify it under
9  * the terms of the GNU General Public License, either version 2
10  * of the License, or any later version.
11  *
12  * For the full copyright and license information, please read the
13  * LICENSE.txt file that was distributed with this source code.
14  *
15  * The TYPO3 project - inspiring people to share!
16  */
17 
19 
20 use Symfony\Component\Console\Command\Command;
21 use Symfony\Component\Console\Input\InputInterface;
22 use Symfony\Component\Console\Input\InputOption;
23 use Symfony\Component\Console\Output\OutputInterface;
24 use Symfony\Component\Console\Style\SymfonyStyle;
33 
38 {
39 
43  public function ‪configure()
44  {
45  $this
46  ->setDescription('Looking for files from TYPO3 managed records which are referenced more than once')
47  ->setHelp('
48 Assumptions:
49 - a perfect integrity of the reference index table (always update the reference index table before using this tool!)
50 - files found in deleted records are included (otherwise you would see a false list of lost files)
51 
52 Files attached to records in TYPO3 using a "group" type configuration in TCA or FlexForm DataStructure are managed exclusively by the system and there must always exist a 1-1 reference between the file and the reference in the record.
53 This tool will expose when such files are referenced from multiple locations which is considered an integrity error.
54 If a multi-reference is found it was typically created because the record was copied or modified outside of DataHandler which will otherwise maintain the relations correctly.
55 Multi-references should be resolved to 1-1 references as soon as possible. The danger of keeping multi-references is that if the file is removed from one of the referring records it will actually be deleted in the file system, leaving missing files for the remaining referrers!
56 
57 If the option "--dry-run" is not set, the files that are referenced multiple times are copied with a new name
58 and the references are updated accordingly.
59 Warning: First, make sure those files are not used somewhere TYPO3 does not know about!
60 
61 If you want to get more detailed information, use the --verbose option.')
62  ->addOption(
63  'dry-run',
64  null,
65  InputOption::VALUE_NONE,
66  'If this option is set, the files will not actually be deleted, but just the output which files would be deleted are shown'
67  )
68  ->addOption(
69  'update-refindex',
70  null,
71  InputOption::VALUE_NONE,
72  'Setting this option automatically updates the reference index and does not ask on command line. Alternatively, use -n to avoid the interactive mode'
73  );
74  }
75 
86  protected function ‪execute(InputInterface $input, OutputInterface ‪$output)
87  {
88  // Make sure the _cli_ user is loaded
90 
91  $io = new SymfonyStyle($input, ‪$output);
92  $io->title($this->getDescription());
93 
94  $dryRun = $input->hasOption('dry-run') && $input->getOption('dry-run') != false ? true : false;
95 
96  $this->‪updateReferenceIndex($input, $io);
97 
98  // Find files which are referenced multiple times
99  $doubleFiles = $this->‪findMultipleReferencedFiles();
100 
101  if (count($doubleFiles)) {
102  if (!$io->isQuiet()) {
103  $io->note('Found ' . count($doubleFiles) . ' files that are referenced more than once.');
104  if ($io->isVerbose()) {
105  $io->listing($doubleFiles);
106  }
107  }
108 
109  $this->‪copyMultipleReferencedFiles($doubleFiles, $dryRun, $io);
110  $io->success('Cleaned up ' . count($doubleFiles) . ' files which have been referenced multiple times.');
111  } else {
112  $io->success('Nothing to do, no files found which are referenced more than once.');
113  }
114  return 0;
115  }
116 
126  protected function ‪updateReferenceIndex(InputInterface $input, SymfonyStyle $io)
127  {
128  // Check for reference index to update
129  $io->note('Finding files referenced multiple times in records managed by TYPO3 requires a clean reference index (sys_refindex)');
130  $updateReferenceIndex = false;
131  if ($input->hasOption('update-refindex') && $input->getOption('update-refindex')) {
132  $updateReferenceIndex = true;
133  } elseif ($input->isInteractive()) {
134  $updateReferenceIndex = $io->confirm('Should the reference index be updated right now?', false);
135  }
136 
137  // Update the reference index
138  if ($updateReferenceIndex) {
139  $progressListener = GeneralUtility::makeInstance(ReferenceIndexProgressListener::class);
140  $progressListener->initialize($io);
141 
142  $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
143  $io->section('Reference Index is now being updated');
144  $referenceIndex->updateIndex(false, $progressListener);
145  } else {
146  $io->writeln('Reference index is assumed to be up to date, continuing.');
147  }
148  }
149 
155  protected function ‪findMultipleReferencedFiles(): array
156  {
157  $multipleReferencesList = [];
158 
159  // Select all files in the reference table not found by a soft reference parser (thus TCA configured)
160  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
161  ->getQueryBuilderForTable('sys_refindex');
162 
163  $result = $queryBuilder
164  ->select('*')
165  ->from('sys_refindex')
166  ->where(
167  $queryBuilder->expr()->eq('ref_table', $queryBuilder->createNamedParameter('_FILE', \PDO::PARAM_STR)),
168  $queryBuilder->expr()->eq('softref_key', $queryBuilder->createNamedParameter('', \PDO::PARAM_STR))
169  )
170  ->execute();
171 
172  // Traverse the files and put into a large table
173  $allReferencesToFiles = [];
174  while ($record = $result->fetch()) {
175  // Compile info string for location of reference
176  $infoString = $this->‪formatReferenceIndexEntryToString($record);
177  $hash = $record['hash'];
178  $fileName = $record['ref_string'];
179  // Add entry if file has multiple references pointing to it
180  if (isset($allReferencesToFiles[$fileName])) {
181  if (!is_array($multipleReferencesList[$fileName])) {
182  $multipleReferencesList[$fileName] = [];
183  $multipleReferencesList[$fileName][$allReferencesToFiles[$fileName]['hash']] = $allReferencesToFiles[$fileName]['infoString'];
184  }
185  $multipleReferencesList[$fileName][$hash] = $infoString;
186  } else {
187  $allReferencesToFiles[$fileName] = [
188  'infoString' => $infoString,
189  'hash' => $hash
190  ];
191  }
192  }
193 
194  return ‪ArrayUtility::sortByKeyRecursive($multipleReferencesList);
195  }
196 
204  protected function ‪copyMultipleReferencedFiles(array $multipleReferencesToFiles, bool $dryRun, SymfonyStyle $io)
205  {
206  $fileFunc = GeneralUtility::makeInstance(BasicFileUtility::class);
207  $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
208 
209  foreach ($multipleReferencesToFiles as $fileName => $usages) {
210  $absoluteFileName = GeneralUtility::getFileAbsFileName($fileName);
211  if ($absoluteFileName && @is_file($absoluteFileName)) {
212  if ($io->isVeryVerbose()) {
213  $io->writeln('Processing file "' . $absoluteFileName . '"');
214  }
215  $counter = 0;
216  foreach ($usages as $hash => $recReference) {
217  if ($counter++ === 0) {
218  $io->writeln('Keeping "' . $fileName . '" for record "' . $recReference . '"');
219  } else {
220  // Create unique name for file
221  $newName = (string)$fileFunc->getUniqueName(‪PathUtility::basename($fileName), ‪PathUtility::dirname($absoluteFileName));
222  $io->writeln('Copying "' . $fileName . '" to "' . ‪PathUtility::stripPathSitePrefix($newName) . '" for record "' . $recReference . '"');
223  if (!$dryRun) {
224  GeneralUtility::upload_copy_move($absoluteFileName, $newName);
225  clearstatcache();
226  if (@is_file($newName)) {
227  $error = $referenceIndex->setReferenceValue($hash, ‪PathUtility::basename($newName));
228  if ($error) {
229  $io->error('ReferenceIndex::setReferenceValue() reported "' . $error . '"');
230  }
231  } else {
232  $io->error('File "' . $newName . '" could not be created.');
233  }
234  }
235  }
236  }
237  } else {
238  $io->error('File "' . $absoluteFileName . '" was not found.');
239  }
240  }
241  }
242 
249  protected function ‪formatReferenceIndexEntryToString(array $record): string
250  {
251  return $record['tablename']
252  . ':' . $record['recuid']
253  . ':' . $record['field']
254  . ($record['flexpointer'] ? ':' . $record['flexpointer'] : '')
255  . ($record['softref_key'] ? ':' . $record['softref_key'] . ' (Soft Reference) ' : '')
256  . ($record['deleted'] ? ' (DELETED)' : '');
257  }
258 }
‪TYPO3\CMS\Core\Utility\PathUtility
Definition: PathUtility.php:24
‪TYPO3\CMS\Lowlevel\Command\FilesWithMultipleReferencesCommand\formatReferenceIndexEntryToString
‪string formatReferenceIndexEntryToString(array $record)
Definition: FilesWithMultipleReferencesCommand.php:249
‪TYPO3\CMS\Backend\Command\ProgressListener\ReferenceIndexProgressListener
Definition: ReferenceIndexProgressListener.php:30
‪TYPO3\CMS\Core\Utility\File\BasicFileUtility
Definition: BasicFileUtility.php:35
‪TYPO3\CMS\Lowlevel\Command\FilesWithMultipleReferencesCommand
Definition: FilesWithMultipleReferencesCommand.php:38
‪TYPO3\CMS\Core\Utility\PathUtility\dirname
‪static string dirname($path)
Definition: PathUtility.php:186
‪TYPO3\CMS\Core\Utility\PathUtility\stripPathSitePrefix
‪static string stripPathSitePrefix($path)
Definition: PathUtility.php:372
‪TYPO3\CMS\Core\Database\ReferenceIndex
Definition: ReferenceIndex.php:48
‪TYPO3\CMS\Lowlevel\Command\FilesWithMultipleReferencesCommand\execute
‪int execute(InputInterface $input, OutputInterface $output)
Definition: FilesWithMultipleReferencesCommand.php:86
‪TYPO3\CMS\Core\Core\Bootstrap\initializeBackendAuthentication
‪static initializeBackendAuthentication($proceedIfNoUserIsLoggedIn=false)
Definition: Bootstrap.php:607
‪TYPO3\CMS\Core\Utility\ArrayUtility\sortByKeyRecursive
‪static array sortByKeyRecursive(array $array)
Definition: ArrayUtility.php:354
‪TYPO3\CMS\Lowlevel\Command\FilesWithMultipleReferencesCommand\copyMultipleReferencedFiles
‪copyMultipleReferencedFiles(array $multipleReferencesToFiles, bool $dryRun, SymfonyStyle $io)
Definition: FilesWithMultipleReferencesCommand.php:204
‪TYPO3\CMS\Core\Utility\PathUtility\basename
‪static string basename($path)
Definition: PathUtility.php:165
‪TYPO3\CMS\Lowlevel\Command\FilesWithMultipleReferencesCommand\updateReferenceIndex
‪updateReferenceIndex(InputInterface $input, SymfonyStyle $io)
Definition: FilesWithMultipleReferencesCommand.php:126
‪TYPO3\CMS\Lowlevel\Command\FilesWithMultipleReferencesCommand\findMultipleReferencedFiles
‪array findMultipleReferencedFiles()
Definition: FilesWithMultipleReferencesCommand.php:155
‪$output
‪$output
Definition: annotationChecker.php:119
‪TYPO3\CMS\Core\Utility\ArrayUtility
Definition: ArrayUtility.php:24
‪TYPO3\CMS\Core\Core\Bootstrap
Definition: Bootstrap.php:66
‪TYPO3\CMS\Lowlevel\Command
Definition: CleanFlexFormsCommand.php:18
‪TYPO3\CMS\Lowlevel\Command\FilesWithMultipleReferencesCommand\configure
‪configure()
Definition: FilesWithMultipleReferencesCommand.php:43
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:46