‪TYPO3CMS  11.5
MissingFilesCommand.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;
32 
36 class ‪MissingFilesCommand extends Command
37 {
41  private ‪$connectionPool;
42 
44  {
45  $this->connectionPool = ‪$connectionPool;
46  parent::__construct();
47  }
51  public function ‪configure()
52  {
53  $this
54  ->setHelp('
55 Assumptions:
56 - a perfect integrity of the reference index table (always update the reference index table before using this tool!)
57 - relevant soft reference parsers applied everywhere file references are used inline
58 
59 Files may be missing for these reasons (except software bugs):
60 - 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.
61 - 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.
62 
63 Manual repair suggestions (using --dry-run):
64 - 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.
65 - 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.
66 
67 If the option "--dry-run" is not set, all managed files (TCA/FlexForm attachments) will silently remove the reference
68 from the record since the file is missing. For this reason you might prefer a manual approach instead.
69 All soft references with missing files require manual fix if you consider it an error.
70 
71 If you want to get more detailed information, use the --verbose option.')
72  ->addOption(
73  'dry-run',
74  null,
75  InputOption::VALUE_NONE,
76  'If this option is set, the references will not be removed, but just the output which files would be deleted are shown'
77  )
78  ->addOption(
79  'update-refindex',
80  null,
81  InputOption::VALUE_NONE,
82  'Setting this option automatically updates the reference index and does not ask on command line. Alternatively, use -n to avoid the interactive mode'
83  );
84  }
85 
96  protected function ‪execute(InputInterface $input, OutputInterface ‪$output)
97  {
98  // Make sure the _cli_ user is loaded
100 
101  $io = new SymfonyStyle($input, ‪$output);
102  $io->title($this->getDescription());
103 
104  $dryRun = $input->hasOption('dry-run') && $input->getOption('dry-run') != false ? true : false;
105 
106  // Update the reference index
107  $this->‪updateReferenceIndex($input, $io);
108 
109  // Find missing soft references (cannot be updated / deleted)
110  $missingSoftReferencedFiles = $this->‪findMissingSoftReferencedFiles();
111  if (count($missingSoftReferencedFiles)) {
112  $io->note('Found ' . count($missingSoftReferencedFiles) . ' soft-referenced files that need manual repair.');
113  $io->listing($missingSoftReferencedFiles);
114  }
115 
116  // Find missing references
117  $missingReferencedFiles = $this->‪findMissingReferencedFiles();
118  if (count($missingReferencedFiles)) {
119  $io->note('Found ' . count($missingReferencedFiles) . ' references to non-existing files.');
120 
121  $this->‪removeReferencesToMissingFiles($missingReferencedFiles, $dryRun, $io);
122  $io->success('All references were updated accordingly.');
123  }
124 
125  if (!count($missingSoftReferencedFiles) && !count($missingReferencedFiles)) {
126  $io->success('Nothing to do, no missing files found. Everything is in place.');
127  }
128  return 0;
129  }
130 
140  protected function ‪updateReferenceIndex(InputInterface $input, SymfonyStyle $io)
141  {
142  // Check for reference index to update
143  $io->note('Finding missing files referenced by TYPO3 requires a clean reference index (sys_refindex)');
144  if ($input->hasOption('update-refindex') && $input->getOption('update-refindex')) {
145  $updateReferenceIndex = true;
146  } elseif ($input->isInteractive()) {
147  $updateReferenceIndex = $io->confirm('Should the reference index be updated right now?', false);
148  } else {
149  $updateReferenceIndex = false;
150  }
151 
152  // Update the reference index
153  if ($updateReferenceIndex) {
154  $progressListener = GeneralUtility::makeInstance(ReferenceIndexProgressListener::class);
155  $progressListener->initialize($io);
156  $referenceIndex = GeneralUtility::makeInstance(ReferenceIndex::class);
157  $referenceIndex->updateIndex(false, $progressListener);
158  } else {
159  $io->writeln('Reference index is assumed to be up to date, continuing.');
160  }
161  }
162 
170  protected function ‪findMissingReferencedFiles(): array
171  {
172  $missingReferences = [];
173  // Select all files in the reference table
174  $queryBuilder = $this->connectionPool
175  ->getQueryBuilderForTable('sys_refindex');
176 
177  $result = $queryBuilder
178  ->select('*')
179  ->from('sys_refindex')
180  ->where(
181  $queryBuilder->expr()->eq('ref_table', $queryBuilder->createNamedParameter('_FILE', ‪Connection::PARAM_STR)),
182  $queryBuilder->expr()->isNull('softref_key')
183  )
184  ->executeQuery();
185 
186  // Traverse the references and check if the files exists
187  while ($record = $result->fetchAssociative()) {
188  $fileName = $this->‪getFileNameWithoutAnchor($record['ref_string']);
189  if (empty($record['softref_key']) && !@is_file(‪Environment::getPublicPath() . '/' . $fileName)) {
190  $missingReferences[$fileName][$record['hash']] = $this->‪formatReferenceIndexEntryToString($record);
191  }
192  }
193 
194  return $missingReferences;
195  }
196 
203  protected function ‪findMissingSoftReferencedFiles(): array
204  {
205  $missingReferences = [];
206  // Select all files in the reference table
207  $queryBuilder = $this->connectionPool
208  ->getQueryBuilderForTable('sys_refindex');
209 
210  $result = $queryBuilder
211  ->select('*')
212  ->from('sys_refindex')
213  ->where(
214  $queryBuilder->expr()->eq('ref_table', $queryBuilder->createNamedParameter('_FILE', ‪Connection::PARAM_STR)),
215  $queryBuilder->expr()->isNotNull('softref_key')
216  )
217  ->executeQuery();
218 
219  // Traverse the references and check if the files exists
220  while ($record = $result->fetchAssociative()) {
221  $fileName = $this->‪getFileNameWithoutAnchor($record['ref_string']);
222  if (!@is_file(‪Environment::getPublicPath() . '/' . $fileName)) {
223  $missingReferences[] = $fileName . ' - ' . $record['hash'] . ' - ' . $this->‪formatReferenceIndexEntryToString($record);
224  }
225  }
226  return $missingReferences;
227  }
228 
235  protected function ‪getFileNameWithoutAnchor(string $fileName): string
236  {
237  if (str_contains($fileName, '#')) {
238  [$fileName] = explode('#', $fileName);
239  }
240  return $fileName;
241  }
242 
250  protected function ‪removeReferencesToMissingFiles(array $missingManagedFiles, bool $dryRun, SymfonyStyle $io)
251  {
252  foreach ($missingManagedFiles as $fileName => $references) {
253  if ($io->isVeryVerbose()) {
254  $io->writeln('Deleting references to missing file "' . $fileName . '"');
255  }
256  foreach ($references as $hash => $recordReference) {
257  $io->writeln('Removing reference in record "' . $recordReference . '"');
258  if (!$dryRun) {
259  $sysRefObj = GeneralUtility::makeInstance(ReferenceIndex::class);
260  $error = $sysRefObj->setReferenceValue($hash, null);
261  if ($error) {
262  $io->error('ReferenceIndex::setReferenceValue() reported "' . $error . '"');
263  }
264  }
265  }
266  }
267  }
268 
275  protected function ‪formatReferenceIndexEntryToString(array $record): string
276  {
277  return $record['tablename'] . ':' . $record['recuid'] . ':' . $record['field'] . ':' . $record['flexpointer'] . ':' . $record['softref_key'];
278  }
279 }
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\findMissingSoftReferencedFiles
‪array findMissingSoftReferencedFiles()
Definition: MissingFilesCommand.php:202
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\formatReferenceIndexEntryToString
‪string formatReferenceIndexEntryToString(array $record)
Definition: MissingFilesCommand.php:274
‪TYPO3\CMS\Core\Core\Environment\getPublicPath
‪static string getPublicPath()
Definition: Environment.php:206
‪TYPO3\CMS\Backend\Command\ProgressListener\ReferenceIndexProgressListener
Definition: ReferenceIndexProgressListener.php:30
‪TYPO3\CMS\Core\Database\ReferenceIndex
Definition: ReferenceIndex.php:42
‪TYPO3\CMS\Core\Core\Bootstrap\initializeBackendAuthentication
‪static initializeBackendAuthentication($proceedIfNoUserIsLoggedIn=false)
Definition: Bootstrap.php:588
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\__construct
‪__construct(ConnectionPool $connectionPool)
Definition: MissingFilesCommand.php:42
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\configure
‪configure()
Definition: MissingFilesCommand.php:50
‪TYPO3\CMS\Core\Database\Connection\PARAM_STR
‪const PARAM_STR
Definition: Connection.php:54
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\$connectionPool
‪ConnectionPool $connectionPool
Definition: MissingFilesCommand.php:40
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\getFileNameWithoutAnchor
‪string getFileNameWithoutAnchor(string $fileName)
Definition: MissingFilesCommand.php:234
‪$output
‪$output
Definition: annotationChecker.php:121
‪TYPO3\CMS\Core\Database\Connection
Definition: Connection.php:38
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\findMissingReferencedFiles
‪array findMissingReferencedFiles()
Definition: MissingFilesCommand.php:169
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\execute
‪int execute(InputInterface $input, OutputInterface $output)
Definition: MissingFilesCommand.php:95
‪TYPO3\CMS\Core\Core\Environment
Definition: Environment.php:43
‪TYPO3\CMS\Core\Core\Bootstrap
Definition: Bootstrap.php:70
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand
Definition: MissingFilesCommand.php:37
‪TYPO3\CMS\Lowlevel\Command
Definition: CleanFlexFormsCommand.php:18
‪TYPO3\CMS\Core\Database\ConnectionPool
Definition: ConnectionPool.php:46
‪TYPO3\CMS\Core\Utility\GeneralUtility
Definition: GeneralUtility.php:50
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\removeReferencesToMissingFiles
‪removeReferencesToMissingFiles(array $missingManagedFiles, bool $dryRun, SymfonyStyle $io)
Definition: MissingFilesCommand.php:249
‪TYPO3\CMS\Lowlevel\Command\MissingFilesCommand\updateReferenceIndex
‪updateReferenceIndex(InputInterface $input, SymfonyStyle $io)
Definition: MissingFilesCommand.php:139