TYPO3 v15 dev-main snapshot ()
This commit is contained in:
@@ -0,0 +1,240 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/*
|
||||
* This file is part of the TYPO3 CMS project.
|
||||
*
|
||||
* It is free software; you can redistribute it and/or modify it under
|
||||
* the terms of the GNU General Public License, either version 2
|
||||
* of the License, or any later version.
|
||||
*
|
||||
* For the full copyright and license information, please read the
|
||||
* LICENSE.txt file that was distributed with this source code.
|
||||
*
|
||||
* The TYPO3 project - inspiring people to share!
|
||||
*/
|
||||
|
||||
namespace TYPO3\CMS\Install\UpgradeAnalysis;
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
|
||||
|
||||
/**
|
||||
* Provide information about documentation files.
|
||||
*
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
final class DocumentationFile
|
||||
{
|
||||
/**
|
||||
* all files handled in this Class need to reside inside the changelog dir
|
||||
* this is a security measure to protect system files
|
||||
*/
|
||||
private string $changelogPath;
|
||||
|
||||
public function __construct(string $changelogDir = '')
|
||||
{
|
||||
$this->changelogPath = $changelogDir !== '' ? $changelogDir : (string)realpath(ExtensionManagementUtility::extPath('core') . 'Documentation/Changelog');
|
||||
$this->changelogPath = str_replace('\\', '/', $this->changelogPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse given directory, select directories
|
||||
*
|
||||
* @return string[] Version directories
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function findDocumentationDirectories(string $path): array
|
||||
{
|
||||
if (strcasecmp($path, $this->changelogPath) < 0 || !str_contains($path, $this->changelogPath)) {
|
||||
throw new \InvalidArgumentException('the given path does not belong to the changelog dir. Aborting', 1537158043);
|
||||
}
|
||||
|
||||
$currentVersion = (int)explode('.', VersionNumberUtility::getNumericTypo3Version())[0];
|
||||
$versions = range($currentVersion, $currentVersion - 2);
|
||||
$pattern = '(' . implode('\.*|', $versions) . '\.*)';
|
||||
$finder = new Finder();
|
||||
$finder
|
||||
->depth(0)
|
||||
->sortByName(true)
|
||||
->ignoreUnreadableDirs()
|
||||
->name($pattern)
|
||||
->in($path);
|
||||
|
||||
$directories = [];
|
||||
foreach ($finder->directories() as $directory) {
|
||||
/** @var SplFileInfo $directory */
|
||||
$directories[] = $directory->getBasename();
|
||||
}
|
||||
|
||||
return $directories;
|
||||
}
|
||||
|
||||
/**
|
||||
* Traverse given directory, select files
|
||||
*
|
||||
* @return array file details of affected documentation files
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function findDocumentationFiles(string $path): array
|
||||
{
|
||||
if (strcasecmp($path, $this->changelogPath) < 0 || !str_contains($path, $this->changelogPath)) {
|
||||
throw new \InvalidArgumentException('the given path does not belong to the changelog dir. Aborting', 1485425530);
|
||||
}
|
||||
return $this->getDocumentationFilesForVersion($path);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get main information from a .rst file
|
||||
*
|
||||
* @param string $file Absolute path to documentation file
|
||||
* @throws \InvalidArgumentException
|
||||
*/
|
||||
public function getListEntry(string $file): array
|
||||
{
|
||||
$entry = [];
|
||||
if (strcasecmp($file, $this->changelogPath) < 0 || !str_contains($file, $this->changelogPath)) {
|
||||
throw new \InvalidArgumentException('the given file does not belong to the changelog dir. Aborting', 1485425531);
|
||||
}
|
||||
$lines = file($file, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
$lines = is_array($lines) ? $lines : [];
|
||||
$headline = $this->extractHeadline($lines);
|
||||
$entry['version'] = PathUtility::basename(PathUtility::dirname($file));
|
||||
$entry['headline'] = $headline;
|
||||
$entry['filepath'] = $file;
|
||||
$entry['filename'] = pathinfo($file)['filename'];
|
||||
$entry['tags'] = $this->extractTags($lines);
|
||||
$entry['class'] = 'default';
|
||||
foreach ($entry['tags'] as $key => $tag) {
|
||||
if (str_starts_with($tag, 'cat:')) {
|
||||
$substr = substr($tag, 4);
|
||||
$entry['class'] = strtolower($substr);
|
||||
$entry['tags'][$key] = $substr;
|
||||
}
|
||||
}
|
||||
$entry['tagList'] = implode(',', $entry['tags']);
|
||||
$entry['tagArray'] = $entry['tags'];
|
||||
$entry['content'] = (string)file_get_contents($file);
|
||||
$entry['parsedContent'] = $this->parseContent($entry['content']);
|
||||
$entry['file_hash'] = md5($entry['content']);
|
||||
if ($entry['version'] !== '') {
|
||||
$entry['url']['documentation'] = sprintf(
|
||||
'https://docs.typo3.org/c/typo3/cms-core/main/en-us/Changelog/%s/%s.html',
|
||||
$entry['version'],
|
||||
$entry['filename']
|
||||
);
|
||||
}
|
||||
$issueId = $this->parseIssueId($entry['filename']);
|
||||
if ($issueId) {
|
||||
$entry['url']['issue'] = sprintf('https://forge.typo3.org/issues/%s', $issueId);
|
||||
}
|
||||
|
||||
return [md5($file) => $entry];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tags from file
|
||||
*
|
||||
* @param array $file file content, each line is an array item
|
||||
*/
|
||||
private function extractTags(array $file): array
|
||||
{
|
||||
$tags = $this->extractTagsFromFile($file);
|
||||
// Headline starting with the category like Breaking, Important or Feature
|
||||
$tags[] = $this->extractCategoryFromHeadline($file);
|
||||
natcasesort($tags);
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Files must contain an index entry, detailing any number of manual tags
|
||||
* each of these tags is extracted and added to the general tag structure for the file
|
||||
*
|
||||
* @param array $file file content, each line is an array item
|
||||
* @return array extracted tags
|
||||
*/
|
||||
private function extractTagsFromFile(array $file): array
|
||||
{
|
||||
foreach ($file as $line) {
|
||||
if (str_starts_with($line, '.. index::')) {
|
||||
$tagString = substr($line, strlen('.. index:: '));
|
||||
return GeneralUtility::trimExplode(',', $tagString, true);
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Files contain a headline (provided as input parameter),
|
||||
* it starts with the category string.
|
||||
* This will be used as a tag.
|
||||
*/
|
||||
private function extractCategoryFromHeadline(array $lines): string
|
||||
{
|
||||
$headline = $this->extractHeadline($lines);
|
||||
if (str_contains($headline, ':')) {
|
||||
return 'cat:' . substr($headline, 0, (int)strpos($headline, ':'));
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Skip include line and markers, use the first line actually containing text.
|
||||
*/
|
||||
private function extractHeadline(array $lines): string
|
||||
{
|
||||
$index = 0;
|
||||
while (str_starts_with($lines[$index], '..') || str_starts_with($lines[$index], '==')) {
|
||||
$index++;
|
||||
}
|
||||
return trim($lines[$index]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle a single directory.
|
||||
*/
|
||||
private function getDocumentationFilesForVersion(string $docDirectory): array
|
||||
{
|
||||
$documentationFiles = [[]];
|
||||
$absolutePath = str_replace('\\', '/', $docDirectory);
|
||||
$finder = $this->getDocumentFinder()->in($absolutePath);
|
||||
foreach ($finder->files() as $file) {
|
||||
/** @var SplFileInfo $file */
|
||||
$documentationFiles[] = $this->getListEntry($file->getPathname());
|
||||
}
|
||||
return array_merge(...$documentationFiles);
|
||||
}
|
||||
|
||||
private function parseContent(string $rstContent): string
|
||||
{
|
||||
$content = htmlspecialchars($rstContent, ENT_COMPAT | ENT_SUBSTITUTE);
|
||||
$content = (string)preg_replace('/:issue:`([\d]*)`/', '<a href="https://forge.typo3.org/issues/\\1" target="_blank" rel="noreferrer">\\1</a>', $content);
|
||||
$content = (string)preg_replace('/#([\d]*)/', '#<a href="https://forge.typo3.org/issues/\\1" target="_blank" rel="noreferrer">\\1</a>', $content);
|
||||
$content = (string)preg_replace('/(\n([=]*)\n(.*)\n([=]*)\n)/', '', $content, 1);
|
||||
$content = (string)preg_replace('/.. index::(.*)/', '', $content);
|
||||
$content = (string)preg_replace('/.. include::(.*)/', '', $content);
|
||||
return trim($content);
|
||||
}
|
||||
|
||||
private function parseIssueId(string $filename): ?string
|
||||
{
|
||||
return GeneralUtility::trimExplode('-', $filename)[1] ?? null;
|
||||
}
|
||||
|
||||
private function getDocumentFinder(): Finder
|
||||
{
|
||||
$finder = new Finder();
|
||||
$finder
|
||||
->depth(0)
|
||||
->sortByName()
|
||||
->name('/^(Feature|Breaking|Deprecation|Important)\-\d+.+\.rst$/i');
|
||||
return $finder;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user