[TASK] Replace PATH_site with Environment API in various exts
[Packages/TYPO3.CMS.git] / typo3 / sysext / lowlevel / Classes / Command / MissingFilesCommand.php
1 <?php
2 declare(strict_types = 1);
3 namespace TYPO3\CMS\Lowlevel\Command;
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;
23 use TYPO3\CMS\Core\Core\Bootstrap;
24 use TYPO3\CMS\Core\Core\Environment;
25 use TYPO3\CMS\Core\Database\ConnectionPool;
26 use TYPO3\CMS\Core\Database\ReferenceIndex;
27 use TYPO3\CMS\Core\Utility\GeneralUtility;
28
29 /**
30 * Finds files which are referenced by TYPO3 but not found in the file system
31 */
32 class MissingFilesCommand extends Command
33 {
34
35 /**
36 * Configure the command by defining the name, options and arguments
37 */
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
74 /**
75 * Executes the command to
76 * - optionally update the reference index (to have clean data)
77 * - find data in sys_refindex (softrefs and regular references) where the actual file does not exist (anymore)
78 * - remove these files if --dry-run is not set (not possible for refindexes)
79 *
80 * @param InputInterface $input
81 * @param OutputInterface $output
82 */
83 protected function execute(InputInterface $input, OutputInterface $output)
84 {
85 // Make sure the _cli_ user is loaded
86 Bootstrap::initializeBackendAuthentication();
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
117 /**
118 * Function to update the reference index
119 * - if the option --update-refindex is set, do it
120 * - otherwise, if in interactive mode (not having -n set), ask the user
121 * - otherwise assume everything is fine
122 *
123 * @param InputInterface $input holds information about entered parameters
124 * @param SymfonyStyle $io necessary for outputting information
125 */
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
147 /**
148 * Find file references that points to non-existing files in system
149 * Fix methods: API in \TYPO3\CMS\Core\Database\ReferenceIndex that allows to
150 * change the value of a reference (or remove it)
151 *
152 * @return array an array of records within sys_refindex
153 */
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 = $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
181 /**
182 * Find file references that points to non-existing files in system
183 * registered as soft references (checked for "softref_key")
184 *
185 * @return array an array of the data within soft references
186 */
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 = $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
213 /**
214 * Removes all references in the sys_file_reference where files were not found
215 *
216 * @param array $missingManagedFiles Contains the records of sys_refindex which need to be updated
217 * @param bool $dryRun if set, the references are just displayed, but not removed
218 * @param SymfonyStyle $io the IO object for output
219 */
220 protected function removeReferencesToMissingFiles(array $missingManagedFiles, bool $dryRun, SymfonyStyle $io)
221 {
222 foreach ($missingManagedFiles as $fileName => $references) {
223 if ($io->isVeryVerbose()) {
224 $io->writeln('Deleting references to missing file "' . $fileName . '"');
225 }
226 foreach ($references as $hash => $recordReference) {
227 $io->writeln('Removing reference in record "' . $recordReference . '"');
228 if (!$dryRun) {
229 $sysRefObj = GeneralUtility::makeInstance(ReferenceIndex::class);
230 $error = $sysRefObj->setReferenceValue($hash, null);
231 if ($error) {
232 $io->error('ReferenceIndex::setReferenceValue() reported "' . $error . '"');
233 }
234 }
235 }
236 }
237 }
238
239 /**
240 * Formats a sys_refindex entry to something readable
241 *
242 * @param array $record
243 * @return string
244 */
245 protected function formatReferenceIndexEntryToString(array $record): string
246 {
247 return $record['tablename'] . ':' . $record['recuid'] . ':' . $record['field'] . ':' . $record['flexpointer'] . ':' . $record['softref_key'] . ($record['deleted'] ? ' (DELETED)' : '');
248 }
249 }