[FOLLOWUP][TASK] Use ISO-8601 dates for display and processing
[Packages/TYPO3.CMS.git] / typo3 / sysext / lowlevel / Classes / RteImagesCommand.php
1 <?php
2 namespace TYPO3\CMS\Lowlevel;
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\ConnectionPool;
18 use TYPO3\CMS\Core\Database\ReferenceIndex;
19 use TYPO3\CMS\Core\Utility\File\ExtendedFileUtility;
20 use TYPO3\CMS\Core\Utility\GeneralUtility;
21 use TYPO3\CMS\Core\Utility\PathUtility;
22
23 /**
24 * Looking for RTE images integrity
25 */
26 class RteImagesCommand extends CleanerCommand
27 {
28 /**
29 * @var bool
30 */
31 public $checkRefIndex = true;
32
33 /**
34 * @var ExtendedFileUtility
35 */
36 protected $fileProcObj = null;
37
38 /**
39 * Constructor
40 */
41 public function __construct()
42 {
43 parent::__construct();
44 // Setting up help:
45 $this->cli_help['name'] = 'rte_images -- Looking up all occurencies of RTEmagic images in the database and check existence of parent and copy files on the file system plus report possibly lost files of this type.';
46 $this->cli_help['description'] = trim('
47 Assumptions:
48 - a perfect integrity of the reference index table (always update the reference index table before using this tool!)
49 - that all RTEmagic image files in the database are registered with the soft reference parser "images"
50 - images found in deleted records are included (means that you might find lost RTEmagic images after flushing deleted records)
51
52 The assumptions are not requirements by the TYPO3 API but reflects the de facto implementation of most TYPO3 installations.
53 However, many custom fields using an RTE will probably not have the "images" soft reference parser registered and so the index will be incomplete and not listing all RTEmagic image files.
54 The consequence of this limitation is that you should be careful if you wish to delete lost RTEmagic images - they could be referenced from a field not parsed by the "images" soft reference parser!
55
56 Automatic Repair of Errors:
57 - Will search for double-usages of RTEmagic images and make copies as required.
58 - Lost files can be deleted automatically by setting the value "lostFiles" as an optional parameter to --AUTOFIX, but otherwise delete them manually if you do not recognize them as used somewhere the system does not know about.
59
60 Manual repair suggestions:
61 - Missing files: Re-insert missing files or edit record where the reference is found.
62 ');
63 $this->cli_help['examples'] = '/.../cli_dispatch.phpsh lowlevel_cleaner rte_images -s -r
64 Reports problems with RTE images';
65 }
66
67 /**
68 * Analyse situation with RTE magic images. (still to define what the most useful output is).
69 * Fix methods: API in \TYPO3\CMS\Core\Database\ReferenceIndex that allows to
70 * change the value of a reference (we could copy the files) or remove reference
71 *
72 * @return array
73 */
74 public function main()
75 {
76 // Initialize result array:
77 $resultArray = [
78 'message' => $this->cli_help['name'] . LF . LF . $this->cli_help['description'],
79 'headers' => [
80 'completeFileList' => ['Complete list of used RTEmagic files', 'Both parent and copy are listed here including usage count (which should in theory all be "1"). This list does not exclude files that might be missing.', 1],
81 'RTEmagicFilePairs' => ['Statistical info about RTEmagic files', '(copy used as index)', 0],
82 'doubleFiles' => ['Duplicate RTEmagic image files', 'These files are RTEmagic images found used in multiple records! RTEmagic images should be used by only one record at a time. A large amount of such images probably stems from previous versions of TYPO3 (before 4.2) which did not support making copies automatically of RTEmagic images in case of new copies / versions.', 3],
83 'missingFiles' => ['Missing RTEmagic image files', 'These files are not found in the file system! Should be corrected!', 3],
84 'lostFiles' => ['Lost RTEmagic files from uploads/', 'These files you might be able to delete but only if _all_ RTEmagic images are found by the soft reference parser. If you are using the RTE in third-party extensions it is likely that the soft reference parser is not applied correctly to their RTE and thus these "lost" files actually represent valid RTEmagic images, just not registered. Lost files can be auto-fixed but only if you specifically set "lostFiles" as parameter to the --AUTOFIX option.', 2]
85 ],
86 'RTEmagicFilePairs' => [],
87 'doubleFiles' => [],
88 'completeFileList' => [],
89 'missingFiles' => [],
90 'lostFiles' => []
91 ];
92 // Select all RTEmagic files in the reference table (only from soft references of course)
93 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class)
94 ->getQueryBuilderForTable('sys_refindex');
95
96 $result = $queryBuilder
97 ->select('*')
98 ->from('sys_refindex')
99 ->where(
100 $queryBuilder->expr()->eq(
101 'ref_table',
102 $queryBuilder->createNamedParameter('_FILE', \PDO::PARAM_STR)
103 ),
104 $queryBuilder->expr()->like(
105 'ref_string',
106 $queryBuilder->createNamedParameter('%/RTEmagic%', \PDO::PARAM_STR)
107 ),
108 $queryBuilder->expr()->eq(
109 'softref_key',
110 $queryBuilder->createNamedParameter('images', \PDO::PARAM_STR)
111 )
112 )
113 ->orderBy('sorting', 'DESC')
114 ->execute();
115
116 // Traverse the files and put into a large table:
117
118 while ($rec = $result->fetch()) {
119 $filename = basename($rec['ref_string']);
120 if (GeneralUtility::isFirstPartOfStr($filename, 'RTEmagicC_')) {
121 $original = 'RTEmagicP_' . preg_replace('/\\.[[:alnum:]]+$/', '', substr($filename, 10));
122 $infoString = $this->infoStr($rec);
123 // Build index:
124 $resultArray['RTEmagicFilePairs'][$rec['ref_string']]['exists'] = @is_file((PATH_site . $rec['ref_string']));
125 $resultArray['RTEmagicFilePairs'][$rec['ref_string']]['original'] = substr($rec['ref_string'], 0, -strlen($filename)) . $original;
126 $resultArray['RTEmagicFilePairs'][$rec['ref_string']]['original_exists'] = @is_file((PATH_site . $resultArray['RTEmagicFilePairs'][$rec['ref_string']]['original']));
127 $resultArray['RTEmagicFilePairs'][$rec['ref_string']]['count']++;
128 $resultArray['RTEmagicFilePairs'][$rec['ref_string']]['usedIn'][$rec['hash']] = $infoString;
129 $resultArray['completeFileList'][$resultArray['RTEmagicFilePairs'][$rec['ref_string']]['original']]++;
130 $resultArray['completeFileList'][$rec['ref_string']]++;
131 // Missing files:
132 if (!$resultArray['RTEmagicFilePairs'][$rec['ref_string']]['exists']) {
133 $resultArray['missingFiles'][$rec['ref_string']] = $resultArray['RTEmagicFilePairs'][$rec['ref_string']]['usedIn'];
134 }
135 if (!$resultArray['RTEmagicFilePairs'][$rec['ref_string']]['original_exists']) {
136 $resultArray['missingFiles'][$resultArray['RTEmagicFilePairs'][$rec['ref_string']]['original']] = $resultArray['RTEmagicFilePairs'][$rec['ref_string']]['usedIn'];
137 }
138 }
139 }
140 // Searching for duplicates:
141 foreach ($resultArray['RTEmagicFilePairs'] as $fileName => $fileInfo) {
142 if ($fileInfo['count'] > 1 && $fileInfo['exists'] && $fileInfo['original_exists']) {
143 $resultArray['doubleFiles'][$fileName] = $fileInfo['usedIn'];
144 }
145 }
146
147 // Now, ask for RTEmagic files inside uploads/ folder:
148 $cleanerModules = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lowlevel']['cleanerModules'];
149 $cleanerMode = GeneralUtility::getUserObj($cleanerModules['lost_files'][0]);
150 $resLostFiles = $cleanerMode->main([], false, true);
151 if (is_array($resLostFiles['RTEmagicFiles'])) {
152 foreach ($resLostFiles['RTEmagicFiles'] as $fileName) {
153 if (!isset($resultArray['completeFileList'][$fileName])) {
154 $resultArray['lostFiles'][$fileName] = $fileName;
155 }
156 }
157 }
158 ksort($resultArray['RTEmagicFilePairs']);
159 ksort($resultArray['completeFileList']);
160 ksort($resultArray['missingFiles']);
161 ksort($resultArray['doubleFiles']);
162 ksort($resultArray['lostFiles']);
163 return $resultArray;
164 }
165
166 /**
167 * Mandatory autofix function
168 * Will run auto-fix on the result array. Echos status during processing.
169 *
170 * @param array $resultArray Result array from main() function
171 * @return void
172 */
173 public function main_autoFix($resultArray)
174 {
175 $limitTo = $this->cli_args['--AUTOFIX'][0];
176 if (is_array($resultArray['doubleFiles'])) {
177 if (!$limitTo || $limitTo === 'doubleFiles') {
178 echo 'FIXING double-usages of RTE files in uploads/: ' . LF;
179 foreach ($resultArray['RTEmagicFilePairs'] as $fileName => $fileInfo) {
180 // Only fix something if there is a usage count of more than 1 plus if both original and copy exists:
181 if ($fileInfo['count'] > 1 && $fileInfo['exists'] && $fileInfo['original_exists']) {
182 // Traverse all records using the file:
183 $c = 0;
184 foreach ($fileInfo['usedIn'] as $hash => $recordID) {
185 if ($c == 0) {
186 echo ' Keeping file ' . $fileName . ' for record ' . $recordID . LF;
187 } else {
188 // CODE below is adapted from \TYPO3\CMS\Impexp\ImportExport where there is support for duplication of RTE images:
189 echo ' Copying file ' . basename($fileName) . ' for record ' . $recordID . ' ';
190 // Initialize; Get directory prefix for file and set the original name:
191 $dirPrefix = dirname($fileName) . '/';
192 $rteOrigName = basename($fileInfo['original']);
193 // If filename looks like an RTE file, and the directory is in "uploads/", then process as a RTE file!
194 if ($rteOrigName && GeneralUtility::isFirstPartOfStr($dirPrefix, 'uploads/') && @is_dir((PATH_site . $dirPrefix))) {
195 // RTE:
196 // From the "original" RTE filename, produce a new "original" destination filename which is unused.
197 $fileProcObj = $this->getFileProcObj();
198 $origDestName = $fileProcObj->getUniqueName($rteOrigName, PATH_site . $dirPrefix);
199 // Create copy file name:
200 $pI = pathinfo($fileName);
201 $copyDestName = dirname($origDestName) . '/RTEmagicC_' . substr(basename($origDestName), 10) . '.' . $pI['extension'];
202 if (!@is_file($copyDestName) && !@is_file($origDestName) && $origDestName === GeneralUtility::getFileAbsFileName($origDestName) && $copyDestName === GeneralUtility::getFileAbsFileName($copyDestName)) {
203 echo ' to ' . basename($copyDestName);
204 if ($bypass = $this->cli_noExecutionCheck($fileName)) {
205 echo $bypass;
206 } else {
207 // Making copies:
208 GeneralUtility::upload_copy_move(PATH_site . $fileInfo['original'], $origDestName);
209 GeneralUtility::upload_copy_move(PATH_site . $fileName, $copyDestName);
210 clearstatcache();
211 if (@is_file($copyDestName)) {
212 $sysRefObj = GeneralUtility::makeInstance(ReferenceIndex::class);
213 $error = $sysRefObj->setReferenceValue($hash, PathUtility::stripPathSitePrefix($copyDestName));
214 if ($error) {
215 echo ' - ERROR: TYPO3\\CMS\\Core\\Database\\ReferenceIndex::setReferenceValue(): ' . $error . LF;
216 die;
217 } else {
218 echo ' - DONE';
219 }
220 } else {
221 echo ' - ERROR: File "' . $copyDestName . '" was not created!';
222 }
223 }
224 } else {
225 echo ' - ERROR: Could not construct new unique names for file!';
226 }
227 } else {
228 echo ' - ERROR: Maybe directory of file was not within "uploads/"?';
229 }
230 echo LF;
231 }
232 $c++;
233 }
234 }
235 }
236 } else {
237 echo 'Bypassing fixing of double-usages since --AUTOFIX was not "doubleFiles"' . LF;
238 }
239 }
240 if (is_array($resultArray['lostFiles'])) {
241 if ($limitTo === 'lostFiles') {
242 echo 'Removing lost RTEmagic files from folders inside uploads/: ' . LF;
243 foreach ($resultArray['lostFiles'] as $key => $value) {
244 $absFileName = GeneralUtility::getFileAbsFileName($value);
245 echo 'Deleting file: "' . $absFileName . '": ';
246 if ($bypass = $this->cli_noExecutionCheck($absFileName)) {
247 echo $bypass;
248 } else {
249 if ($absFileName && @is_file($absFileName)) {
250 unlink($absFileName);
251 echo 'DONE';
252 } else {
253 echo ' ERROR: File "' . $absFileName . '" was not found!';
254 }
255 }
256 echo LF;
257 }
258 }
259 } else {
260 echo 'Bypassing fixing of double-usages since --AUTOFIX was not "lostFiles"' . LF;
261 }
262 }
263
264 /**
265 * Returns file processing object, initialized only once.
266 *
267 * @return ExtendedFileUtility File processor object
268 */
269 public function getFileProcObj()
270 {
271 if (!is_object($this->fileProcObj)) {
272 $this->fileProcObj = GeneralUtility::makeInstance(ExtendedFileUtility::class);
273 $this->fileProcObj->setActionPermissions();
274 }
275 return $this->fileProcObj;
276 }
277 }