2 declare(strict_types
= 1);
4 namespace TYPO3\CMS\Install\UpgradeAnalysis
;
7 * This file is part of the TYPO3 CMS project.
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.
13 * For the full copyright and license information, please read the
14 * LICENSE.txt file that was distributed with this source code.
16 * The TYPO3 project - inspiring people to share!
19 use TYPO3\CMS\Core\Registry
;
20 use TYPO3\CMS\Core\Utility\ExtensionManagementUtility
;
21 use TYPO3\CMS\Core\Utility\GeneralUtility
;
22 use TYPO3\CMS\Core\Utility\PathUtility
;
25 * Provide information about documentation files
27 class DocumentationFile
35 * @var array Unified array of used tags
37 protected $tagsTotal = [];
40 * all files handled in this Class need to reside inside the changelog dir
41 * this is a security measure to protect system files
45 protected $changelogPath = '';
48 * DocumentationFile constructor.
49 * @param Registry|null $registry
51 public function __construct(Registry
$registry = null
, $changelogDir = '')
53 $this->registry
= $registry;
54 if ($this->registry
=== null
) {
55 $this->registry
= new Registry();
57 $this->changelogPath
= $changelogDir !== '' ?
$changelogDir : realpath(ExtensionManagementUtility
::extPath('core') . 'Documentation/Changelog');
58 $this->changelogPath
= str_replace('\\', '/', $this->changelogPath
);
62 * Traverse given directory, select files
65 * @return array file details of affected documentation files
66 * @throws \InvalidArgumentException
68 public function findDocumentationFiles(string $path): array
70 if (strcasecmp($path, $this->changelogPath
) < 0 ||
strpos($path, $this->changelogPath
) === false
) {
71 throw new \
InvalidArgumentException('the given path does not belong to the changelog dir. Aborting', 1485425530);
74 $documentationFiles = [];
75 $versionDirectories = scandir($path);
77 $fileInfo = pathinfo($path);
78 $absolutePath = str_replace('\\', '/', $fileInfo['dirname']) . '/' . $fileInfo['basename'];
79 foreach ($versionDirectories as $version) {
80 $directory = $absolutePath . '/' . $version;
81 $documentationFiles +
= $this->getDocumentationFilesForVersion($directory, $version);
83 $this->tagsTotal
= $this->collectTagTotal($documentationFiles);
85 return $documentationFiles;
89 * Get main information from a .rst file
91 * @param string $file Absolute path to documentation file
94 public function getListEntry(string $file): array
96 if (strcasecmp($file, $this->changelogPath
) < 0 ||
strpos($file, $this->changelogPath
) === false
) {
97 throw new \
InvalidArgumentException('the given file does not belong to the changelog dir. Aborting', 1485425531);
99 $lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES
);
100 $headline = $this->extractHeadline($lines);
101 $entry['headline'] = $headline;
102 $entry['filepath'] = $file;
103 $entry['tags'] = $this->extractTags($lines);
104 $entry['class'] = 'default';
105 foreach ($entry['tags'] as $tag) {
106 if (strpos($tag, 'cat:') !== false
) {
107 $entry['class'] = strtolower(substr($tag, 4));
110 $entry['tagList'] = implode(',', $entry['tags']);
111 $entry['content'] = file_get_contents($file);
112 $entry['parsedContent'] = $this->parseContent($entry['content']);
113 $entry['file_hash'] = md5($entry['content']);
115 return [md5($file) => $entry];
119 * True if file should be considered
121 * @param array $fileInfo
124 protected function isRelevantFile(array $fileInfo): bool
126 $isRelevantFile = $fileInfo['extension'] === 'rst' && $fileInfo['filename'] !== 'Index';
127 // file might be ignored by users choice
128 if ($isRelevantFile && $this->isFileIgnoredByUsersChoice($fileInfo['basename'])) {
129 $isRelevantFile = false
;
132 return $isRelevantFile;
138 * @param array $file file content, each line is an array item
141 protected function extractTags(array $file): array
143 $tags = $this->extractTagsFromFile($file);
144 // Headline starting with the category like Breaking, Important or Feature
145 $tags[] = $this->extractCategoryFromHeadline($file);
152 * Files must contain an index entry, detailing any number of manual tags
153 * each of these tags is extracted and added to the general tag structure for the file
155 * @param array $file file content, each line is an array item
156 * @return array extracted tags
158 protected function extractTagsFromFile(array $file): array
160 foreach ($file as $line) {
161 if (strpos($line, '.. index::') === 0) {
162 $tagString = substr($line, strlen('.. index:: '));
163 return GeneralUtility
::trimExplode(',', $tagString, true
);
171 * Files contain a headline (provided as input parameter,
172 * it starts with the category string.
173 * This will used as a tag
175 * @param array $lines
178 protected function extractCategoryFromHeadline(array $lines): string
180 $headline = $this->extractHeadline($lines);
181 if (strpos($headline, ':') !== false
) {
182 return 'cat:' . substr($headline, 0, strpos($headline, ':'));
189 * Skip include line and markers, use the first line actually containing text
191 * @param array $lines
194 protected function extractHeadline(array $lines): string
197 while (strpos($lines[$index], '..') === 0 ||
strpos($lines[$index], '==') === 0) {
200 return trim($lines[$index]);
204 * True for real directories and a valid version
206 * @param string $versionDirectory
207 * @param string $version
210 protected function isRelevantDirectory(string $versionDirectory, string $version): bool
212 return is_dir($versionDirectory) && $version !== '.' && $version !== '..';
216 * Handle a single directory
218 * @param string $docDirectory
219 * @param string $version
222 protected function getDocumentationFilesForVersion(
223 string $docDirectory,
226 $documentationFiles = [];
227 if ($this->isRelevantDirectory($docDirectory, $version)) {
228 $documentationFiles[$version] = [];
229 $absolutePath = str_replace('\\', '/', PathUtility
::dirname($docDirectory)) . '/' . $version;
230 $rstFiles = scandir($docDirectory);
231 foreach ($rstFiles as $file) {
232 $fileInfo = pathinfo($file);
233 if ($this->isRelevantFile($fileInfo)) {
234 $filePath = $absolutePath . '/' . $fileInfo['basename'];
235 $documentationFiles[$version] +
= $this->getListEntry($filePath);
240 return $documentationFiles;
246 * @param $documentationFiles
249 protected function collectTagTotal($documentationFiles): array
252 foreach ($documentationFiles as $versionArray) {
253 foreach ($versionArray as $fileArray) {
254 $tags = array_merge(array_unique($tags), $fileArray['tags']);
258 return array_unique($tags);
262 * Return full tag list
266 public function getTagsTotal(): array
268 return $this->tagsTotal
;
272 * whether that file has been removed from users view
274 * @param string $filename
277 protected function isFileIgnoredByUsersChoice(string $filename): bool
279 $isFileIgnoredByUsersChoice = false
;
281 $ignoredFiles = $this->registry
->get('upgradeAnalysisIgnoreFilter', 'ignoredDocumentationFiles');
282 if (is_array($ignoredFiles)) {
283 foreach ($ignoredFiles as $filePath) {
284 if ($filePath !== null
&& strlen($filePath) > 0) {
285 if (strpos($filePath, $filename) !== false
) {
286 $isFileIgnoredByUsersChoice = true
;
292 return $isFileIgnoredByUsersChoice;
296 * @param string $rstContent
299 * @throws \InvalidArgumentException
301 protected function parseContent(string $rstContent): string
303 $content = htmlspecialchars($rstContent);
304 $content = preg_replace('/:issue:`([\d]*)`/', '<a href="https://forge.typo3.org/issues/\\1" target="_blank">\\1</a>', $content);
305 $content = preg_replace('/#([\d]*)/', '#<a href="https://forge.typo3.org/issues/\\1" target="_blank">\\1</a>', $content);
306 $content = preg_replace('/(\n([=]*)\n(.*)\n([=]*)\n)/', '', $content, 1);
307 $content = preg_replace('/.. index::(.*)/', '', $content);
308 $content = preg_replace('/.. include::(.*)/', '', $content);
309 return trim($content);