Commit 4d4c37b3 authored by Andreas Fernandez's avatar Andreas Fernandez
Browse files

[BUGFIX] Render correct version information in Core Updater and reports module

Currently, the TYPO3 backend shows incomplete version information
regarding updates in the Core Updater and the reports. Both take
community-supported releases into account only and ignore the fact that
certain versions are covered by the ELTS program and thus render messages
about unsupported or invalid versions, which are false statements.

We now use the full information from get.typo3.org, and added lengthy
tests to avoid any further issues. The internally used
CoreVersionService is now able to handle ELTS releases as well and give
proper information to admins.

Resolves: #94745
Releases: master, 10.4, 9.5
Change-Id: I6485d36ded943acba723d55e23275554484e4f82
Reviewed-on: https://review.typo3.org/c/Packages/TYPO3.CMS/+/70311


Tested-by: core-ci's avatarcore-ci <typo3@b13.com>
Tested-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
Reviewed-by: Andreas Fernandez's avatarAndreas Fernandez <a.fernandez@scripting-base.de>
parent a02928f5
......@@ -37,6 +37,7 @@ use TYPO3\CMS\Core\Registry;
use TYPO3\CMS\Core\Service\OpcodeCacheService;
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\CoreVersion\CoreRelease;
use TYPO3\CMS\Install\ExtensionScanner\Php\CodeStatistics;
use TYPO3\CMS\Install\ExtensionScanner\Php\GeneratorClassesResolver;
use TYPO3\CMS\Install\ExtensionScanner\Php\Matcher\ArrayDimensionMatcher;
......@@ -276,43 +277,116 @@ class UpgradeController extends AbstractController
*/
public function coreUpdateIsUpdateAvailableAction(): ResponseInterface
{
$action = null;
$this->coreUpdateInitialize();
$messageQueue = new FlashMessageQueue('install');
$messages = [];
if ($this->coreVersionService->isInstalledVersionAReleasedVersion()) {
$isDevelopmentUpdateAvailable = $this->coreVersionService->isYoungerPatchDevelopmentReleaseAvailable();
$isUpdateAvailable = $this->coreVersionService->isYoungerPatchReleaseAvailable();
$isUpdateSecurityRelevant = $this->coreVersionService->isUpdateSecurityRelevant();
if (!$isUpdateAvailable && !$isDevelopmentUpdateAvailable) {
$messageQueue->enqueue(new FlashMessage(
'',
'No regular update available',
FlashMessage::NOTICE
));
} elseif ($isUpdateAvailable) {
$newVersion = $this->coreVersionService->getYoungestPatchRelease();
if ($isUpdateSecurityRelevant) {
$messageQueue->enqueue(new FlashMessage(
'',
'Update to security relevant released version ' . $newVersion . ' is available!',
FlashMessage::WARNING
));
$action = ['title' => 'Update now', 'action' => 'updateRegular'];
$versionMaintenanceWindow = $this->coreVersionService->getMaintenanceWindow();
$renderVersionInformation = false;
if (!$versionMaintenanceWindow->isSupportedByCommunity() && !$versionMaintenanceWindow->isSupportedByElts()) {
$messages[] = [
'title' => 'Outdated version',
'message' => 'The currently installed TYPO3 version ' . $this->coreVersionService->getInstalledVersion() . ' does not receive any further updates, please consider upgrading to a supported version!',
'severity' => FlashMessage::ERROR,
];
$renderVersionInformation = true;
} else {
$currentVersion = $this->coreVersionService->getInstalledVersion();
$isCurrentVersionElts = $this->coreVersionService->isCurrentInstalledVersionElts();
$latestRelease = $this->coreVersionService->getYoungestPatchRelease();
$availableReleases = [];
if ($this->coreVersionService->isPatchReleaseSuitableForUpdate($latestRelease)) {
$availableReleases[] = $latestRelease;
}
if (!$versionMaintenanceWindow->isSupportedByCommunity()) {
if ($latestRelease->isElts()) {
// Check if there's a public release left that's not installed yet
$latestCommunityDrivenRelease = $this->coreVersionService->getYoungestCommunityPatchRelease();
if ($this->coreVersionService->isPatchReleaseSuitableForUpdate($latestCommunityDrivenRelease)) {
$availableReleases[] = $latestCommunityDrivenRelease;
$action = ['title' => 'Update now to version ' . $latestCommunityDrivenRelease->getVersion(), 'action' => 'updateRegular'];
}
} elseif (!$isCurrentVersionElts) {
// Inform user about ELTS being available soon if:
// - regular support ran out
// - the current installed version is no ELTS
// - no ELTS update was released, yet
$messages[] = [
'title' => 'ELTS will be available soon',
'message' => sprintf('The currently installed TYPO3 version %s doesn\'t receive any community-driven updates anymore, consider subscribing to Extended Long Term Support (ELTS) releases. Please read the information below.', $currentVersion),
'severity' => FlashMessage::WARNING,
];
$renderVersionInformation = true;
}
}
if ($availableReleases === []) {
$messages[] = [
'title' => 'Up to date',
'message' => 'There are no TYPO3 updates available.',
'severity' => FlashMessage::NOTICE,
];
} else {
$messageQueue->enqueue(new FlashMessage(
'',
'Update to regular released version ' . $newVersion . ' is available!',
FlashMessage::INFO
));
$action = ['title' => 'Update now', 'action' => 'updateRegular'];
foreach ($availableReleases as $availableRelease) {
$isUpdateSecurityRelevant = $this->coreVersionService->isUpdateSecurityRelevant($availableRelease);
$versionString = $availableRelease->getVersion();
if ($availableRelease->isElts()) {
$versionString .= ' ELTS';
}
if ($isUpdateSecurityRelevant) {
$title = ($availableRelease->isElts() ? 'ELTS ' : '') . 'Security update available!';
$message = sprintf('The currently installed version is %s, update to security relevant released version %s is available.', $currentVersion, $versionString);
$severity = FlashMessage::ERROR;
} else {
$title = ($availableRelease->isElts() ? 'ELTS ' : '') . 'Update available!';
$message = sprintf('Currently installed version is %s, update to regular released version %s is available.', $currentVersion, $versionString);
$severity = FlashMessage::WARNING;
}
if ($availableRelease->isElts()) {
if ($isCurrentVersionElts) {
$message .= ' Please visit my.typo3.org to download the release in your ELTS area.';
} else {
$message .= ' ' . sprintf('The currently installed TYPO3 version %s doesn\'t receive any community-driven updates anymore, consider subscribing to Extended Long Term Support (ELTS) releases. Please read the information below.', $currentVersion);
}
$renderVersionInformation = true;
}
$messages[] = [
'title' => $title,
'message' => $message,
'severity' => $severity,
];
}
}
} elseif ($isDevelopmentUpdateAvailable) {
$newVersion = $this->coreVersionService->getYoungestPatchDevelopmentRelease();
$messageQueue->enqueue(new FlashMessage(
'',
'Update to development release ' . $newVersion . ' is available!',
FlashMessage::INFO
));
$action = ['title' => 'Update now', 'action' => 'updateDevelopment'];
}
if ($renderVersionInformation) {
$supportedMajorReleases = $this->coreVersionService->getSupportedMajorReleases();
$supportMessages = [];
if (!empty($supportedMajorReleases['community'])) {
$supportMessages[] = sprintf('Currently community-supported TYPO3 versions: %s (more information at https://get.typo3.org).', implode(', ', $supportedMajorReleases['community']));
}
if (!empty($supportedMajorReleases['elts'])) {
$supportMessages[] = sprintf('Currently supported TYPO3 ELTS versions: %s (more information at https://typo3.com/elts).', implode(', ', $supportedMajorReleases['elts']));
}
$messages[] = [
'title' => 'TYPO3 Version information',
'message' => implode(' ', $supportMessages),
'severity' => FlashMessage::INFO,
];
}
foreach ($messages as $message) {
$messageQueue->enqueue(new FlashMessage($message['message'], $message['title'], $message['severity']));
}
} else {
$messageQueue->enqueue(new FlashMessage(
......@@ -1117,9 +1191,9 @@ class UpgradeController extends AbstractController
*
* @param ServerRequestInterface $request
* @throws \RuntimeException
* @return string Version to handle, eg. 6.2.2
* @return CoreRelease
*/
protected function coreUpdateGetVersionToHandle(ServerRequestInterface $request): string
protected function coreUpdateGetVersionToHandle(ServerRequestInterface $request): CoreRelease
{
$type = $request->getQueryParams()['install']['type'];
if (!isset($type) || empty($type)) {
......@@ -1128,12 +1202,7 @@ class UpgradeController extends AbstractController
1380975303
);
}
if ($type === 'development') {
$versionToHandle = $this->coreVersionService->getYoungestPatchDevelopmentRelease();
} else {
$versionToHandle = $this->coreVersionService->getYoungestPatchRelease();
}
return $versionToHandle;
return $this->coreVersionService->getYoungestCommunityPatchRelease();
}
/**
......
<?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\CoreVersion;
class CoreRelease
{
protected const RELEASE_TYPE_REGULAR = 'regular';
protected const RELEASE_TYPE_SECURITY = 'security';
protected $version;
protected $date;
protected $type;
protected $checksum;
protected $isElts;
public function __construct(string $version, \DateTimeInterface $date, string $type, string $checksum, bool $isElts = false)
{
$this->version = $version;
$this->date = $date;
$this->type = $type;
$this->checksum = $checksum;
$this->isElts = $isElts;
}
public static function fromApiResponse(array $response): self
{
return new self($response['version'], new \DateTimeImmutable($response['date']), $response['type'], $response['tar_package']['sha1sum'], $response['elts'] ?? false);
}
public function getVersion(): string
{
return $this->version;
}
public function getDate(): \DateTimeInterface
{
return $this->date;
}
public function isSecurityUpdate(): bool
{
return $this->type === self::RELEASE_TYPE_SECURITY;
}
public function getChecksum(): string
{
return $this->checksum;
}
public function isElts(): bool
{
return $this->isElts;
}
}
<?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\CoreVersion;
class MaintenanceWindow
{
protected $communitySupport;
protected $eltsSupport;
public function __construct(?\DateTimeInterface $communitySupport, ?\DateTimeInterface $eltsSupport)
{
$this->communitySupport = $communitySupport;
$this->eltsSupport = $eltsSupport;
}
public static function fromApiResponse(array $response): self
{
$maintainedUntil = isset($response['maintained_until']) ? new \DateTimeImmutable($response['maintained_until']) : null;
$eltsUntil = isset($response['elts_until']) ? new \DateTimeImmutable($response['elts_until']) : null;
return new self($maintainedUntil, $eltsUntil);
}
public function isSupportedByCommunity(): bool
{
return $this->isSupported($this->communitySupport);
}
public function isSupportedByElts(): bool
{
return $this->isSupported($this->eltsSupport);
}
protected function isSupported(?\DateTimeInterface $supportedUntil): bool
{
return $supportedUntil !== null
&& (
$supportedUntil >=
new \DateTimeImmutable('now', new \DateTimeZone('UTC'))
);
}
}
<?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\CoreVersion;
class MajorRelease
{
protected $version;
protected $lts;
protected $title;
protected $maintenanceWindow;
public function __construct(string $version, ?string $lts, string $title, MaintenanceWindow $maintenanceWindow)
{
$this->version = $version;
$this->lts = $lts;
$this->title = $title;
$this->maintenanceWindow = $maintenanceWindow;
}
public static function fromApiResponse(array $response): self
{
$maintenanceWindow = MaintenanceWindow::fromApiResponse($response);
$ltsVersion = isset($response['lts']) ? (string)$response['lts'] : null;
return new self((string)$response['version'], $ltsVersion, $response['title'], $maintenanceWindow);
}
public function getVersion(): string
{
return $this->version;
}
public function getLts(): ?string
{
return $this->lts;
}
public function getTitle(): string
{
return $this->title;
}
public function getMaintenanceWindow(): MaintenanceWindow
{
return $this->maintenanceWindow;
}
}
......@@ -17,7 +17,7 @@ namespace TYPO3\CMS\Install\Report;
use TYPO3\CMS\Core\Core\Environment;
use TYPO3\CMS\Core\Localization\LanguageService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Install\Service\Exception;
use TYPO3\CMS\Install\Service\Exception\RemoteFetchException;
use TYPO3\CMS\Install\Service\UpgradeWizardsService;
use TYPO3\CMS\Reports\Status;
......@@ -27,6 +27,16 @@ use TYPO3\CMS\Reports\Status;
*/
class InstallStatusReport implements \TYPO3\CMS\Reports\StatusProviderInterface
{
protected const WRAP_FLAT = 1;
protected const WRAP_NESTED = 2;
protected $useMarkup;
public function __construct(bool $useMarkup = true)
{
$this->useMarkup = $useMarkup;
}
/**
* Compiles a collection of system status checks as a status report.
*
......@@ -205,9 +215,8 @@ class InstallStatusReport implements \TYPO3\CMS\Reports\StatusProviderInterface
}
try {
$isUpdateAvailable = $coreVersionService->isYoungerPatchReleaseAvailable();
$isMaintainedVersion = $coreVersionService->isVersionActivelyMaintained();
} catch (Exception\RemoteFetchException $remoteFetchException) {
$versionMaintenanceWindow = $coreVersionService->getMaintenanceWindow();
} catch (RemoteFetchException $remoteFetchException) {
return GeneralUtility::makeInstance(
Status::class,
'TYPO3',
......@@ -219,29 +228,98 @@ class InstallStatusReport implements \TYPO3\CMS\Reports\StatusProviderInterface
);
}
if (!$isUpdateAvailable && $isMaintainedVersion) {
// Everything is fine, working with the latest version
$message = '';
$status = Status::OK;
} elseif ($isUpdateAvailable) {
if (!$versionMaintenanceWindow->isSupportedByCommunity() && !$versionMaintenanceWindow->isSupportedByElts()) {
// Version is not maintained
$message = $languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_versionOutdated');
$status = Status::ERROR;
} else {
// There is an update available
$newVersion = $coreVersionService->getYoungestPatchRelease();
if ($coreVersionService->isUpdateSecurityRelevant()) {
$message = sprintf($languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_newVersionSecurityRelevant'), $newVersion);
$status = Status::ERROR;
$availableReleases = [];
$latestRelease = $coreVersionService->getYoungestPatchRelease();
$isCurrentVersionElts = $coreVersionService->isCurrentInstalledVersionElts();
if ($coreVersionService->isPatchReleaseSuitableForUpdate($latestRelease)) {
$availableReleases[] = $latestRelease;
}
if (!$versionMaintenanceWindow->isSupportedByCommunity()) {
if ($latestRelease->isElts()) {
$latestCommunityDrivenRelease = $coreVersionService->getYoungestCommunityPatchRelease();
if ($coreVersionService->isPatchReleaseSuitableForUpdate($latestCommunityDrivenRelease)) {
$availableReleases[] = $latestCommunityDrivenRelease;
}
}
}
if ($availableReleases === []) {
// Everything is fine, working with the latest version
$message = '';
$status = Status::OK;
} else {
$message = sprintf($languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_newVersion'), $newVersion);
$messages = [];
$status = Status::WARNING;
foreach ($availableReleases as $availableRelease) {
$versionString = $availableRelease->getVersion();
if ($availableRelease->isElts()) {
$versionString .= ' ELTS';
}
if ($coreVersionService->isUpdateSecurityRelevant($availableRelease)) {
$status = Status::ERROR;
$updateMessage = sprintf($languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_newVersionSecurityRelevant'), $versionString);
} else {
$updateMessage = sprintf($languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_newVersion'), $versionString);
}
if ($availableRelease->isElts()) {
if ($isCurrentVersionElts) {
$updateMessage .= ' ' . sprintf(
$languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_elts_download'),
'<a href="https://my.typo3.org" target="_blank" rel="noopener">my.typo3.org</a>'
);
} else {
$updateMessage .= ' ' . sprintf(
$languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_elts_subscribe'),
$coreVersionService->getInstalledVersion(),
'<a href="https://typo3.com/elts" target="_blank" rel="noopener">https://typo3.com/elts</a>'
);
}
}
$messages[] = $updateMessage;
}
$message = $this->wrapList($messages, count($messages) > 1 ? self::WRAP_NESTED : self::WRAP_FLAT);
}
} else {
// Version is not maintained
$message = $languageService->sL('LLL:EXT:install/Resources/Private/Language/Report/locallang.xlf:status_versionOutdated');
$status = Status::ERROR;
}
return GeneralUtility::makeInstance(Status::class, 'TYPO3', TYPO3_version, $message, $status);
}
protected function wrapList(array $items, int $style): string
{
if (!$this->useMarkup) {
return implode(', ', $items);
}
if ($style === self::WRAP_NESTED) {
return sprintf(
'<ul>%s</ul>',
implode('', $this->wrapItems($items, '<li>', '</li>'))
);
}
return sprintf(
'<p>%s</p>',
implode('', $this->wrapItems($items, '<br>', ''))
);
}
protected function wrapItems(array $items, string $before, string $after): array
{
return array_map(
static function (string $item) use ($before, $after): string {
return $before . $item . $after;
},
array_filter($items)
);
}
/**
* @return LanguageService
*/
......
......@@ -21,6 +21,7 @@ use TYPO3\CMS\Core\Service\OpcodeCacheService;
use TYPO3\CMS\Core\Utility\GeneralUtility;
use TYPO3\CMS\Core\Utility\PathUtility;
use TYPO3\CMS\Core\Utility\StringUtility;
use TYPO3\CMS\Install\CoreVersion\CoreRelease;
use TYPO3\CMS\Install\FolderStructure\DefaultFactory;
use TYPO3\CMS\Install\Service\Exception\RemoteFetchException;
......@@ -155,10 +156,10 @@ class CoreUpdateService
/**
* Check if an update is possible at all
*
* @param string $version The target version number
* @param CoreRelease $coreRelease The target core release
* @return bool TRUE on success
*/
public function checkPreConditions($version)
public function checkPreConditions(CoreRelease $coreRelease)
{
$success = true;
......@@ -216,7 +217,7 @@ class CoreUpdateService
unlink($file);
}
if (!$this->checkCoreFilesAvailable($version)) {
if (!$this->checkCoreFilesAvailable($coreRelease->getVersion())) {
// Explicit write check to upper directory of current core location
$coreLocation = @realpath($this->symlinkToCoreFiles . '/../');
$file = $coreLocation . '/' . StringUtility::getUniqueId('install-core-update-test-');
......@@ -251,11 +252,12 @@ class CoreUpdateService
/**
* Download the specified version
*
* @param string $version A version to download
* @param CoreRelease $coreRelease A core release to download
* @return bool TRUE on success
*/
public function downloadVersion($version)
public function downloadVersion(CoreRelease $coreRelease)
{
$version = $coreRelease->getVersion();
$success = true;
if ($this->checkCoreFilesAvailable($version)) {
$this->messages->enqueue(new FlashMessage(
......@@ -307,11 +309,12 @@ class CoreUpdateService
/**
* Verify checksum of downloaded version
*
* @param string $version A downloaded version to check
* @param CoreRelease $coreRelease A downloaded core release to check
* @return bool TRUE on success
*/
public function verifyFileChecksum($version)
public function verifyFileChecksum(CoreRelease $coreRelease)
{
$version = $coreRelease->getVersion();
$success = true;
if ($this->checkCoreFilesAvailable($version)) {
$this->messages->enqueue(new FlashMessage(
......@@ -321,7 +324,7 @@ class CoreUpdateService
));
} else {
$fileLocation = $this->getDownloadTarGzTargetPath($version);
$expectedChecksum = $this->coreVersionService->getTarGzSha1OfVersion($version);
$expectedChecksum = $coreRelease->getChecksum();