[FEATURE] Storable Filters for Upgrade Analysis
[Packages/TYPO3.CMS.git] / typo3 / sysext / install / Classes / UpgradeAnalysis / DocumentationFile.php
1 <?php
2 declare(strict_types=1);
3
4 namespace TYPO3\CMS\Install\UpgradeAnalysis;
5
6 /*
7 * This file is part of the TYPO3 CMS project.
8 *
9 * It is free software; you can redistribute it and/or modify it under
10 * the terms of the GNU General Public License, either version 2
11 * of the License, or any later version.
12 *
13 * For the full copyright and license information, please read the
14 * LICENSE.txt file that was distributed with this source code.
15 *
16 * The TYPO3 project - inspiring people to share!
17 */
18
19 use TYPO3\CMS\Core\Registry;
20 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
21 use TYPO3\CMS\Core\Utility\GeneralUtility;
22
23 /**
24 * Provide information about documentation files
25 */
26 class DocumentationFile
27 {
28 /**
29 * @var Registry
30 */
31 protected $registry;
32
33 /**
34 * @var array Unified array of used tags
35 */
36 protected $tagsTotal = [];
37
38 /**
39 * all files handled in this Class need to reside inside the changelog dir
40 * this is a security measure to protect system files
41 *
42 * @var string
43 */
44 protected $changelogPath = '';
45
46 /**
47 * DocumentationFile constructor.
48 * @param Registry|null $registry
49 */
50 public function __construct(Registry $registry = null, $changelogDir = '')
51 {
52 $this->registry = $registry;
53 if ($this->registry === null) {
54 $this->registry = new Registry();
55 }
56 $this->changelogPath = $changelogDir !== '' ? $changelogDir : realpath(PATH_site . ExtensionManagementUtility::siteRelPath('core') . 'Documentation/Changelog');
57 }
58
59 /**
60 * Traverse given directory, select files
61 *
62 * @param string $path
63 * @return array file details of affected documentation files
64 * @throws \InvalidArgumentException
65 */
66 public function findDocumentationFiles(string $path): array
67 {
68 if (strcasecmp($path, $this->changelogPath) < 0 || strpos($path, $this->changelogPath) === false) {
69 throw new \InvalidArgumentException('the given path does not belong to the changelog dir. Aborting', 1485425530);
70 }
71
72 $documentationFiles = [];
73 $versionDirectories = scandir($path);
74
75 $fileInfo = pathinfo($path);
76 $absolutePath = $fileInfo['dirname'] . DIRECTORY_SEPARATOR . $fileInfo['basename'];
77 foreach ($versionDirectories as $version) {
78 $directory = $absolutePath . DIRECTORY_SEPARATOR . $version;
79 $documentationFiles += $this->getDocumentationFilesForVersion($directory, $version);
80 }
81 $this->tagsTotal = $this->collectTagTotal($documentationFiles);
82
83 return $documentationFiles;
84 }
85
86 /**
87 * Get main information from a .rst file
88 *
89 * @param string $file
90 * @return array
91 */
92 public function getListEntry(string $file): array
93 {
94 if (strcasecmp($file, $this->changelogPath) < 0 || strpos($file, $this->changelogPath) === false) {
95 throw new \InvalidArgumentException('the given file does not belong to the changelog dir. Aborting', 1485425531);
96 }
97 $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
98 $headline = $this->extractHeadline($lines);
99 $entry['headline'] = $headline;
100 $entry['filepath'] = $file;
101 $entry['tags'] = $this->extractTags($lines);
102 $entry['tagList'] = implode(',', $entry['tags']);
103 $entry['content'] = file_get_contents($file);
104 $issueNumber = $this->extractIssueNumber($headline);
105
106 return [$issueNumber => $entry];
107 }
108
109 /**
110 * True if file should be considered
111 *
112 * @param array $fileInfo
113 * @return bool
114 */
115 protected function isRelevantFile(array $fileInfo): bool
116 {
117 $isRelevantFile = $fileInfo['extension'] === 'rst' && $fileInfo['filename'] !== 'Index';
118 // file might be ignored by users choice
119 if ($isRelevantFile && $this->isFileIgnoredByUsersChoice($fileInfo['basename'])) {
120 $isRelevantFile = false;
121 }
122
123 return $isRelevantFile;
124 }
125
126 /**
127 * Add tags from file
128 *
129 * @param array $file file content, each line is an array item
130 * @return array
131 */
132 protected function extractTags(array $file): array
133 {
134 $tags = $this->extractTagsFromFile($file);
135 // Headline starting with the category like Breaking, Important or Feature
136 $tags[] = $this->extractCategoryFromHeadline($file);
137
138 return $tags;
139 }
140
141 /**
142 * Files must contain an index entry, detailing any number of manual tags
143 * each of these tags is extracted and added to the general tag structure for the file
144 *
145 * @param array $file file content, each line is an array item
146 * @return array extracted tags
147 */
148 protected function extractTagsFromFile(array $file): array
149 {
150 foreach ($file as $line) {
151 if (strpos($line, '.. index::') === 0) {
152 $tagString = substr($line, strlen('.. index:: '));
153 return GeneralUtility::trimExplode(',', $tagString, true);
154 }
155 }
156
157 return [];
158 }
159
160 /**
161 * Files contain a headline (provided as input parameter,
162 * it starts with the category string.
163 * This will used as a tag
164 *
165 * @param array $lines
166 * @return string
167 */
168 protected function extractCategoryFromHeadline(array $lines): string
169 {
170 $headline = $this->extractHeadline($lines);
171 if (strpos($headline, ':') !== false) {
172 return 'cat:' . substr($headline, 0, strpos($headline, ':'));
173 }
174
175 return '';
176 }
177
178 /**
179 * Skip include line and markers, use the first line actually containing text
180 *
181 * @param array $lines
182 * @return string
183 */
184 protected function extractHeadline(array $lines): string
185 {
186 $index = 0;
187 while (strpos($lines[$index], '..') === 0 || strpos($lines[$index], '==') === 0) {
188 $index++;
189 }
190 return trim($lines[$index]);
191 }
192
193 /**
194 * Get issue number from headline
195 *
196 * @param string $headline
197 * @return int
198 */
199 protected function extractIssueNumber(string $headline): int
200 {
201 return (int)substr($headline, strpos($headline, '#') + 1, 5);
202 }
203
204 /**
205 * True for real directories and a valid version
206 *
207 * @param string $versionDirectory
208 * @param string $version
209 * @return bool
210 */
211 protected function isRelevantDirectory(string $versionDirectory, string $version): bool
212 {
213 return is_dir($versionDirectory) && $version !== '.' && $version !== '..';
214 }
215
216 /**
217 * Handle a single directory
218 *
219 * @param string $docDirectory
220 * @param string $version
221 * @return array
222 */
223 protected function getDocumentationFilesForVersion(
224 string $docDirectory,
225 string $version
226 ): array {
227 $documentationFiles = [];
228 if ($this->isRelevantDirectory($docDirectory, $version)) {
229 $documentationFiles[$version] = [];
230 $absolutePath = dirname($docDirectory) . DIRECTORY_SEPARATOR . $version;
231 $rstFiles = scandir($docDirectory);
232 foreach ($rstFiles as $file) {
233 $fileInfo = pathinfo($file);
234 if ($this->isRelevantFile($fileInfo)) {
235 $filePath = $absolutePath . DIRECTORY_SEPARATOR . $fileInfo['basename'];
236 $documentationFiles[$version] += $this->getListEntry($filePath);
237 }
238 }
239 }
240
241 return $documentationFiles;
242 }
243
244 /**
245 * Merge tag list
246 *
247 * @param $documentationFiles
248 * @return array
249 */
250 protected function collectTagTotal($documentationFiles): array
251 {
252 $tags = [];
253 foreach ($documentationFiles as $versionArray) {
254 foreach ($versionArray as $fileArray) {
255 $tags = array_merge(array_unique($tags), $fileArray['tags']);
256 }
257 }
258
259 return array_unique($tags);
260 }
261
262 /**
263 * Return full tag list
264 *
265 * @return array
266 */
267 public function getTagsTotal(): array
268 {
269 return $this->tagsTotal;
270 }
271
272 /**
273 * whether that file has been removed from users view
274 *
275 * @param string $filename
276 * @return bool
277 */
278 protected function isFileIgnoredByUsersChoice(string $filename): bool
279 {
280 $isFileIgnoredByUsersChoice = false;
281
282 $ignoredFiles = $this->registry->get('upgradeAnalysisIgnoreFilter', 'ignoredDocumentationFiles');
283 if (is_array($ignoredFiles)) {
284 foreach ($ignoredFiles as $filePath) {
285 if ($filePath !== null && strlen($filePath) > 0) {
286 if (strpos($filePath, $filename) !== false) {
287 $isFileIgnoredByUsersChoice = true;
288 break;
289 }
290 }
291 }
292 }
293 return $isFileIgnoredByUsersChoice;
294 }
295 }