‪TYPO3CMS  9.5
MissingFilesCommand.php
Go to the documentation of this file.
1 <?php
2 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 
18 use Symfony\Component\Console\Command\Command;
19 use Symfony\Component\Console\Input\InputInterface;
20 use Symfony\Component\Console\Input\InputOption;
21 use Symfony\Component\Console\Output\OutputInterface;
22 use Symfony\Component\Console\Style\SymfonyStyle;
28 
32 class ‪MissingFilesCommand extends Command
33 {
34 
38  public function ‪configure()
39  {
40  $this
41  ->setDescription('Find all file references from records pointing to a missing (non-existing) file.')
42  ->setHelp('
43 Assumptions:
44 - a perfect integrity of the reference index table (always update the reference index table before using this tool!)
45 - relevant soft reference parsers applied everywhere file references are used inline
46 
47 Files may be missing for these reasons (except software bugs):
48 - someone manually deleted the file inside fileadmin/ or another user maintained folder. If the reference was a soft reference (opposite to a DataHandler managed file relation from "group" type fields), technically it is not an error although it might be a mistake that someone did so.
49 - someone manually deleted the file inside the uploads/ folder (typically containing managed files) which is an error since no user interaction should take place there.
50 
51 Manual repair suggestions (using --dry-run):
52 - Managed files: You might be able to locate the file and re-insert it in the correct location. However, no automatic fix can do that for you.
53 - Soft References: You should investigate each case and edit the content accordingly. A soft reference to a file could be in an HTML image tag (for example <img src="missing_file.jpg" />) and you would have to either remove the whole tag, change the filename or re-create the missing file.
54 
55 If the option "--dry-run" is not set, all managed files (TCA/FlexForm attachments) will silently remove the reference
56 from the record since the file is missing. For this reason you might prefer a manual approach instead.
57 All soft references with missing files require manual fix if you consider it an error.
58 
59 If you want to get more detailed information, use the --verbose option.')
60  ->addOption(
61  'dry-run',
62  null,
63  InputOption::VALUE_NONE,
64  'If this option is set, the references will not be removed, but just the output which files would be deleted are shown'
65  )
66  ->addOption(
67  'update-refindex',
68  null,
69  InputOption::VALUE_NONE,
70  'Setting this option automatically updates the reference index and does not ask on command line. Alternatively, use -n to avoid the interactive mode'
71  );
72  }
73 
83  protected function ‪execute(InputInterface $input, OutputInterface ‪$output)
84  {
85  // Make sure the _cli_ user is loaded
87 
88  $io = new SymfonyStyle($input, ‪$output);
89  $io->title($this->getDescription());
90 
91  $dryRun = $input->hasOption('dry-run') && $input->getOption('dry-run') != false ? true : false;
92 
93  // Update the reference index
94  $this->‪updateReferenceIndex($input, $io);
95 
96  // Find missing soft references (cannot be updated / deleted)
97  $missingSoftReferencedFiles = $this->‪findMissingSoftReferencedFiles();
98  if (count($missingSoftReferencedFiles)) {
99  $io->note('Found ' . count($missingSoftReferencedFiles) . ' soft-referenced files that need manual repair.');
100  $io->listing($missingSoftReferencedFiles);
101  }
102 
103  // Find missing references
104  $missingReferencedFiles = $this->‪findMissingReferencedFiles();
105  if (count($missingReferencedFiles)) {
106  $io->note('Found ' . count($missingReferencedFiles) . ' references to non-existing files.');
107 
108  $this->‪removeReferencesToMissingFiles($missingReferencedFiles, $dryRun, $io);
109  $io->success('All references were updated accordingly.');
110  }
111 
112  if (!count($missingSoftReferencedFiles) && !count($missingReferencedFiles)) {
113  $io->success('Nothing to do, no missing files found. Everything is in place.');
114  }
115  }
116 
126  protected function ‪updateReferenceIndex(InputInterface $input, SymfonyStyle $io)
127  {
128  // Check for reference index to update
129  $io->note('Finding missing files referenced by TYPO3 requires a clean reference index (sys_refindex)');
130  if ($input->hasOption('update-refindex') && $input->getOption('update-refindex')) {
131  $updateReferenceIndex = true;
132  } elseif ($input->isInteractive()) {
133  $updateReferenceIndex = $io->confirm('Should the reference index be updated right now?', false);
134  } else {
135  $updateReferenceIndex = false;
136  }
137 
138  // Update the reference index
139  if ($updateReferenceIndex) {
140  $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
141  $referenceIndex->updateIndex(false, !$io->isQuiet());
142  } else {
143  $io->writeln('Reference index is assumed to be up to date, continuing.');
144  }
145  }
146 
154  protected function ‪findMissingReferencedFiles(): array
155  {
156  $missingReferences = [];
157  // Select all files in the reference table
158  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
159  ->getQueryBuilderForTable('sys_refindex');
160 
161  $result = $queryBuilder
162  ->select('*')
163  ->from('sys_refindex')
164  ->where(
165  $queryBuilder->expr()->eq('ref_table', $queryBuilder->createNamedParameter('_FILE', \PDO::PARAM_STR)),
166  $queryBuilder->expr()->isNull('softref_key')
167  )
168  ->execute();
169 
170  // Traverse the references and check if the files exists
171  while ($record = $result->fetch()) {
172  $fileName = $this->‪getFileNameWithoutAnchor($record['ref_string']);
173  if (empty($record['softref_key']) && !@is_file(‪Environment::getPublicPath() . '/' . $fileName)) {
174  $missingReferences[$fileName][$record['hash']] = $this->‪formatReferenceIndexEntryToString($record);
175  }
176  }
177 
178  return $missingReferences;
179  }
180 
187  protected function ‪findMissingSoftReferencedFiles(): array
188  {
189  $missingReferences = [];
190  // Select all files in the reference table
191  $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
192  ->getQueryBuilderForTable('sys_refindex');
193 
194  $result = $queryBuilder
195  ->select('*')
196  ->from('sys_refindex')
197  ->where(
198  $queryBuilder->expr()->eq('ref_table', $queryBuilder->createNamedParameter('_FILE', \PDO::PARAM_STR)),
199  $queryBuilder->expr()->isNotNull('softref_key')
200  )
201  ->execute();
202 
203  // Traverse the references and check if the files exists
204  while ($record = $result->fetch()) {
205  $fileName = $this->‪getFileNameWithoutAnchor($record['ref_string']);
206  if (!@is_file(‪Environment::getPublicPath() . '/' . $fileName)) {
207  $missingReferences[] = $fileName . ' - ' . $record['hash'] . ' - ' . $this->‪formatReferenceIndexEntryToString($record);
208  }
209  }
210  return $missingReferences;
211  }
212 
219  protected function ‪getFileNameWithoutAnchor(string $fileName): string
220  {
221  if (strpos($fileName, '#') !== false) {
222  [$fileName] = explode('#', $fileName);
223  }
224  return $fileName;
225  }
226 
234  protected function ‪removeReferencesToMissingFiles(array $missingManagedFiles, bool $dryRun, SymfonyStyle $io)
235  {
236  foreach ($missingManagedFiles as $fileName => $references) {
237  if ($io->isVeryVerbose()) {
238  $io->writeln('Deleting references to missing file "' . $fileName . '"');
239  }
240  foreach ($references as $hash => $recordReference) {
241  $io->writeln('Removing reference in record "' . $recordReference . '"');
242  if (!$dryRun) {
243  $sysRefObj = GeneralUtility::makeInstance(ReferenceIndex::class);
244  $error = $sysRefObj->setReferenceValue($hash, null);
245  if ($error) {
246  $io->error('ReferenceIndex::setReferenceValue() reported "' . $error . '"');
247  }
248  }
249  }
250  }
251  }
252 
259  protected function ‪formatReferenceIndexEntryToString(array $record): string
260  {
261  return $record['tablename'] . ':' . $record['recuid'] . ':' . $record['field'] . ':' . $record['flexpointer'] . ':' . $record['softref_key'] . ($record['deleted'] ? ' (DELETED)' : '');
262  }
263 }
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\findMissingSoftReferencedFiles
‪array findMissingSoftReferencedFiles()
Definition: MissingFilesCommand.php:187
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\formatReferenceIndexEntryToString
‪string formatReferenceIndexEntryToString(array $record)
Definition: MissingFilesCommand.php:259
‪TYPO3\CMS\Core\Core\Environment\getPublicPath
‪static string getPublicPath()
Definition: Environment.php:153
‪TYPO3\CMS\Core\Database\ReferenceIndex
Definition: ReferenceIndex.php:50
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\configure
‪configure()
Definition: MissingFilesCommand.php:38
‪TYPO3\CMS\Core\Core\Bootstrap\initializeBackendAuthentication
‪static Bootstrap null initializeBackendAuthentication($proceedIfNoUserIsLoggedIn=false)
Definition: Bootstrap.php:974
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\getFileNameWithoutAnchor
‪string getFileNameWithoutAnchor(string $fileName)
Definition: MissingFilesCommand.php:219
‪$output
‪$output
Definition: annotationChecker.php:113
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\findMissingReferencedFiles
‪array findMissingReferencedFiles()
Definition: MissingFilesCommand.php:154
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:39
‪TYPO3\CMS\Core\Core\Bootstrap
Definition: Bootstrap.php:50
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand
Definition: MissingFilesCommand.php:33
‪TYPO3\CMS\Lowlevel\Command
Definition: CleanFlexFormsCommand.php:3
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:44
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:45
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\removeReferencesToMissingFiles
‪removeReferencesToMissingFiles(array $missingManagedFiles, bool $dryRun, SymfonyStyle $io)
Definition: MissingFilesCommand.php:234
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\updateReferenceIndex
‪updateReferenceIndex(InputInterface $input, SymfonyStyle $io)
Definition: MissingFilesCommand.php:126
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\execute
‪execute(InputInterface $input, OutputInterface $output)
Definition: MissingFilesCommand.php:83