[BUGFIX] Fix checkbox items in "Global Configuration" card
[Packages/TYPO3.CMS.git] / Build / Scripts / checkIntegrityCsvFixtures.php
1 #!/usr/bin/env php
2 <?php
3 declare(strict_types=1);
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\Utility\MathUtility;
18
19 require __DIR__ . '/../../vendor/autoload.php';
20
21 if (PHP_SAPI !== 'cli') {
22 die('Script must be called from command line.' . chr(10));
23 }
24
25 /**
26 * Core integrity test script:
27 *
28 * Find all CSV files in fixtures and make sure they have the correct column
29 * count across all lines in them and fix them if --fix argument is given.
30 */
31 class checkIntegrityCsvFixtures
32 {
33 /**
34 * @var bool True to fix broken files
35 */
36 private $fix = false;
37
38 /**
39 * @var bool True to drop superfluous comma on all CSV fixture files
40 */
41 private $fixAll = false;
42
43 public function setFix(bool $fix)
44 {
45 $this->fix = $fix;
46 }
47
48 public function setFixAll(bool $fixAll)
49 {
50 $this->fixAll = $fixAll;
51 }
52
53 /**
54 * Executes the CGL check.
55 * The return value is used directly in the ext() call outside this class.
56 *
57 * @return int
58 */
59 public function execute(): int
60 {
61 $filesToProcess = $this->findCsvFixtures();
62 $outputLines = [];
63 $output = new \Symfony\Component\Console\Output\ConsoleOutput();
64
65 $exitStatus = 0;
66 /** @var \SplFileInfo $csvFixture */
67 foreach ($filesToProcess as $csvFixture) {
68 $fullFilePath = $csvFixture->getRealPath();
69 if ($this->fixAll) {
70 $changed = $this->fixCsvFile($fullFilePath);
71 if ($changed) {
72 $outputLines[] = 'Changed file "' . $this->formatOutputString($this->getRelativePath($fullFilePath)) . '"';
73 }
74 continue;
75 }
76 $singleFileScanResult = $this->validateCsvFile($fullFilePath);
77 if ($singleFileScanResult !== '') {
78 if ($this->fix) {
79 $this->fixCsvFile($fullFilePath);
80 $outputLines[] = 'Fixed file "' . $this->formatOutputString($this->getRelativePath($fullFilePath)) . '"';
81 } else {
82 $exitStatus = 1;
83 $outputLines[] = 'File "' . $this->formatOutputString($this->getRelativePath($fullFilePath)) . '"'
84 . ' is not in valid CSV format: ' . $singleFileScanResult;
85 }
86 }
87 }
88 if (!empty($outputLines)) {
89 foreach ($outputLines as $line) {
90 $output->writeln($line);
91 }
92 }
93 return $exitStatus;
94 }
95
96 /**
97 * Finds all CSV fixtures in TYPO3s core
98 *
99 * @return \Symfony\Component\Finder\Finder
100 */
101 private function findCsvFixtures(): \Symfony\Component\Finder\Finder
102 {
103 $finder = new Symfony\Component\Finder\Finder();
104 $csvFixtures = $finder
105 ->files()
106 ->in(__DIR__ . '/../../typo3/sysext/*/Tests/Functional/**')
107 ->name('*.csv');
108 return $csvFixtures;
109 }
110
111 /**
112 * Checks if a CSV uses the same amount of columns across all
113 * lines in that file
114 *
115 * @param string $csvFixture
116 * @return string
117 */
118 private function validateCsvFile(string $csvFixture): string
119 {
120 // Load file content into array split by line
121 $lines = file($csvFixture);
122 $columnCount = 0;
123 foreach ($lines as $index => $line) {
124 // count columns in file
125 $columns = str_getcsv($line);
126 if ($columnCount === 0) {
127 $columnCount = count($columns);
128 } else {
129 if (count($columns) !== $columnCount) {
130 // Skip CSV lines with starting with comments
131 if (count($columns) === 1 && strpos($columns[0], '#') === 0) {
132 continue;
133 }
134 return 'Line ' . ($index + 1) . '; Expected column count: ' . $columnCount . '; Actual: ' . count($columns);
135 }
136 }
137 }
138 return '';
139 }
140
141 /**
142 * Fix a single CSV file.
143 *
144 * @param string $csvFixture
145 * @return bool True if the file has been changed
146 */
147 private function fixCsvFile(string $csvFixture): bool
148 {
149 $changeNeeded = false;
150 // Load file content into array split by line
151 $lines = file($csvFixture);
152 $neededColumns = 0;
153 $csvLines = [];
154 foreach ($lines as $line) {
155 // Find out how many columns are needed in this file
156 $csvLine = str_getcsv($line);
157 $csvLines[] = $csvLine;
158 foreach ($csvLine as $columnNumber => $columnContent) {
159 if (!empty($columnContent) && $columnNumber + 1 > $neededColumns) {
160 $neededColumns = $columnNumber + 1;
161 }
162 }
163 }
164 foreach ($csvLines as $csvLine) {
165 // Set $changeNeeded to true if this file needs an update and line is not a comment
166 if (count($csvLine) !== $neededColumns && substr($csvLine[0], 0, 2) !== '# ') {
167 $changeNeeded = true;
168 break;
169 }
170 }
171 if ($changeNeeded) {
172 // Update file
173 $fileHandle = fopen($csvFixture, 'w');
174 if (!$fileHandle) {
175 throw new \Exception('Opening file "' . $csvFixture . '" for writing failed.');
176 }
177 foreach ($csvLines as $csvLine) {
178 // Extend / reduce to needed size
179 $csvLine = array_slice(array_pad($csvLine, $neededColumns, ''), 0, $neededColumns);
180 $isComment = false;
181 $line = array_reduce($csvLine, function ($carry, $column) use (&$isComment) {
182 if ($carry === null && substr($column, 0, 2) === '# ') {
183 $isComment = true;
184 $carry .= $column;
185 } elseif ($isComment) {
186 // comment lines are not filled up with comma
187 return $carry;
188 } elseif (empty($column) && $column !== '0') {
189 // No leading comma if first column
190 $carry .= $carry === null ? '' : ',';
191 } elseif (MathUtility::canBeInterpretedAsInteger($column)) {
192 // No leading comma if first column and integer payload
193 $carry .= ($carry === null ? '' : ',') . $column;
194 } else {
195 // No leading comma if first column and string payload and quote " to ""
196 $column = str_replace('"', '""', $column);
197 $carry .= ($carry === null ? '' : ',') . '"' . $column . '"';
198 }
199 return $carry;
200 });
201 fwrite($fileHandle, $line . chr(10));
202 }
203 fclose($fileHandle);
204 }
205 return $changeNeeded;
206 }
207
208 private function getRelativePath(string $fullPath): string
209 {
210 $pathSegment = str_replace('Build/Scripts', '', __DIR__);
211 return str_replace($pathSegment, '', $fullPath);
212 }
213
214 /**
215 * Makes the output on CLI a bit more readable
216 *
217 * @param string $filename
218 * @return string
219 */
220 private function formatOutputString(string $filename): string
221 {
222 $pattern = '#typo3[\\\\/]sysext[\\\\/](?<extName>[a-z].+?)[\\\\/]Tests[\\\\/]Functional[\\\\/](?<file>.*)#';
223 preg_match_all($pattern, $filename, $matches, PREG_SET_ORDER, 0);
224 if (isset($matches[0])) {
225 return 'EXT:' . $matches[0]['extName'] . ' > ' . $matches[0]['file'];
226 }
227 return $filename;
228 }
229 }
230
231 $cglFixer = new checkIntegrityCsvFixtures();
232 $args = getopt('', ['fix', 'fixAll']);
233 if (array_key_exists('fix', $args)) {
234 $cglFixer->setFix(true);
235 }
236 if (array_key_exists('fixAll', $args)) {
237 $cglFixer->setFixAll(true);
238 }
239 exit($cglFixer->execute());