[BUGFIX] Always use forward slashes in 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 $this->changelogPath = strtr($this->changelogPath, '\\', '/');
58 }
59
60 /**
61 * Traverse given directory, select files
62 *
63 * @param string $path
64 * @return array file details of affected documentation files
65 * @throws \InvalidArgumentException
66 */
67 public function findDocumentationFiles(string $path): array
68 {
69 if (strcasecmp($path, $this->changelogPath) < 0 || strpos($path, $this->changelogPath) === false) {
70 throw new \InvalidArgumentException('the given path does not belong to the changelog dir. Aborting', 1485425530);
71 }
72
73 $documentationFiles = [];
74 $versionDirectories = scandir($path);
75
76 $fileInfo = pathinfo($path);
77 $absolutePath = strtr($fileInfo['dirname'], '\\', '/') . '/' . $fileInfo['basename'];
78 foreach ($versionDirectories as $version) {
79 $directory = $absolutePath . '/' . $version;
80 $documentationFiles += $this->getDocumentationFilesForVersion($directory, $version);
81 }
82 $this->tagsTotal = $this->collectTagTotal($documentationFiles);
83
84 return $documentationFiles;
85 }
86
87 /**
88 * Get main information from a .rst file
89 *
90 * @param string $file
91 * @return array
92 */
93 public function getListEntry(string $file): array
94 {
95 if (strcasecmp($file, $this->changelogPath) < 0 || strpos($file, $this->changelogPath) === false) {
96 throw new \InvalidArgumentException('the given file does not belong to the changelog dir. Aborting', 1485425531);
97 }
98 $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
99 $headline = $this->extractHeadline($lines);
100 $entry['headline'] = $headline;
101 $entry['filepath'] = $file;
102 $entry['tags'] = $this->extractTags($lines);
103 $entry['tagList'] = implode(',', $entry['tags']);
104 $entry['content'] = file_get_contents($file);
105 $issueNumber = $this->extractIssueNumber($headline);
106
107 return [$issueNumber => $entry];
108 }
109
110 /**
111 * True if file should be considered
112 *
113 * @param array $fileInfo
114 * @return bool
115 */
116 protected function isRelevantFile(array $fileInfo): bool
117 {
118 $isRelevantFile = $fileInfo['extension'] === 'rst' && $fileInfo['filename'] !== 'Index';
119 // file might be ignored by users choice
120 if ($isRelevantFile && $this->isFileIgnoredByUsersChoice($fileInfo['basename'])) {
121 $isRelevantFile = false;
122 }
123
124 return $isRelevantFile;
125 }
126
127 /**
128 * Add tags from file
129 *
130 * @param array $file file content, each line is an array item
131 * @return array
132 */
133 protected function extractTags(array $file): array
134 {
135 $tags = $this->extractTagsFromFile($file);
136 // Headline starting with the category like Breaking, Important or Feature
137 $tags[] = $this->extractCategoryFromHeadline($file);
138
139 return $tags;
140 }
141
142 /**
143 * Files must contain an index entry, detailing any number of manual tags
144 * each of these tags is extracted and added to the general tag structure for the file
145 *
146 * @param array $file file content, each line is an array item
147 * @return array extracted tags
148 */
149 protected function extractTagsFromFile(array $file): array
150 {
151 foreach ($file as $line) {
152 if (strpos($line, '.. index::') === 0) {
153 $tagString = substr($line, strlen('.. index:: '));
154 return GeneralUtility::trimExplode(',', $tagString, true);
155 }
156 }
157
158 return [];
159 }
160
161 /**
162 * Files contain a headline (provided as input parameter,
163 * it starts with the category string.
164 * This will used as a tag
165 *
166 * @param array $lines
167 * @return string
168 */
169 protected function extractCategoryFromHeadline(array $lines): string
170 {
171 $headline = $this->extractHeadline($lines);
172 if (strpos($headline, ':') !== false) {
173 return 'cat:' . substr($headline, 0, strpos($headline, ':'));
174 }
175
176 return '';
177 }
178
179 /**
180 * Skip include line and markers, use the first line actually containing text
181 *
182 * @param array $lines
183 * @return string
184 */
185 protected function extractHeadline(array $lines): string
186 {
187 $index = 0;
188 while (strpos($lines[$index], '..') === 0 || strpos($lines[$index], '==') === 0) {
189 $index++;
190 }
191 return trim($lines[$index]);
192 }
193
194 /**
195 * Get issue number from headline
196 *
197 * @param string $headline
198 * @return int
199 */
200 protected function extractIssueNumber(string $headline): int
201 {
202 return (int)substr($headline, strpos($headline, '#') + 1, 5);
203 }
204
205 /**
206 * True for real directories and a valid version
207 *
208 * @param string $versionDirectory
209 * @param string $version
210 * @return bool
211 */
212 protected function isRelevantDirectory(string $versionDirectory, string $version): bool
213 {
214 return is_dir($versionDirectory) && $version !== '.' && $version !== '..';
215 }
216
217 /**
218 * Handle a single directory
219 *
220 * @param string $docDirectory
221 * @param string $version
222 * @return array
223 */
224 protected function getDocumentationFilesForVersion(
225 string $docDirectory,
226 string $version
227 ): array {
228 $documentationFiles = [];
229 if ($this->isRelevantDirectory($docDirectory, $version)) {
230 $documentationFiles[$version] = [];
231 $absolutePath = strtr(dirname($docDirectory), '\\', '/') . '/' . $version;
232 $rstFiles = scandir($docDirectory);
233 foreach ($rstFiles as $file) {
234 $fileInfo = pathinfo($file);
235 if ($this->isRelevantFile($fileInfo)) {
236 $filePath = $absolutePath . '/' . $fileInfo['basename'];
237 $documentationFiles[$version] += $this->getListEntry($filePath);
238 }
239 }
240 }
241
242 return $documentationFiles;
243 }
244
245 /**
246 * Merge tag list
247 *
248 * @param $documentationFiles
249 * @return array
250 */
251 protected function collectTagTotal($documentationFiles): array
252 {
253 $tags = [];
254 foreach ($documentationFiles as $versionArray) {
255 foreach ($versionArray as $fileArray) {
256 $tags = array_merge(array_unique($tags), $fileArray['tags']);
257 }
258 }
259
260 return array_unique($tags);
261 }
262
263 /**
264 * Return full tag list
265 *
266 * @return array
267 */
268 public function getTagsTotal(): array
269 {
270 return $this->tagsTotal;
271 }
272
273 /**
274 * whether that file has been removed from users view
275 *
276 * @param string $filename
277 * @return bool
278 */
279 protected function isFileIgnoredByUsersChoice(string $filename): bool
280 {
281 $isFileIgnoredByUsersChoice = false;
282
283 $ignoredFiles = $this->registry->get('upgradeAnalysisIgnoreFilter', 'ignoredDocumentationFiles');
284 if (is_array($ignoredFiles)) {
285 foreach ($ignoredFiles as $filePath) {
286 if ($filePath !== null && strlen($filePath) > 0) {
287 if (strpos($filePath, $filename) !== false) {
288 $isFileIgnoredByUsersChoice = true;
289 break;
290 }
291 }
292 }
293 }
294 return $isFileIgnoredByUsersChoice;
295 }
296 }