first commit
This commit is contained in:
commit
1218dd1517
12716 files changed
+989233
No files matched your search
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Install\Service;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Core\Bootstrap;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Basic service to clear caches within the install tool.
|
||||
* @internal This is NOT an API class, it is for internal use in the install tool only.
|
||||
*/
|
||||
class ClearCacheService
|
||||
{
|
||||
/**
|
||||
* This clear cache implementation follows a pretty brutal approach.
|
||||
* Goal is to reliably get rid of cache entries, even if some broken
|
||||
* extension is loaded that would kill the backend 'clear cache' action.
|
||||
*
|
||||
* Therefor this method "knows" implementation details of the cache
|
||||
* framework and uses them to clear all file based cache (typo3temp/Cache)
|
||||
* and database caches (tables prefixed with cf_) manually.
|
||||
*
|
||||
* After that ext_tables and ext_localconf of extensions are loaded, those
|
||||
* may register additional caches in the caching framework with different
|
||||
* backend, and will then clear them with the usual flush() method.
|
||||
*/
|
||||
public function clearAll()
|
||||
{
|
||||
// Delete typo3temp/Cache
|
||||
GeneralUtility::flushDirectory(Environment::getVarPath() . '/cache', true, true);
|
||||
|
||||
// Get all table names from Default connection starting with 'cf_' and truncate them
|
||||
$connectionPool = GeneralUtility::makeInstance(ConnectionPool::class);
|
||||
$connection = $connectionPool->getConnectionByName('Default');
|
||||
$tableNames = $connection->getSchemaManager()->listTableNames();
|
||||
foreach ($tableNames as $tableName) {
|
||||
if (strpos($tableName, 'cf_') === 0 || $tableName === 'cache_treelist') {
|
||||
$connection->truncate($tableName);
|
||||
}
|
||||
}
|
||||
|
||||
// check tables on other connections
|
||||
$remappedTables = isset($GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
|
||||
? array_keys((array)$GLOBALS['TYPO3_CONF_VARS']['DB']['TableMapping'])
|
||||
: [];
|
||||
foreach ($remappedTables as $tableName) {
|
||||
if (strpos((string)$tableName, 'cf_') === 0 || $tableName === 'cache_treelist') {
|
||||
$connectionPool->getConnectionForTable($tableName)->truncate($tableName);
|
||||
}
|
||||
}
|
||||
|
||||
// From this point on, the code may fatal, if some broken extension is loaded.
|
||||
|
||||
// Use bootstrap to load all ext_localconf and ext_tables
|
||||
Bootstrap::loadTypo3LoadedExtAndExtLocalconf(false);
|
||||
Bootstrap::unsetReservedGlobalVariables();
|
||||
Bootstrap::loadBaseTca(false);
|
||||
Bootstrap::loadExtTables(false);
|
||||
|
||||
// The cache manager is already instantiated in the install tool
|
||||
// with some hacked settings to disable caching of extbase and fluid.
|
||||
// We want a "fresh" object here to operate on a different cache setup.
|
||||
// cacheManager implements SingletonInterface, so the only way to get a "fresh"
|
||||
// instance is by circumventing makeInstance and/or the objectManager and
|
||||
// using new directly!
|
||||
$cacheManager = new \TYPO3\CMS\Core\Cache\CacheManager();
|
||||
$cacheManager->setCacheConfigurations($GLOBALS['TYPO3_CONF_VARS']['SYS']['caching']['cacheConfigurations']);
|
||||
$cacheManager->flushCaches();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Install\Service;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Service handling clearing and statistics of semi-persistent
|
||||
* core tables.
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
class ClearTableService
|
||||
{
|
||||
/**
|
||||
* @var array List of table and their description
|
||||
*/
|
||||
protected $tableList = [
|
||||
[
|
||||
'name' => 'be_sessions',
|
||||
'description' => 'Backend user sessions'
|
||||
],
|
||||
[
|
||||
'name' => 'fe_sessions',
|
||||
'description' => 'Frontend user sessions',
|
||||
],
|
||||
[
|
||||
'name' => 'sys_history',
|
||||
'description' => 'Tracking of database record changes through TYPO3 backend forms',
|
||||
],
|
||||
[
|
||||
'name' => 'sys_lockedrecords',
|
||||
'description' => 'Record locking of backend user editing',
|
||||
],
|
||||
[
|
||||
'name' => 'sys_log',
|
||||
'description' => 'General log table',
|
||||
],
|
||||
[
|
||||
'name' => 'sys_preview',
|
||||
'description' => 'Workspace preview links',
|
||||
],
|
||||
[
|
||||
'name' => 'tx_extensionmanager_domain_model_extension',
|
||||
'description' => 'List of TER extensions',
|
||||
],
|
||||
[
|
||||
'name' => 'tx_rsaauth_keys',
|
||||
'description' => 'Login process key storage'
|
||||
],
|
||||
];
|
||||
|
||||
/**
|
||||
* Get an array of all affected tables, a short description and their row counts
|
||||
*
|
||||
* @return array Details per table
|
||||
*/
|
||||
public function getTableStatistics(): array
|
||||
{
|
||||
$tableStatistics = [];
|
||||
foreach ($this->tableList as $table) {
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($table['name']);
|
||||
if ($connection->getSchemaManager()->tablesExist([$table['name']])) {
|
||||
$table['rowCount'] = $connection->count(
|
||||
'*',
|
||||
$table['name'],
|
||||
[]
|
||||
);
|
||||
$tableStatistics[] = $table;
|
||||
}
|
||||
}
|
||||
return $tableStatistics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate a table from $this->tableList
|
||||
*
|
||||
* @param string $tableName
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function clearSelectedTable(string $tableName)
|
||||
{
|
||||
$tableFound = false;
|
||||
foreach ($this->tableList as $table) {
|
||||
if ($table['name'] === $tableName) {
|
||||
$tableFound = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$tableFound) {
|
||||
throw new \RuntimeException(
|
||||
'Selected table ' . $tableName . ' can not be cleared',
|
||||
1501942151
|
||||
);
|
||||
}
|
||||
GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable($tableName)->truncate($tableName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,562 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Install\Service;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Core update service.
|
||||
* This service handles core updates, all the nasty details are encapsulated
|
||||
* here. The single public methods 'depend' on each other, for example a new
|
||||
* core has to be downloaded before it can be unpacked.
|
||||
*
|
||||
* Each method returns only TRUE of FALSE indicating if it was successful or
|
||||
* not. Detailed information can be fetched with getMessages() and will return
|
||||
* a list of status messages of the previous operation.
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
class CoreUpdateService
|
||||
{
|
||||
/**
|
||||
* @var \TYPO3\CMS\Install\Service\CoreVersionService
|
||||
*/
|
||||
protected $coreVersionService;
|
||||
|
||||
/**
|
||||
* @var FlashMessageQueue
|
||||
*/
|
||||
protected $messages;
|
||||
|
||||
/**
|
||||
* Absolute path to download location
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $downloadTargetPath;
|
||||
|
||||
/**
|
||||
* Absolute path to the symlink pointing to the currently used TYPO3 core files
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $symlinkToCoreFiles;
|
||||
|
||||
/**
|
||||
* Base URI for TYPO3 downloads
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $downloadBaseUri;
|
||||
|
||||
/**
|
||||
* @param CoreVersionService $coreVersionService
|
||||
*/
|
||||
public function __construct(CoreVersionService $coreVersionService = null)
|
||||
{
|
||||
$this->coreVersionService = $coreVersionService ?: GeneralUtility::makeInstance(CoreVersionService::class);
|
||||
$this->setDownloadTargetPath(Environment::getVarPath() . '/transient/');
|
||||
$this->symlinkToCoreFiles = $this->discoverCurrentCoreSymlink();
|
||||
$this->downloadBaseUri = 'https://get.typo3.org';
|
||||
$this->messages = new FlashMessageQueue('install');
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this installation wants to enable the core updater
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function isCoreUpdateEnabled()
|
||||
{
|
||||
$coreUpdateDisabled = getenv('TYPO3_DISABLE_CORE_UPDATER') ?: (getenv('REDIRECT_TYPO3_DISABLE_CORE_UPDATER') ?: false);
|
||||
return !Environment::isComposerMode() && !$coreUpdateDisabled;
|
||||
}
|
||||
|
||||
/**
|
||||
* In future implementations we might implement some smarter logic here
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected function discoverCurrentCoreSymlink()
|
||||
{
|
||||
return Environment::getPublicPath() . '/typo3_src';
|
||||
}
|
||||
|
||||
/**
|
||||
* Create download location in case the folder does not exist
|
||||
* @todo move this to folder structure
|
||||
*
|
||||
* @param string $downloadTargetPath
|
||||
*/
|
||||
protected function setDownloadTargetPath($downloadTargetPath)
|
||||
{
|
||||
if (!is_dir($downloadTargetPath)) {
|
||||
GeneralUtility::mkdir_deep($downloadTargetPath);
|
||||
}
|
||||
$this->downloadTargetPath = $downloadTargetPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get messages of previous method call
|
||||
*
|
||||
* @return FlashMessageQueue
|
||||
*/
|
||||
public function getMessages(): FlashMessageQueue
|
||||
{
|
||||
return $this->messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper method for CoreVersionService
|
||||
*
|
||||
* @deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0 - use REST api directly (see https://get.typo3.org/v1/api/doc)
|
||||
* @return bool TRUE on success
|
||||
*/
|
||||
public function updateVersionMatrix()
|
||||
{
|
||||
trigger_error(
|
||||
'The method updateVersionMatrix() is deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0, use the REST api directly (see https://get.typo3.org/v1/api/doc).',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
$success = true;
|
||||
try {
|
||||
$this->coreVersionService->getYoungestPatchRelease();
|
||||
} catch (RemoteFetchException $e) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'Current version specification could not be fetched from https://get.typo3.org.'
|
||||
. ' This is probably a network issue, please fix it.',
|
||||
'Version information could not be fetched from get.typo3.org',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an update is possible at all
|
||||
*
|
||||
* @param CoreRelease $coreRelease The target core release
|
||||
* @return bool TRUE on success
|
||||
*/
|
||||
public function checkPreConditions(CoreRelease $coreRelease)
|
||||
{
|
||||
$success = true;
|
||||
|
||||
// Folder structure test: Update can be done only if folder structure returns no errors
|
||||
$folderStructureFacade = GeneralUtility::makeInstance(DefaultFactory::class)->getStructure();
|
||||
$folderStructureMessageQueue = $folderStructureFacade->getStatus();
|
||||
$folderStructureErrors = $folderStructureMessageQueue->getAllMessages(FlashMessage::ERROR);
|
||||
$folderStructureWarnings = $folderStructureMessageQueue->getAllMessages(FlashMessage::WARNING);
|
||||
if (!empty($folderStructureErrors) || !empty($folderStructureWarnings) || !is_link(Environment::getPublicPath() . '/typo3_src')) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'To perform an update, the folder structure of this TYPO3 CMS instance must'
|
||||
. ' stick to the conventions, or the update process could lead to unexpected'
|
||||
. ' results and may be hazardous to your system',
|
||||
'Automatic TYPO3 CMS core update not possible: Folder structure has errors or warnings',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
}
|
||||
|
||||
// No core update on windows
|
||||
if (Environment::isWindows()) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Automatic TYPO3 CMS core update not possible: Update not supported on Windows OS',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
}
|
||||
|
||||
if ($success) {
|
||||
// Explicit write check to document root
|
||||
$file = Environment::getPublicPath() . '/' . StringUtility::getUniqueId('install-core-update-test-');
|
||||
$result = @touch($file);
|
||||
if (!$result) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'Could not write a file in path "' . Environment::getPublicPath() . '/"!',
|
||||
'Automatic TYPO3 CMS core update not possible: No write access to document root',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} else {
|
||||
// Check symlink creation
|
||||
$link = Environment::getPublicPath() . '/' . StringUtility::getUniqueId('install-core-update-test-');
|
||||
@symlink($file, $link);
|
||||
if (!is_link($link)) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'Could not create a symbolic link in path "' . Environment::getPublicPath() . '/"!',
|
||||
'Automatic TYPO3 CMS core update not possible: No symlink creation possible',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} else {
|
||||
unlink($link);
|
||||
}
|
||||
unlink($file);
|
||||
}
|
||||
|
||||
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-');
|
||||
$result = @touch($file);
|
||||
if (!$result) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'New TYPO3 CMS core should be installed in "' . $coreLocation . '", but this directory is not writable!',
|
||||
'Automatic TYPO3 CMS core update not possible: No write access to TYPO3 CMS core location',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} else {
|
||||
unlink($file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($success && !$this->coreVersionService->isInstalledVersionAReleasedVersion()) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'Your current version is specified as ' . $this->coreVersionService->getInstalledVersion() . '.'
|
||||
. ' This is a development version and can not be updated automatically. If this is a "git"'
|
||||
. ' checkout, please update using git directly.',
|
||||
'Automatic TYPO3 CMS core update not possible: You are running a development version of TYPO3',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
}
|
||||
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download the specified version
|
||||
*
|
||||
* @param CoreRelease $coreRelease A core release to download
|
||||
* @return bool TRUE on success
|
||||
*/
|
||||
public function downloadVersion(CoreRelease $coreRelease)
|
||||
{
|
||||
$version = $coreRelease->getVersion();
|
||||
$success = true;
|
||||
if ($this->checkCoreFilesAvailable($version)) {
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Skipped download of TYPO3 CMS core. A core source directory already exists in destination path. Using this instead.',
|
||||
FlashMessage::NOTICE
|
||||
));
|
||||
} else {
|
||||
$downloadUri = $this->downloadBaseUri . '/' . $version;
|
||||
$fileLocation = $this->getDownloadTarGzTargetPath($version);
|
||||
|
||||
if (@file_exists($fileLocation)) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'TYPO3 CMS core download exists in download location: ' . PathUtility::stripPathSitePrefix($this->downloadTargetPath),
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} else {
|
||||
$fileContent = GeneralUtility::getUrl($downloadUri);
|
||||
if (!$fileContent) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'Failed to download ' . $downloadUri,
|
||||
'Download not successful',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} else {
|
||||
$fileStoreResult = file_put_contents($fileLocation, $fileContent);
|
||||
if (!$fileStoreResult) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Unable to store download content',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} else {
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'TYPO3 CMS core download finished'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify checksum of downloaded version
|
||||
*
|
||||
* @param CoreRelease $coreRelease A downloaded core release to check
|
||||
* @return bool TRUE on success
|
||||
*/
|
||||
public function verifyFileChecksum(CoreRelease $coreRelease)
|
||||
{
|
||||
$version = $coreRelease->getVersion();
|
||||
$success = true;
|
||||
if ($this->checkCoreFilesAvailable($version)) {
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Verifying existing TYPO3 CMS core checksum is not possible',
|
||||
FlashMessage::WARNING
|
||||
));
|
||||
} else {
|
||||
$fileLocation = $this->getDownloadTarGzTargetPath($version);
|
||||
$expectedChecksum = $coreRelease->getChecksum();
|
||||
if (!file_exists($fileLocation)) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Downloaded TYPO3 CMS core not found',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} else {
|
||||
$actualChecksum = sha1_file($fileLocation);
|
||||
if ($actualChecksum !== $expectedChecksum) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'The official TYPO3 CMS version system on https://get.typo3.org expects a sha1 checksum of '
|
||||
. $expectedChecksum . ' from the content of the downloaded new TYPO3 CMS core version ' . $version . '.'
|
||||
. ' The actual checksum is ' . $actualChecksum . '. The update is stopped. This may be a'
|
||||
. ' failed download, an attack, or an issue with the typo3.org infrastructure.',
|
||||
'New TYPO3 CMS core checksum mismatch',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} else {
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Checksum verified'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unpack a downloaded core
|
||||
*
|
||||
* @param CoreRelease $coreRelease A core release to unpack
|
||||
* @return bool TRUE on success
|
||||
*/
|
||||
public function unpackVersion(CoreRelease $coreRelease)
|
||||
{
|
||||
$version = $coreRelease->getVersion();
|
||||
$success = true;
|
||||
if ($this->checkCoreFilesAvailable($version)) {
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Unpacking TYPO3 CMS core files skipped',
|
||||
FlashMessage::NOTICE
|
||||
));
|
||||
} else {
|
||||
$fileLocation = $this->downloadTargetPath . $version . '.tar.gz';
|
||||
if (!@is_file($fileLocation)) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Downloaded TYPO3 CMS core not found',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} elseif (@file_exists($this->downloadTargetPath . 'typo3_src-' . $version)) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Unpacked TYPO3 CMS core exists in download location: ' . PathUtility::stripPathSitePrefix($this->downloadTargetPath),
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} else {
|
||||
$unpackCommand = 'tar xf ' . escapeshellarg($fileLocation) . ' -C ' . escapeshellarg($this->downloadTargetPath) . ' 2>&1';
|
||||
exec($unpackCommand, $output, $errorCode);
|
||||
if ($errorCode) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Unpacking TYPO3 CMS core not successful',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} else {
|
||||
$removePackedFileResult = unlink($fileLocation);
|
||||
if (!$removePackedFileResult) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Removing packed TYPO3 CMS core not successful',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} else {
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Unpacking TYPO3 CMS core successful'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an unpacked core to its final destination
|
||||
*
|
||||
* @param CoreRelease $coreRelease A core release to move
|
||||
* @return bool TRUE on success
|
||||
*/
|
||||
public function moveVersion(CoreRelease $coreRelease)
|
||||
{
|
||||
$version = $coreRelease->getVersion();
|
||||
$success = true;
|
||||
if ($this->checkCoreFilesAvailable($version)) {
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Moving TYPO3 CMS core files skipped',
|
||||
FlashMessage::NOTICE
|
||||
));
|
||||
} else {
|
||||
$downloadedCoreLocation = $this->downloadTargetPath . 'typo3_src-' . $version;
|
||||
$newCoreLocation = @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
|
||||
|
||||
if (!@is_dir($downloadedCoreLocation)) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Unpacked TYPO3 CMS core not found',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} else {
|
||||
$moveResult = rename($downloadedCoreLocation, $newCoreLocation);
|
||||
if (!$moveResult) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Moving TYPO3 CMS core to ' . $newCoreLocation . ' failed',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} else {
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Moved TYPO3 CMS core to final location'
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Activate a core version
|
||||
*
|
||||
* @param CoreRelease $coreRelease A core release to activate
|
||||
* @return bool TRUE on success
|
||||
*/
|
||||
public function activateVersion(CoreRelease $coreRelease)
|
||||
{
|
||||
$newCoreLocation = @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $coreRelease->getVersion();
|
||||
$success = true;
|
||||
if (!is_dir($newCoreLocation)) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'New TYPO3 CMS core not found',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} elseif (!is_link($this->symlinkToCoreFiles)) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'TYPO3 CMS core source directory (typo3_src) is not a link',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} else {
|
||||
$isCurrentCoreSymlinkAbsolute = PathUtility::isAbsolutePath(readlink($this->symlinkToCoreFiles));
|
||||
$unlinkResult = unlink($this->symlinkToCoreFiles);
|
||||
if (!$unlinkResult) {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Removing old symlink failed',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
} else {
|
||||
if (!$isCurrentCoreSymlinkAbsolute) {
|
||||
$newCoreLocation = $this->getRelativePath($newCoreLocation);
|
||||
}
|
||||
$symlinkResult = symlink($newCoreLocation, $this->symlinkToCoreFiles);
|
||||
if ($symlinkResult) {
|
||||
GeneralUtility::makeInstance(OpcodeCacheService::class)->clearAllActive();
|
||||
} else {
|
||||
$success = false;
|
||||
$this->messages->enqueue(new FlashMessage(
|
||||
'',
|
||||
'Linking new TYPO3 CMS core failed',
|
||||
FlashMessage::ERROR
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
return $success;
|
||||
}
|
||||
|
||||
/**
|
||||
* Absolute path of downloaded .tar.gz
|
||||
*
|
||||
* @param string $version A version number
|
||||
* @return string
|
||||
*/
|
||||
protected function getDownloadTarGzTargetPath($version)
|
||||
{
|
||||
return $this->downloadTargetPath . $version . '.tar.gz';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get relative path to TYPO3 source directory from webroot
|
||||
*
|
||||
* @param string $absolutePath to TYPO3 source directory
|
||||
* @return string relative path to TYPO3 source directory
|
||||
*/
|
||||
protected function getRelativePath($absolutePath)
|
||||
{
|
||||
$sourcePath = explode(DIRECTORY_SEPARATOR, Environment::getPublicPath());
|
||||
$targetPath = explode(DIRECTORY_SEPARATOR, rtrim($absolutePath, DIRECTORY_SEPARATOR));
|
||||
while (count($sourcePath) && count($targetPath) && $sourcePath[0] === $targetPath[0]) {
|
||||
array_shift($sourcePath);
|
||||
array_shift($targetPath);
|
||||
}
|
||||
return str_pad('', count($sourcePath) * 3, '..' . DIRECTORY_SEPARATOR) . implode(DIRECTORY_SEPARATOR, $targetPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is are already core files available
|
||||
* at the download destination.
|
||||
*
|
||||
* @param string $version A version number
|
||||
* @return bool true when core files are available
|
||||
*/
|
||||
protected function checkCoreFilesAvailable($version)
|
||||
{
|
||||
$newCoreLocation = @realpath($this->symlinkToCoreFiles . '/../') . '/typo3_src-' . $version;
|
||||
return @is_dir($newCoreLocation);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,335 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace TYPO3\CMS\Install\Service;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Registry;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\VersionNumberUtility;
|
||||
use TYPO3\CMS\Install\CoreVersion\CoreRelease;
|
||||
use TYPO3\CMS\Install\CoreVersion\MaintenanceWindow;
|
||||
use TYPO3\CMS\Install\CoreVersion\MajorRelease;
|
||||
|
||||
/**
|
||||
* Core version service
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
class CoreVersionService
|
||||
{
|
||||
/**
|
||||
* @var \TYPO3\CMS\Core\Registry
|
||||
*/
|
||||
protected $registry;
|
||||
|
||||
/**
|
||||
* Base URI for TYPO3 Version REST api
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $apiBaseUrl = 'https://get.typo3.org/v1/api/';
|
||||
|
||||
/**
|
||||
* Initialize update URI
|
||||
*
|
||||
* @param Registry $registry Deprecated
|
||||
*/
|
||||
public function __construct(Registry $registry = null)
|
||||
{
|
||||
if (null !== $registry) {
|
||||
trigger_error(
|
||||
'The constructor parameter $registry for CoreVersionService is deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0.',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
$this->registry = $registry;
|
||||
} else {
|
||||
$this->registry = GeneralUtility::makeInstance(Registry::class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Development git checkout versions always end with '-dev'. They are
|
||||
* not "released" as such and can not be updated.
|
||||
*
|
||||
* @return bool FALSE If some development version is installed
|
||||
*/
|
||||
public function isInstalledVersionAReleasedVersion(): bool
|
||||
{
|
||||
$version = $this->getInstalledVersion();
|
||||
return substr($version, -4) !== '-dev';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current installed version number
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getInstalledVersion(): string
|
||||
{
|
||||
return VersionNumberUtility::getCurrentTypo3Version();
|
||||
}
|
||||
|
||||
public function getMaintenanceWindow(): MaintenanceWindow
|
||||
{
|
||||
$url = 'major/' . $this->getInstalledMajorVersion();
|
||||
$result = $this->fetchFromRemote($url);
|
||||
|
||||
return MaintenanceWindow::fromApiResponse($result);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{community: string[], elts: string[]}
|
||||
*/
|
||||
public function getSupportedMajorReleases(): array
|
||||
{
|
||||
$url = 'major';
|
||||
$result = $this->fetchFromRemote($url);
|
||||
|
||||
$majorReleases = [
|
||||
'community' => [],
|
||||
'elts' => [],
|
||||
];
|
||||
foreach ($result as $release) {
|
||||
$majorRelease = MajorRelease::fromApiResponse($release);
|
||||
$maintenanceWindow = $majorRelease->getMaintenanceWindow();
|
||||
|
||||
if ($maintenanceWindow->isSupportedByCommunity()) {
|
||||
$group = 'community';
|
||||
} elseif ($maintenanceWindow->isSupportedByElts()) {
|
||||
$group = 'elts';
|
||||
} else {
|
||||
// Major version is unsupported
|
||||
continue;
|
||||
}
|
||||
|
||||
$majorReleases[$group][] = $majorRelease->getLts() ?? $majorRelease->getVersion();
|
||||
}
|
||||
|
||||
return $majorReleases;
|
||||
}
|
||||
|
||||
public function isPatchReleaseSuitableForUpdate(CoreRelease $coreRelease): bool
|
||||
{
|
||||
return version_compare($this->getInstalledVersion(), $coreRelease->getVersion()) === -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if a younger patch level release exists in version matrix.
|
||||
*
|
||||
* @return bool TRUE if younger patch release is exists
|
||||
* @throws \TYPO3\CMS\Install\Service\Exception\RemoteFetchException
|
||||
*/
|
||||
public function isYoungerPatchReleaseAvailable(): bool
|
||||
{
|
||||
return version_compare($this->getInstalledVersion(), $this->getYoungestPatchRelease()->getVersion()) === -1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if an upgrade from current version is security relevant
|
||||
*
|
||||
* @return bool TRUE if there is a pending security update
|
||||
* @throws \TYPO3\CMS\Install\Service\Exception\RemoteFetchException
|
||||
*/
|
||||
public function isUpdateSecurityRelevant(CoreRelease $releaseToCheck): bool
|
||||
{
|
||||
$url = 'major/' . $this->getInstalledMajorVersion() . '/release';
|
||||
$result = $this->fetchFromRemote($url);
|
||||
|
||||
$installedVersion = $this->getInstalledVersion();
|
||||
foreach ($result as $release) {
|
||||
$coreRelease = CoreRelease::fromApiResponse($release);
|
||||
if ($coreRelease->isSecurityUpdate()
|
||||
&& version_compare($installedVersion, $coreRelease->getVersion()) === -1 // installed version is lower than release
|
||||
&& version_compare($releaseToCheck->getVersion(), $coreRelease->getVersion()) > -1 // release to check is equal or higher than release
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function isCurrentInstalledVersionElts(): bool
|
||||
{
|
||||
$url = 'major/' . $this->getInstalledMajorVersion() . '/release';
|
||||
$result = $this->fetchFromRemote($url);
|
||||
|
||||
$installedVersion = $this->getInstalledVersion();
|
||||
foreach ($result as $release) {
|
||||
if (version_compare($installedVersion, $release['version']) === 0) {
|
||||
return $release['elts'] ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Youngest patch release
|
||||
*
|
||||
* @return CoreRelease
|
||||
* @throws \TYPO3\CMS\Install\Service\Exception\RemoteFetchException
|
||||
*/
|
||||
public function getYoungestPatchRelease(): CoreRelease
|
||||
{
|
||||
$url = 'major/' . $this->getInstalledMajorVersion() . '/release/latest';
|
||||
$result = $this->fetchFromRemote($url);
|
||||
return CoreRelease::fromApiResponse($result);
|
||||
}
|
||||
|
||||
public function getYoungestCommunityPatchRelease(): CoreRelease
|
||||
{
|
||||
$url = 'major/' . $this->getInstalledMajorVersion() . '/release';
|
||||
$result = $this->fetchFromRemote($url);
|
||||
|
||||
// Make sure all releases are sorted by their version
|
||||
$columns = array_column($result, 'version');
|
||||
array_multisort($columns, SORT_NATURAL, $result);
|
||||
|
||||
// Remove any ELTS release
|
||||
$releases = array_filter($result, static function (array $release) {
|
||||
return ($release['elts'] ?? false) === false;
|
||||
});
|
||||
|
||||
$latestRelease = end($releases);
|
||||
|
||||
return CoreRelease::fromApiResponse($latestRelease);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $url
|
||||
* @return array
|
||||
* @throws \TYPO3\CMS\Install\Service\Exception\RemoteFetchException
|
||||
*/
|
||||
protected function fetchFromRemote(string $url): array
|
||||
{
|
||||
$url = $this->apiBaseUrl . $url;
|
||||
$json = GeneralUtility::getUrl($url);
|
||||
|
||||
if (!$json) {
|
||||
$this->throwFetchException($url);
|
||||
}
|
||||
return json_decode($json, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 'major version' from installed version of TYPO3, e.g., '7' from '7.3.0'
|
||||
*
|
||||
* @return string For example 7
|
||||
*/
|
||||
protected function getInstalledMajorVersion(): string
|
||||
{
|
||||
return $this->getMajorVersion($this->getInstalledVersion());
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 'major version' of version, e.g., '7' from '7.3.0'
|
||||
*
|
||||
* @param string $version to check
|
||||
* @return string Major version, e.g., '7'
|
||||
*/
|
||||
protected function getMajorVersion(string $version): string
|
||||
{
|
||||
$explodedVersion = explode('.', $version);
|
||||
return $explodedVersion[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Update version matrix from remote and store in registry
|
||||
*
|
||||
* @deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0 - use new REST API directly (see https://get.typo3.org/v1/api/doc)
|
||||
* @throws Exception\RemoteFetchException
|
||||
*/
|
||||
public function updateVersionMatrix(): void
|
||||
{
|
||||
trigger_error('Method updateVersionMatrix will be removed in TYPO3 v10.0, use new REST API directly (see https://get.typo3.org/v1/api/doc).', E_USER_DEPRECATED);
|
||||
$url = 'https://get.typo3.org/json';
|
||||
$versionJson = GeneralUtility::getUrl($url);
|
||||
if (!$versionJson) {
|
||||
$this->throwFetchException($url);
|
||||
}
|
||||
$versionArray = json_decode($versionJson, true);
|
||||
$installedMajorVersion = (int)$this->getInstalledMajorVersion();
|
||||
|
||||
foreach ($versionArray as $versionNumber => $versionDetails) {
|
||||
if (is_array($versionDetails) && (int)$this->getMajorVersion((string)$versionNumber) < $installedMajorVersion) {
|
||||
unset($versionArray[$versionNumber]);
|
||||
}
|
||||
}
|
||||
|
||||
$this->registry->set('TYPO3.CMS.Install', 'coreVersionMatrix', $versionArray);
|
||||
}
|
||||
|
||||
/**
|
||||
* Youngest development patch release, e.g., 6.2.0alpha3 or 6.2-snapshot-20131004
|
||||
*
|
||||
* @deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0 - TYPO3 release cycles do not contain development releases anymore
|
||||
* @return string
|
||||
* @throws \TYPO3\CMS\Install\Service\Exception\RemoteFetchException
|
||||
*/
|
||||
public function getYoungestPatchDevelopmentRelease(): string
|
||||
{
|
||||
trigger_error(
|
||||
'Method getYoungestPatchDevelopmentRelease() is deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0, use getYoungestPatchRelease() instead.',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
return $this->getYoungestPatchRelease()->getVersion();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns TRUE if a younger patch level release exists in version matrix that may be a development release.
|
||||
*
|
||||
* @deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0 - TYPO3 release cycles do not contain development releases anymore
|
||||
* @return bool TRUE if younger patch release is exists
|
||||
* @throws \TYPO3\CMS\Install\Service\Exception\RemoteFetchException
|
||||
*/
|
||||
public function isYoungerPatchDevelopmentReleaseAvailable(): bool
|
||||
{
|
||||
trigger_error(
|
||||
'Method isYoungerPatchDevelopmentReleaseAvailable() is deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0, use isYoungerPatchReleaseAvailable() instead.',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
return $this->isYoungerPatchReleaseAvailable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0, use 'https://get.typo3.org' directly
|
||||
* @return string
|
||||
*/
|
||||
public function getDownloadBaseUrl(): string
|
||||
{
|
||||
trigger_error(
|
||||
'Method getDownloadBaseUrl() is deprecated since TYPO3 v9 and will be removed in TYPO3 v10.0, use https://get.typo3.org directly.',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
return $this->apiBaseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method to throw same exception in multiple places
|
||||
*
|
||||
* @param string $url
|
||||
* @throws \TYPO3\CMS\Install\Service\Exception\RemoteFetchException
|
||||
*/
|
||||
protected function throwFetchException(string $url): void
|
||||
{
|
||||
throw new Exception\RemoteFetchException(
|
||||
'Fetching ' .
|
||||
$url .
|
||||
' failed. Maybe this instance can not connect to the remote system properly.',
|
||||
1380897593
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Install\Service;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Basic Service to check and create install tool files
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
class EnableFileService
|
||||
{
|
||||
/**
|
||||
* @var string Relative path to ENABLE_INSTALL_TOOL file
|
||||
*/
|
||||
const INSTALL_TOOL_ENABLE_FILE_PATH = 'typo3conf/ENABLE_INSTALL_TOOL';
|
||||
|
||||
/**
|
||||
* @var string Relative path to FIRST_INSTALL file
|
||||
*/
|
||||
const FIRST_INSTALL_FILE_PATH = 'FIRST_INSTALL';
|
||||
|
||||
/**
|
||||
* @var string Maximum age of ENABLE_INSTALL_TOOL file before it gets removed (in seconds)
|
||||
*/
|
||||
const INSTALL_TOOL_ENABLE_FILE_LIFETIME = 3600;
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public static function isFirstInstallAllowed()
|
||||
{
|
||||
$files = self::getFirstInstallFilePaths();
|
||||
if (!empty($files)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates the INSTALL_TOOL_ENABLE file
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function createInstallToolEnableFile()
|
||||
{
|
||||
$installEnableFilePath = self::getInstallToolEnableFilePath();
|
||||
if (!is_file($installEnableFilePath)) {
|
||||
$result = touch($installEnableFilePath);
|
||||
} else {
|
||||
$result = true;
|
||||
self::extendInstallToolEnableFileLifetime();
|
||||
}
|
||||
GeneralUtility::fixPermissions($installEnableFilePath);
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the INSTALL_TOOL_ENABLE file
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function removeInstallToolEnableFile()
|
||||
{
|
||||
return unlink(self::getInstallToolEnableFilePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the FIRST_INSTALL file
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function removeFirstInstallFile()
|
||||
{
|
||||
$result = true;
|
||||
$files = self::getFirstInstallFilePaths();
|
||||
foreach ($files as $file) {
|
||||
$result = unlink(Environment::getPublicPath() . '/' . $file) && $result;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the install tool file exists
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function installToolEnableFileExists()
|
||||
{
|
||||
return @is_file(self::getInstallToolEnableFilePath());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the install tool file exists
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function checkInstallToolEnableFile()
|
||||
{
|
||||
if (!self::installToolEnableFileExists()) {
|
||||
return false;
|
||||
}
|
||||
if (!self::isInstallToolEnableFilePermanent()) {
|
||||
if (self::installToolEnableFileLifetimeExpired()) {
|
||||
self::removeInstallToolEnableFile();
|
||||
return false;
|
||||
}
|
||||
self::extendInstallToolEnableFileLifetime();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the install tool file should be kept
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInstallToolEnableFilePermanent()
|
||||
{
|
||||
if (self::installToolEnableFileExists()) {
|
||||
$content = @file_get_contents(self::getInstallToolEnableFilePath());
|
||||
if (strpos($content, 'KEEP_FILE') !== false) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the lifetime of the install tool file is expired
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public static function installToolEnableFileLifetimeExpired()
|
||||
{
|
||||
if (time() - @filemtime(self::getInstallToolEnableFilePath()) > self::INSTALL_TOOL_ENABLE_FILE_LIFETIME) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the last modification of the ENABLE_INSTALL_TOOL file
|
||||
*/
|
||||
protected static function extendInstallToolEnableFileLifetime()
|
||||
{
|
||||
$enableFile = self::getInstallToolEnableFilePath();
|
||||
// Extend the age of the ENABLE_INSTALL_TOOL file by one hour
|
||||
if (is_file($enableFile)) {
|
||||
$couldTouch = @touch($enableFile);
|
||||
if (!$couldTouch) {
|
||||
// If we can't remove the creation method will call us again.
|
||||
if (self::removeInstallToolEnableFile()) {
|
||||
self::createInstallToolEnableFile();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path to the INSTALL_TOOL_ENABLE file
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
protected static function getInstallToolEnableFilePath()
|
||||
{
|
||||
return Environment::getPublicPath() . '/' . self::INSTALL_TOOL_ENABLE_FILE_PATH;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the paths to the FIRST_INSTALL files
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected static function getFirstInstallFilePaths()
|
||||
{
|
||||
$files = array_filter(scandir(Environment::getPublicPath() . '/'), function ($file) {
|
||||
return @is_file(Environment::getPublicPath() . '/' . $file) && preg_match('~^' . self::FIRST_INSTALL_FILE_PATH . '.*~i', $file);
|
||||
});
|
||||
return $files;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Install\Service;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* A service exception
|
||||
*/
|
||||
class Exception extends \TYPO3\CMS\Install\Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Install\Service\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Install\Service\Exception;
|
||||
|
||||
/**
|
||||
* An exception thrown if the silent configuration updater changed configuration
|
||||
*/
|
||||
class ConfigurationChangedException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Install\Service\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Install\Service\Exception;
|
||||
|
||||
/**
|
||||
* An exception thrown if version validation against official version matrix fails
|
||||
*/
|
||||
class CoreVersionServiceException extends Exception
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Install\Service\Exception;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
/**
|
||||
* An exception thrown if fetching a resource from a remote server fails
|
||||
*/
|
||||
class RemoteFetchException extends CoreVersionServiceException
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,351 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Install\Service;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Configuration\Exception\ExtensionConfigurationPathDoesNotExistException;
|
||||
use TYPO3\CMS\Core\Configuration\ExtensionConfiguration;
|
||||
use TYPO3\CMS\Core\Localization\LanguageService;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
use TYPO3\CMS\Core\Utility\ArrayUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Service to prepare extension configuration settings from ext_conf_template.txt
|
||||
* to be viewed in the install tool. The class basically adds display related
|
||||
* stuff on top of ext:core ExtensionConfiguration.
|
||||
*
|
||||
* Extension authors should use TYPO3\CMS\Core\Configuration\ExtensionConfiguration
|
||||
* class to get() extension configuration settings.
|
||||
*
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
class ExtensionConfigurationService
|
||||
{
|
||||
/**
|
||||
* This will be filled with the available categories of the current template.
|
||||
* Used parsing ext_conf_template.txt
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $subCategories = [
|
||||
// Standard categories:
|
||||
'enable' => ['Enable features', 'a'],
|
||||
'dims' => ['Dimensions, widths, heights, pixels', 'b'],
|
||||
'file' => ['Files', 'c'],
|
||||
'typo' => ['Typography', 'd'],
|
||||
'color' => ['Colors', 'e'],
|
||||
'links' => ['Links and targets', 'f'],
|
||||
'language' => ['Language specific constants', 'g'],
|
||||
// subcategories based on the default content elements
|
||||
'cheader' => ['Content: \'Header\'', 'ma'],
|
||||
'cheader_g' => ['Content: \'Header\', Graphical', 'ma'],
|
||||
'ctext' => ['Content: \'Text\'', 'mb'],
|
||||
'cimage' => ['Content: \'Image\'', 'md'],
|
||||
'ctextmedia' => ['Content: \'Textmedia\'', 'ml'],
|
||||
'cbullets' => ['Content: \'Bullet list\'', 'me'],
|
||||
'ctable' => ['Content: \'Table\'', 'mf'],
|
||||
'cuploads' => ['Content: \'Filelinks\'', 'mg'],
|
||||
'cmultimedia' => ['Content: \'Multimedia\'', 'mh'],
|
||||
'cmedia' => ['Content: \'Media\'', 'mr'],
|
||||
'cmailform' => ['Content: \'Form\'', 'mi'],
|
||||
'csearch' => ['Content: \'Search\'', 'mj'],
|
||||
'clogin' => ['Content: \'Login\'', 'mk'],
|
||||
'cmenu' => ['Content: \'Menu/Sitemap\'', 'mm'],
|
||||
'cshortcut' => ['Content: \'Insert records\'', 'mn'],
|
||||
'clist' => ['Content: \'List of records\'', 'mo'],
|
||||
'chtml' => ['Content: \'HTML\'', 'mq']
|
||||
];
|
||||
|
||||
/**
|
||||
* Compiles ext_conf_template file and merges it with values from LocalConfiguration['EXTENSIONS'].
|
||||
* Returns a funny array used to display the configuration form in the install tool.
|
||||
*
|
||||
* @param string $extensionKey Extension key
|
||||
* @return array
|
||||
*/
|
||||
public function getConfigurationPreparedForView(string $extensionKey): array
|
||||
{
|
||||
$package = GeneralUtility::makeInstance(PackageManager::class)->getPackage($extensionKey);
|
||||
if (!@is_file($package->getPackagePath() . 'ext_conf_template.txt')) {
|
||||
return [];
|
||||
}
|
||||
$extensionConfiguration = new ExtensionConfiguration();
|
||||
$configuration = $this->getDefaultConfigurationFromExtConfTemplateAsValuedArray($extensionKey);
|
||||
foreach ($configuration as $configurationPath => &$details) {
|
||||
try {
|
||||
$valueFromLocalConfiguration = $extensionConfiguration->get($extensionKey, str_replace('.', '/', $configurationPath));
|
||||
$details['value'] = $valueFromLocalConfiguration;
|
||||
} catch (ExtensionConfigurationPathDoesNotExistException $e) {
|
||||
// Deliberately empty - it can happen at runtime that a written config does not return
|
||||
// back all values (eg. saltedpassword with its userFuncs), which then miss in the written
|
||||
// configuration and are only synced after next install tool run. This edge case is
|
||||
// taken care off here.
|
||||
}
|
||||
}
|
||||
$resultArray = [];
|
||||
if (!empty($configuration)) {
|
||||
$hierarchicConfiguration = [];
|
||||
foreach ($configuration as $configurationOption) {
|
||||
$originalConfiguration = $this->buildConfigurationArray($configurationOption);
|
||||
ArrayUtility::mergeRecursiveWithOverrule($originalConfiguration, $hierarchicConfiguration);
|
||||
$hierarchicConfiguration = $originalConfiguration;
|
||||
}
|
||||
// Flip category array as it was merged the other way around
|
||||
$hierarchicConfiguration = array_reverse($hierarchicConfiguration);
|
||||
// Sort configurations of each subcategory
|
||||
foreach ($hierarchicConfiguration as &$catConfigurationArray) {
|
||||
foreach ($catConfigurationArray as &$subcatConfigurationArray) {
|
||||
uasort($subcatConfigurationArray, function ($a, $b) {
|
||||
return strnatcmp($a['subcat'], $b['subcat']);
|
||||
});
|
||||
}
|
||||
unset($subcatConfigurationArray);
|
||||
}
|
||||
unset($tempConfiguration);
|
||||
$resultArray = $hierarchicConfiguration;
|
||||
}
|
||||
return $resultArray;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a configuration array from each line (option) of the config file.
|
||||
* Helper method for getConfigurationPreparedForView()
|
||||
*
|
||||
* @param array $configurationOption config file line representing one setting
|
||||
* @return array
|
||||
*/
|
||||
protected function buildConfigurationArray(array $configurationOption): array
|
||||
{
|
||||
$hierarchicConfiguration = [];
|
||||
if (GeneralUtility::isFirstPartOfStr($configurationOption['type'], 'user')) {
|
||||
$configurationOption = $this->extractInformationForConfigFieldsOfTypeUser($configurationOption);
|
||||
} elseif (GeneralUtility::isFirstPartOfStr($configurationOption['type'], 'options')) {
|
||||
$configurationOption = $this->extractInformationForConfigFieldsOfTypeOptions($configurationOption);
|
||||
}
|
||||
$languageService = $this->getLanguageService();
|
||||
if (is_string($configurationOption['label'])) {
|
||||
$translatedLabel = $languageService->sL($configurationOption['label']);
|
||||
if ($translatedLabel) {
|
||||
$configurationOption['label'] = $translatedLabel;
|
||||
}
|
||||
}
|
||||
$configurationOption['labels'] = GeneralUtility::trimExplode(':', $configurationOption['label'], false, 2);
|
||||
$configurationOption['subcat_name'] = $configurationOption['subcat_name'] ?: '__default';
|
||||
$hierarchicConfiguration[$configurationOption['cat']][$configurationOption['subcat_name']][$configurationOption['name']] = $configurationOption;
|
||||
return $hierarchicConfiguration;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts additional information for fields of type "options"
|
||||
* Extracts "type", "label" and values information
|
||||
*
|
||||
* @param array $configurationOption
|
||||
* @return array
|
||||
*/
|
||||
protected function extractInformationForConfigFieldsOfTypeOptions(array $configurationOption): array
|
||||
{
|
||||
preg_match('/options\[(.*)\]/is', $configurationOption['type'], $typeMatches);
|
||||
$optionItems = GeneralUtility::trimExplode(',', $typeMatches[1]);
|
||||
foreach ($optionItems as $optionItem) {
|
||||
$optionPair = GeneralUtility::trimExplode('=', $optionItem);
|
||||
if (count($optionPair) === 2) {
|
||||
$configurationOption['generic'][$optionPair[0]] = $optionPair[1];
|
||||
} else {
|
||||
$configurationOption['generic'][$optionPair[0]] = $optionPair[0];
|
||||
}
|
||||
}
|
||||
$configurationOption['type'] = 'options';
|
||||
return $configurationOption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract additional information for fields of type "user"
|
||||
* Extracts "type" and the function to be called
|
||||
*
|
||||
* @param array $configurationOption
|
||||
* @return array
|
||||
*/
|
||||
protected function extractInformationForConfigFieldsOfTypeUser(array $configurationOption): array
|
||||
{
|
||||
preg_match('/user\\[(.*)\\]/is', $configurationOption['type'], $matches);
|
||||
$configurationOption['generic'] = $matches[1];
|
||||
$configurationOption['type'] = 'user';
|
||||
return $configurationOption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a flat array of configuration options from
|
||||
* ext_conf_template.txt of an extension using core's typoscript parser.
|
||||
*
|
||||
* Result is an array, with configuration item as array keys,
|
||||
* and item properties as key-value sub-array:
|
||||
*
|
||||
* array(
|
||||
* 'fooOption' => array(
|
||||
* 'type' => 'string',
|
||||
* 'value' => 'foo',
|
||||
* ...
|
||||
* ),
|
||||
* 'barOption' => array(
|
||||
* 'type' => boolean,
|
||||
* 'default_value' => 0,
|
||||
* ...
|
||||
* ),
|
||||
* ...
|
||||
* )
|
||||
*
|
||||
* @param string $extensionKey Extension key
|
||||
* @return array
|
||||
*/
|
||||
protected function getDefaultConfigurationFromExtConfTemplateAsValuedArray(string $extensionKey): array
|
||||
{
|
||||
$parsedConstants = (new ExtensionConfiguration())->getParsedExtConfTemplate($extensionKey);
|
||||
$flatSetup = $this->flattenSetup($parsedConstants);
|
||||
$theConstants = $this->parseComments($flatSetup);
|
||||
|
||||
// Loop through configuration items, see if it is assigned to a sub category
|
||||
// and add the sub category label to the item property if so.
|
||||
foreach ($theConstants as $configurationOptionName => $configurationOption) {
|
||||
if (
|
||||
array_key_exists('subcat_name', $configurationOption)
|
||||
&& isset($this->subCategories[$configurationOption['subcat_name']])
|
||||
&& isset($this->subCategories[$configurationOption['subcat_name']][0])
|
||||
) {
|
||||
$theConstants[$configurationOptionName]['subcat_label'] = $this->subCategories[$configurationOption['subcat_name']][0];
|
||||
}
|
||||
$theConstants[$configurationOptionName]['extensionKey'] = $extensionKey;
|
||||
}
|
||||
return $theConstants;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function compares the flattened constants (default and all).
|
||||
* Returns an array with the constants from the whole template which may be edited by the module.
|
||||
*
|
||||
* @param array $flatSetup
|
||||
* @return array
|
||||
*/
|
||||
protected function parseComments($flatSetup)
|
||||
{
|
||||
$categoryLabels = [];
|
||||
$editableComments = [];
|
||||
$counter = 0;
|
||||
foreach ($flatSetup as $const => $value) {
|
||||
if (substr($const, -2) === '..' || !isset($flatSetup[$const . '..'])) {
|
||||
continue;
|
||||
}
|
||||
$counter++;
|
||||
$comment = trim($flatSetup[$const . '..']);
|
||||
$c_arr = explode(LF, $comment);
|
||||
foreach ($c_arr as $k => $v) {
|
||||
$line = trim(preg_replace('/^[#\\/]*/', '', $v));
|
||||
if (!$line) {
|
||||
continue;
|
||||
}
|
||||
$parts = explode(';', $line);
|
||||
foreach ($parts as $par) {
|
||||
if (strstr($par, '=')) {
|
||||
$keyValPair = explode('=', $par, 2);
|
||||
switch (trim(strtolower($keyValPair[0]))) {
|
||||
case 'type':
|
||||
// Type:
|
||||
$editableComments[$const]['type'] = trim($keyValPair[1]);
|
||||
break;
|
||||
case 'cat':
|
||||
// List of categories.
|
||||
$catSplit = explode('/', strtolower($keyValPair[1]));
|
||||
$catSplit[0] = trim($catSplit[0]);
|
||||
if (isset($categoryLabels[$catSplit[0]])) {
|
||||
$catSplit[0] = $categoryLabels[$catSplit[0]];
|
||||
}
|
||||
$editableComments[$const]['cat'] = $catSplit[0];
|
||||
// This is the subcategory. Must be a key in $this->subCategories[].
|
||||
// catSplit[2] represents the search-order within the subcat.
|
||||
$catSplit[1] = !empty($catSplit[1]) ? trim($catSplit[1]) : '';
|
||||
if ($catSplit[1] && isset($this->subCategories[$catSplit[1]])) {
|
||||
$editableComments[$const]['subcat_name'] = $catSplit[1];
|
||||
$orderIdentifier = isset($catSplit[2]) ? trim($catSplit[2]) : $counter;
|
||||
$editableComments[$const]['subcat'] = $this->subCategories[$catSplit[1]][1]
|
||||
. '/' . $catSplit[1] . '/' . $orderIdentifier . 'z';
|
||||
} elseif (isset($catSplit[2])) {
|
||||
$editableComments[$const]['subcat'] = 'x' . '/' . trim($catSplit[2]) . 'z';
|
||||
} else {
|
||||
$editableComments[$const]['subcat'] = 'x' . '/' . $counter . 'z';
|
||||
}
|
||||
break;
|
||||
case 'label':
|
||||
// Label
|
||||
$editableComments[$const]['label'] = trim($keyValPair[1]);
|
||||
break;
|
||||
case 'customcategory':
|
||||
// Custom category label
|
||||
$customCategory = explode('=', $keyValPair[1], 2);
|
||||
if (trim($customCategory[0])) {
|
||||
$categoryKey = strtolower($customCategory[0]);
|
||||
$categoryLabels[$categoryKey] = $this->getLanguageService()->sL($customCategory[1]);
|
||||
}
|
||||
break;
|
||||
case 'customsubcategory':
|
||||
// Custom subCategory label
|
||||
$customSubcategory = explode('=', $keyValPair[1], 2);
|
||||
if (trim($customSubcategory[0])) {
|
||||
$subCategoryKey = strtolower($customSubcategory[0]);
|
||||
$this->subCategories[$subCategoryKey][0] = $this->getLanguageService()->sL($customSubcategory[1]);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (isset($editableComments[$const])) {
|
||||
$editableComments[$const]['name'] = $const;
|
||||
$editableComments[$const]['value'] = trim($value);
|
||||
$editableComments[$const]['default_value'] = trim($value);
|
||||
}
|
||||
}
|
||||
return $editableComments;
|
||||
}
|
||||
|
||||
/**
|
||||
* This flattens a hierarchical TypoScript array to a dotted notation
|
||||
*
|
||||
* @param array $setupArray TypoScript array
|
||||
* @param string $prefix Prefix to the object path. Used for recursive calls to this function.
|
||||
* @return array
|
||||
*/
|
||||
protected function flattenSetup($setupArray, $prefix = '')
|
||||
{
|
||||
$flatSetup = [];
|
||||
if (is_array($setupArray)) {
|
||||
foreach ($setupArray as $key => $val) {
|
||||
if (is_array($val)) {
|
||||
$flatSetup = array_merge($flatSetup, $this->flattenSetup($val, $prefix . $key));
|
||||
} else {
|
||||
$flatSetup[$prefix . $key] = $val;
|
||||
}
|
||||
}
|
||||
}
|
||||
return $flatSetup;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return LanguageService
|
||||
*/
|
||||
protected function getLanguageService()
|
||||
{
|
||||
return $GLOBALS['LANG'];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,370 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Install\Service;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use TYPO3\CMS\Core\Configuration\Features;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Http\RequestFactory;
|
||||
use TYPO3\CMS\Core\Localization\Locales;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
use TYPO3\CMS\Core\Registry;
|
||||
use TYPO3\CMS\Core\Service\Archive\ZipService;
|
||||
use TYPO3\CMS\Core\Utility\ExtensionManagementUtility;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Core\Utility\PathUtility;
|
||||
use TYPO3\CMS\Extbase\SignalSlot\Dispatcher;
|
||||
|
||||
/**
|
||||
* Service class handling language pack details
|
||||
* Used by 'manage language packs' module and 'language packs command'
|
||||
*
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
class LanguagePackService
|
||||
{
|
||||
/**
|
||||
* @var Locales
|
||||
*/
|
||||
protected $locales;
|
||||
|
||||
/**
|
||||
* @var Registry
|
||||
*/
|
||||
protected $registry;
|
||||
|
||||
/**
|
||||
* @var RequestFactory
|
||||
*/
|
||||
protected $requestFactory;
|
||||
|
||||
private const OLD_LANGUAGE_PACK_URLS = [
|
||||
'https://extensions.typo3.org/fileadmin/l10n/',
|
||||
'https://typo3.org/fileadmin/ter/',
|
||||
'https://beta-translation.typo3.org/fileadmin/ter/',
|
||||
'https://localize.typo3.org/fileadmin/ter/'
|
||||
];
|
||||
|
||||
private const OLD_LANGUAGE_PACK_URL = 'https://extensions.typo3.org/fileadmin/l10n/';
|
||||
|
||||
private const LANGUAGE_PACK_URL = 'https://localize.typo3.org/xliff/';
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->locales = GeneralUtility::makeInstance(Locales::class);
|
||||
$this->registry = GeneralUtility::makeInstance(Registry::class);
|
||||
$this->requestFactory = GeneralUtility::makeInstance(RequestFactory::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available languages
|
||||
*
|
||||
* @return array iso=>name
|
||||
*/
|
||||
public function getAvailableLanguages(): array
|
||||
{
|
||||
return $this->locales->getLanguages();
|
||||
}
|
||||
|
||||
/**
|
||||
* List of languages active in this instance
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getActiveLanguages(): array
|
||||
{
|
||||
$availableLanguages = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lang']['availableLanguages'] ?? [];
|
||||
return array_filter($availableLanguages);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an array with language details: active or not, iso codes, last update, ...
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getLanguageDetails(): array
|
||||
{
|
||||
$availableLanguages = $this->getAvailableLanguages();
|
||||
$activeLanguages = $this->getActiveLanguages();
|
||||
$languages = [];
|
||||
foreach ($availableLanguages as $iso => $name) {
|
||||
if ($iso === 'default') {
|
||||
continue;
|
||||
}
|
||||
$lastUpdate = $this->registry->get('languagePacks', $iso);
|
||||
$languages[] = [
|
||||
'iso' => $iso,
|
||||
'name' => $name,
|
||||
'active' => in_array($iso, $activeLanguages, true),
|
||||
'lastUpdate' => $this->getFormattedDate($lastUpdate),
|
||||
'dependencies' => $this->locales->getLocaleDependencies($iso),
|
||||
];
|
||||
}
|
||||
usort($languages, function ($a, $b) {
|
||||
// Sort languages by name
|
||||
if ($a['name'] === $b['name']) {
|
||||
return 0;
|
||||
}
|
||||
return $a['name'] < $b['name'] ? -1 : 1;
|
||||
});
|
||||
return $languages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a list of loaded extensions and their language packs details
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getExtensionLanguagePackDetails(): array
|
||||
{
|
||||
$activeLanguages = $this->getActiveLanguages();
|
||||
$packageManager = GeneralUtility::makeInstance(PackageManager::class);
|
||||
$activePackages = $packageManager->getActivePackages();
|
||||
$extensions = [];
|
||||
$activeExtensions = [];
|
||||
foreach ($activePackages as $package) {
|
||||
$path = $package->getPackagePath();
|
||||
$finder = new Finder();
|
||||
try {
|
||||
$files = $finder->files()->in($path . 'Resources/Private/Language/')->name('*.xlf');
|
||||
if ($files->count() === 0) {
|
||||
// This extension has no .xlf files
|
||||
continue;
|
||||
}
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
// Dir does not exist
|
||||
continue;
|
||||
}
|
||||
$key = $package->getPackageKey();
|
||||
$activeExtensions[] = $key;
|
||||
$title = $package->getValueFromComposerManifest('description') ?? '';
|
||||
if (is_file($path . 'ext_emconf.php')) {
|
||||
$_EXTKEY = $key;
|
||||
$EM_CONF = [];
|
||||
include $path . 'ext_emconf.php';
|
||||
$title = $EM_CONF[$key]['title'] ?? $title;
|
||||
}
|
||||
$extension = [
|
||||
'key' => $key,
|
||||
'title' => $title,
|
||||
];
|
||||
if (!empty(ExtensionManagementUtility::getExtensionIcon($path, false))) {
|
||||
$extension['icon'] = PathUtility::stripPathSitePrefix(ExtensionManagementUtility::getExtensionIcon($path, true));
|
||||
}
|
||||
$extension['packs'] = [];
|
||||
foreach ($activeLanguages as $iso) {
|
||||
$isLanguagePackDownloaded = is_dir(Environment::getLabelsPath() . '/' . $iso . '/' . $key . '/');
|
||||
$lastUpdate = $this->registry->get('languagePacks', $iso . '-' . $key);
|
||||
$extension['packs'][] = [
|
||||
'iso' => $iso,
|
||||
'exists' => $isLanguagePackDownloaded,
|
||||
'lastUpdate' => $this->getFormattedDate($lastUpdate),
|
||||
];
|
||||
}
|
||||
$extensions[] = $extension;
|
||||
}
|
||||
usort($extensions, function ($a, $b) {
|
||||
// Sort extensions by key
|
||||
if ($a['key'] === $b['key']) {
|
||||
return 0;
|
||||
}
|
||||
return $a['key'] < $b['key'] ? -1 : 1;
|
||||
});
|
||||
return $extensions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update main language pack download location if possible.
|
||||
* Store to registry to be used during language pack update
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function updateMirrorBaseUrl(): string
|
||||
{
|
||||
$repositoryUrl = 'https://repositories.typo3.org/mirrors.xml.gz';
|
||||
$downloadBaseUrl = false;
|
||||
try {
|
||||
$response = $this->requestFactory->request($repositoryUrl);
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$xmlContent = @gzdecode($response->getBody()->getContents());
|
||||
if (!empty($xmlContent['mirror']['host']) && !empty($xmlContent['mirror']['path'])) {
|
||||
$downloadBaseUrl = 'https://' . $xmlContent['mirror']['host'] . $xmlContent['mirror']['path'];
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
// Catch generic exception, fallback handled below
|
||||
}
|
||||
if (empty($downloadBaseUrl)) {
|
||||
// Hard coded fallback if something went wrong fetching & parsing mirror list
|
||||
$downloadBaseUrl = self::OLD_LANGUAGE_PACK_URL;
|
||||
}
|
||||
$this->registry->set('languagePacks', 'baseUrl', $downloadBaseUrl);
|
||||
return $downloadBaseUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Download and unpack a single language pack of one extension.
|
||||
*
|
||||
* @param string $key Extension key
|
||||
* @param string $iso Language iso code
|
||||
* @return string One of 'update', 'new' or 'failed'
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function languagePackDownload(string $key, string $iso): string
|
||||
{
|
||||
// Sanitize extension and iso code
|
||||
$availableLanguages = $this->getAvailableLanguages();
|
||||
$activeLanguages = $this->getActiveLanguages();
|
||||
if (!array_key_exists($iso, $availableLanguages) || !in_array($iso, $activeLanguages, true)) {
|
||||
throw new \RuntimeException('Language iso code ' . (string)$iso . ' not available or active', 1520117054);
|
||||
}
|
||||
$packageManager = GeneralUtility::makeInstance(PackageManager::class);
|
||||
$activePackages = $packageManager->getActivePackages();
|
||||
$packageActive = false;
|
||||
foreach ($activePackages as $package) {
|
||||
if ($package->getPackageKey() === $key) {
|
||||
$packageActive = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!$packageActive) {
|
||||
throw new \RuntimeException('Extension ' . (string)$key . ' not loaded', 1520117245);
|
||||
}
|
||||
|
||||
$languagePackBaseUrl = $this->registry->get('languagePacks', 'baseUrl');
|
||||
if (empty($languagePackBaseUrl)) {
|
||||
throw new \RuntimeException('Language pack baseUrl not found', 1520169691);
|
||||
}
|
||||
|
||||
if (in_array($languagePackBaseUrl, self::OLD_LANGUAGE_PACK_URLS, true)
|
||||
&& GeneralUtility::makeInstance(Features::class)->isFeatureEnabled('newTranslationServer')) {
|
||||
$languagePackBaseUrl = self::LANGUAGE_PACK_URL;
|
||||
}
|
||||
|
||||
// Allow to modify the base url on the fly by calling a signal
|
||||
$signalSlotDispatcher = GeneralUtility::makeInstance(Dispatcher::class);
|
||||
$signalSlotDispatcher->dispatch(
|
||||
'TYPO3\\CMS\\Lang\\Service\\TranslationService',
|
||||
'postProcessMirrorUrl',
|
||||
[
|
||||
'extensionKey' => $key,
|
||||
'mirrorUrl' => &$languagePackBaseUrl,
|
||||
]
|
||||
);
|
||||
|
||||
$path = ExtensionManagementUtility::extPath($key);
|
||||
$majorVersion = explode('.', TYPO3_branch)[0];
|
||||
if (strpos($path, '/sysext/') !== false) {
|
||||
// This is a system extension and the package URL should be adapted to have different packs per core major version
|
||||
// https://extensions.typo3.org/fileadmin/l10n/b/a/backend-l10n/backend-l10n-fr.v9.zip
|
||||
$packageUrl = $key[0] . '/' . $key[1] . '/' . $key . '-l10n/' . $key . '-l10n-' . $iso . '.v' . $majorVersion . '.zip';
|
||||
} else {
|
||||
// Typical non sysext path, Hungarian:
|
||||
// https://extensions.typo3.org/fileadmin/l10n/a/n/anextension-l10n/anextension-l10n-hu.zip
|
||||
$packageUrl = $key[0] . '/' . $key[1] . '/' . $key . '-l10n/' . $key . '-l10n-' . $iso . '.zip';
|
||||
}
|
||||
|
||||
$absoluteLanguagePath = Environment::getLabelsPath() . '/' . $iso . '/';
|
||||
$absoluteExtractionPath = $absoluteLanguagePath . $key . '/';
|
||||
$absolutePathToZipFile = Environment::getVarPath() . '/transient/' . $key . '-l10n-' . $iso . '.zip';
|
||||
|
||||
$packExists = is_dir($absoluteExtractionPath);
|
||||
|
||||
$packResult = $packExists ? 'update' : 'new';
|
||||
|
||||
$operationResult = false;
|
||||
try {
|
||||
$response = $this->requestFactory->request($languagePackBaseUrl . $packageUrl);
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$languagePackContent = $response->getBody()->getContents();
|
||||
if (!empty($languagePackContent)) {
|
||||
$operationResult = true;
|
||||
if ($packExists) {
|
||||
$operationResult = GeneralUtility::rmdir($absoluteExtractionPath, true);
|
||||
}
|
||||
if ($operationResult) {
|
||||
GeneralUtility::mkdir_deep(Environment::getVarPath() . '/transient/');
|
||||
$operationResult = GeneralUtility::writeFileToTypo3tempDir($absolutePathToZipFile, $languagePackContent) === null;
|
||||
}
|
||||
$this->unzipTranslationFile($absolutePathToZipFile, $absoluteLanguagePath);
|
||||
if ($operationResult) {
|
||||
$operationResult = unlink($absolutePathToZipFile);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
$operationResult = false;
|
||||
}
|
||||
if (!$operationResult) {
|
||||
$packResult = 'failed';
|
||||
$this->registry->set('languagePacks', $iso . '-' . $key, time());
|
||||
}
|
||||
return $packResult;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set 'last update' timestamp in registry for a series of iso codes.
|
||||
*
|
||||
* @param string[] $isos List of iso code timestamps to set
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function setLastUpdatedIsoCode(array $isos)
|
||||
{
|
||||
$activeLanguages = $GLOBALS['TYPO3_CONF_VARS']['EXTCONF']['lang']['availableLanguages'] ?? [];
|
||||
$registry = GeneralUtility::makeInstance(Registry::class);
|
||||
foreach ($isos as $iso) {
|
||||
if (!in_array($iso, $activeLanguages, true)) {
|
||||
throw new \RuntimeException('Language iso code ' . (string)$iso . ' not available or active', 1520176318);
|
||||
}
|
||||
$registry->set('languagePacks', $iso, time());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a timestamp to a formatted date string
|
||||
*
|
||||
* @param int|null $timestamp
|
||||
* @return string|null
|
||||
*/
|
||||
protected function getFormattedDate($timestamp)
|
||||
{
|
||||
if (is_int($timestamp)) {
|
||||
$date = new \DateTime('@' . $timestamp);
|
||||
$format = $GLOBALS['TYPO3_CONF_VARS']['SYS']['ddmmyy'] . ' ' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['hhmm'];
|
||||
$timestamp = $date->format($format);
|
||||
}
|
||||
return $timestamp;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unzip an language zip file
|
||||
*
|
||||
* @param string $file path to zip file
|
||||
* @param string $path path to extract to
|
||||
*/
|
||||
protected function unzipTranslationFile(string $file, string $path)
|
||||
{
|
||||
if (!is_dir($path)) {
|
||||
GeneralUtility::mkdir_deep($path);
|
||||
}
|
||||
|
||||
$zipService = GeneralUtility::makeInstance(ZipService::class);
|
||||
if ($zipService->verify($file)) {
|
||||
$zipService->extract($file, $path);
|
||||
}
|
||||
GeneralUtility::fixPermissions($path, true);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Install\Service;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Category\CategoryRegistry;
|
||||
use TYPO3\CMS\Core\Package\PackageManager;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Service for loading the TCA
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
class LoadTcaService
|
||||
{
|
||||
/**
|
||||
* Load TCA
|
||||
* Mostly a copy of ExtensionManagementUtility to include TCA without migrations.
|
||||
* To be used in install tool only.
|
||||
*
|
||||
* This will set up $GLOBALS['TCA']
|
||||
*
|
||||
* @see \TYPO3\CMS\Core\Utility\ExtensionManagementUtility::buildBaseTcaFromSingleFiles
|
||||
*/
|
||||
public function loadExtensionTablesWithoutMigration()
|
||||
{
|
||||
$GLOBALS['TCA'] = [];
|
||||
|
||||
$activePackages = GeneralUtility::makeInstance(PackageManager::class)->getActivePackages();
|
||||
|
||||
// First load "full table" files from Configuration/TCA
|
||||
foreach ($activePackages as $package) {
|
||||
$tcaConfigurationDirectory = $package->getPackagePath() . 'Configuration/TCA';
|
||||
if (is_dir($tcaConfigurationDirectory)) {
|
||||
$files = scandir($tcaConfigurationDirectory);
|
||||
foreach ($files as $file) {
|
||||
if (is_file($tcaConfigurationDirectory . '/' . $file)
|
||||
&& ($file !== '.')
|
||||
&& ($file !== '..')
|
||||
&& (substr($file, -4, 4) === '.php')
|
||||
) {
|
||||
$tcaOfTable = require $tcaConfigurationDirectory . '/' . $file;
|
||||
if (is_array($tcaOfTable)) {
|
||||
// TCA table name is filename without .php suffix, eg 'sys_notes', not 'sys_notes.php'
|
||||
$tcaTableName = substr($file, 0, -4);
|
||||
$GLOBALS['TCA'][$tcaTableName] = $tcaOfTable;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Apply category stuff
|
||||
CategoryRegistry::getInstance()->applyTcaForPreRegisteredTables();
|
||||
|
||||
// Execute override files from Configuration/TCA/Overrides
|
||||
foreach ($activePackages as $package) {
|
||||
$tcaOverridesPathForPackage = $package->getPackagePath() . 'Configuration/TCA/Overrides';
|
||||
if (is_dir($tcaOverridesPathForPackage)) {
|
||||
$files = scandir($tcaOverridesPathForPackage);
|
||||
foreach ($files as $file) {
|
||||
if (is_file($tcaOverridesPathForPackage . '/' . $file)
|
||||
&& ($file !== '.')
|
||||
&& ($file !== '..')
|
||||
&& (substr($file, -4, 4) === '.php')
|
||||
) {
|
||||
require $tcaOverridesPathForPackage . '/' . $file;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load ext_tables.php of a single extension
|
||||
*
|
||||
* @param string $extensionKey The extension to load a ext_tables.php file from.
|
||||
*/
|
||||
public function loadSingleExtTablesFile(string $extensionKey)
|
||||
{
|
||||
global $T3_SERVICES, $T3_VAR, $TYPO3_CONF_VARS;
|
||||
global $TBE_MODULES, $TBE_MODULES_EXT, $TCA;
|
||||
global $PAGES_TYPES, $TBE_STYLES;
|
||||
global $_EXTKEY;
|
||||
|
||||
$packageManager = GeneralUtility::makeInstance(PackageManager::class);
|
||||
try {
|
||||
$package = $packageManager->getPackage($extensionKey);
|
||||
} catch (\TYPO3\CMS\Core\Package\Exception\UnknownPackageException $e) {
|
||||
throw new \RuntimeException(
|
||||
'Extension ' . $extensionKey . ' is not active',
|
||||
1477217619
|
||||
);
|
||||
}
|
||||
|
||||
$extTablesPath = $package->getPackagePath() . 'ext_tables.php';
|
||||
// Load ext_tables.php file of the extension
|
||||
if (@file_exists($extTablesPath)) {
|
||||
// $_EXTKEY and $_EXTCONF are available in ext_tables.php
|
||||
// and are explicitly set in cached file as well
|
||||
$_EXTKEY = $extensionKey;
|
||||
$_EXTCONF = $GLOBALS['TYPO3_CONF_VARS']['EXT']['extConf'][$_EXTKEY] ?? null;
|
||||
require $extTablesPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,235 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
namespace TYPO3\CMS\Install\Service;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use TYPO3\CMS\Core\Configuration\ConfigurationManager;
|
||||
use TYPO3\CMS\Core\Configuration\Loader\YamlFileLoader;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Service handling bulk read and write of LocalConfiguration values.
|
||||
*
|
||||
* Used by "Configure global settings" / "All configuration" view.
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
class LocalConfigurationValueService
|
||||
{
|
||||
|
||||
/**
|
||||
* Get up configuration data. Prepares main TYPO3_CONF_VARS
|
||||
* array to be displayed and merges is with the description file
|
||||
*
|
||||
* @return array Configuration data
|
||||
*/
|
||||
public function getCurrentConfigurationData(): array
|
||||
{
|
||||
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
|
||||
$localConfiguration = $configurationManager->getMergedLocalConfiguration();
|
||||
|
||||
$data = [];
|
||||
$commentArray = $this->getDefaultConfigArrayComments();
|
||||
|
||||
foreach ($localConfiguration as $sectionName => $section) {
|
||||
if (isset($commentArray[$sectionName])) {
|
||||
$data[$sectionName]['description'] = $commentArray[$sectionName]['description'] ?? $sectionName;
|
||||
$data[$sectionName]['items'] = $this->recursiveConfigurationFetching(
|
||||
$section,
|
||||
$GLOBALS['TYPO3_CONF_VARS'][$sectionName] ?? null,
|
||||
$commentArray[$sectionName]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ksort($data);
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Because configuration entries can be at any sub-array level, we need
|
||||
* to check entries recursively.
|
||||
*
|
||||
* @param array $sections
|
||||
* @param array|null $sectionsFromCurrentConfiguration
|
||||
* @param array $descriptions
|
||||
* @param array $path
|
||||
* @return array
|
||||
*/
|
||||
protected function recursiveConfigurationFetching(array $sections, array $sectionsFromCurrentConfiguration, array $descriptions, array $path = []): array
|
||||
{
|
||||
$data = [];
|
||||
|
||||
foreach ($sections as $key => $value) {
|
||||
if (!isset($descriptions['items'][$key])) {
|
||||
// @todo should we do something here?
|
||||
continue;
|
||||
}
|
||||
|
||||
$descriptionInfo = $descriptions['items'][$key];
|
||||
$descriptionType = $descriptionInfo['type'];
|
||||
|
||||
$newPath = $path;
|
||||
$newPath[] = $key;
|
||||
|
||||
if ($descriptionType === 'container') {
|
||||
$valueFromCurrentConfiguration = $sectionsFromCurrentConfiguration[$key] ?? null;
|
||||
$data = array_merge($data, $this->recursiveConfigurationFetching($value, $valueFromCurrentConfiguration, $descriptionInfo, $newPath));
|
||||
} elseif (!preg_match('/[' . LF . CR . ']/', (string)$value) || $descriptionType === 'multiline') {
|
||||
$itemData = [];
|
||||
$itemData['key'] = implode('/', $newPath);
|
||||
$itemData['path'] = '[' . implode('][', $newPath) . ']';
|
||||
$itemData['fieldType'] = $descriptionInfo['type'];
|
||||
$itemData['description'] = $descriptionInfo['description'];
|
||||
$itemData['allowedValues'] = $descriptionInfo['allowedValues'];
|
||||
$itemData['differentValueInCurrentConfiguration'] = (!isset($descriptionInfo['compareValuesWithCurrentConfiguration']) ||
|
||||
$descriptionInfo['compareValuesWithCurrentConfiguration']) &&
|
||||
isset($sectionsFromCurrentConfiguration[$key]) &&
|
||||
$value !== $sectionsFromCurrentConfiguration[$key];
|
||||
switch ($descriptionType) {
|
||||
case 'multiline':
|
||||
$itemData['type'] = 'textarea';
|
||||
$itemData['value'] = str_replace(['\' . LF . \'', '\' . LF . \''], [LF, LF], $value);
|
||||
break;
|
||||
case 'bool':
|
||||
$itemData['type'] = 'checkbox';
|
||||
$itemData['value'] = $value ? '1' : '0';
|
||||
$itemData['checked'] = (bool)$value;
|
||||
break;
|
||||
case 'int':
|
||||
$itemData['type'] = 'number';
|
||||
$itemData['value'] = (int)$value;
|
||||
break;
|
||||
case 'array':
|
||||
$itemData['type'] = 'input';
|
||||
// @todo The line below should be improved when the array handling is introduced in the global settings manager.
|
||||
$itemData['value'] = is_array($value)
|
||||
? implode(',', $value)
|
||||
: (string)$value;
|
||||
break;
|
||||
// Check if the setting is a PHP error code, will trigger a view helper in fluid
|
||||
case 'errors':
|
||||
$itemData['type'] = 'input';
|
||||
$itemData['value'] = $value;
|
||||
$itemData['phpErrorCode'] = true;
|
||||
break;
|
||||
case 'password':
|
||||
$itemData['type'] = 'password';
|
||||
$itemData['value'] = $value;
|
||||
$itemData['hideValue'] = true;
|
||||
break;
|
||||
default:
|
||||
$itemData['type'] = 'input';
|
||||
$itemData['value'] = $value;
|
||||
}
|
||||
|
||||
$data[] = $itemData;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Store changed values in LocalConfiguration
|
||||
*
|
||||
* @param array $valueList Nested array with key['key'] value
|
||||
* @return FlashMessageQueue
|
||||
*/
|
||||
public function updateLocalConfigurationValues(array $valueList): FlashMessageQueue
|
||||
{
|
||||
$messageQueue = new FlashMessageQueue('install');
|
||||
$configurationPathValuePairs = [];
|
||||
$commentArray = $this->getDefaultConfigArrayComments();
|
||||
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
|
||||
foreach ($valueList as $path => $value) {
|
||||
$oldValue = $configurationManager->getConfigurationValueByPath($path);
|
||||
$pathParts = explode('/', $path);
|
||||
$descriptionData = $commentArray[$pathParts[0]];
|
||||
|
||||
while ($part = next($pathParts)) {
|
||||
$descriptionData = $descriptionData['items'][$part];
|
||||
}
|
||||
|
||||
$dataType = $descriptionData['type'];
|
||||
|
||||
if ($dataType === 'multiline') {
|
||||
// Force Unix line breaks in text areas
|
||||
// Preserve line breaks
|
||||
$value = str_replace([CR, LF], ['', '\' . LF . \''], $value);
|
||||
}
|
||||
|
||||
if ($dataType === 'bool') {
|
||||
// When submitting settings in the Install Tool, values that default to "FALSE" or "TRUE"
|
||||
// in EXT:core/Configuration/DefaultConfiguration.php will be sent as "0" resp. "1".
|
||||
$value = $value === '1';
|
||||
$valueHasChanged = (bool)$oldValue !== $value;
|
||||
} elseif ($dataType === 'int') {
|
||||
// Cast integer values to integers (but only for values that can not contain a string as well)
|
||||
$value = (int)$value;
|
||||
$valueHasChanged = (int)$oldValue !== $value;
|
||||
} elseif ($dataType === 'array') {
|
||||
$oldValueAsString = is_array($oldValue)
|
||||
? implode(',', $oldValue)
|
||||
: (string)$oldValue;
|
||||
$valueHasChanged = $oldValueAsString !== $value;
|
||||
$value = GeneralUtility::trimExplode(',', $value, true);
|
||||
} else {
|
||||
$valueHasChanged = (string)$oldValue !== (string)$value;
|
||||
}
|
||||
|
||||
// Save if value changed
|
||||
if ($valueHasChanged) {
|
||||
$configurationPathValuePairs[$path] = $value;
|
||||
|
||||
if (is_bool($value)) {
|
||||
$messageBody = 'New value = ' . ($value ? 'true' : 'false');
|
||||
} elseif (empty($value)) {
|
||||
$messageBody = 'New value = none';
|
||||
} elseif (is_array($value)) {
|
||||
$messageBody = "New value = ['" . implode("', '", $value) . "']";
|
||||
} elseif ($dataType === 'password') {
|
||||
$messageBody = 'New value is set';
|
||||
} else {
|
||||
$messageBody = 'New value = ' . $value;
|
||||
}
|
||||
|
||||
$messageQueue->enqueue(new FlashMessage(
|
||||
$messageBody,
|
||||
$path
|
||||
));
|
||||
}
|
||||
}
|
||||
if (!empty($messageQueue)) {
|
||||
$configurationManager->setLocalConfigurationValuesByPathValuePairs($configurationPathValuePairs);
|
||||
}
|
||||
return $messageQueue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read descriptions from description file
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function getDefaultConfigArrayComments(): array
|
||||
{
|
||||
$configurationManager = GeneralUtility::makeInstance(ConfigurationManager::class);
|
||||
$fileName = $configurationManager->getDefaultConfigurationDescriptionFileLocation();
|
||||
$fileLoader = GeneralUtility::makeInstance(YamlFileLoader::class);
|
||||
return $fileLoader->load($fileName);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,564 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Install\Service;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use Symfony\Component\HttpFoundation\Cookie;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Http\CookieHeaderTrait;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Security\BlockSerializationTrait;
|
||||
use TYPO3\CMS\Core\SingletonInterface;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Secure session handling for the install tool.
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
class SessionService implements SingletonInterface
|
||||
{
|
||||
use BlockSerializationTrait;
|
||||
use CookieHeaderTrait;
|
||||
|
||||
/**
|
||||
* The path to our var/ folder (where we can write our sessions). Set in the
|
||||
* constructor.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $basePath;
|
||||
|
||||
/**
|
||||
* Path where to store our session files in var/session/. %s will be
|
||||
* non-guessable.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $sessionPath = 'session/%s';
|
||||
|
||||
/**
|
||||
* the cookie to store the session ID of the install tool
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $cookieName = 'Typo3InstallTool';
|
||||
|
||||
/**
|
||||
* time (minutes) to expire an unused session
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $expireTimeInMinutes = 15;
|
||||
|
||||
/**
|
||||
* time (minutes) to generate a new session id for our current session
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $regenerateSessionIdTime = 5;
|
||||
|
||||
/**
|
||||
* Constructor. Starts PHP session handling in our own private store
|
||||
*
|
||||
* Side-effect: might set a cookie, so must be called before any other output.
|
||||
*/
|
||||
public function __construct()
|
||||
{
|
||||
$this->basePath = Environment::getVarPath() . '/';
|
||||
// Start our PHP session early so that hasSession() works
|
||||
$sessionSavePath = $this->getSessionSavePath();
|
||||
// Register our "save" session handler
|
||||
session_set_save_handler([$this, 'open'], [$this, 'close'], [$this, 'read'], [$this, 'write'], [$this, 'destroy'], [$this, 'gc']);
|
||||
session_save_path($sessionSavePath);
|
||||
session_name($this->cookieName);
|
||||
ini_set('session.cookie_httponly', true);
|
||||
if ($this->hasSameSiteCookieSupport()) {
|
||||
ini_set('session.cookie_samesite', Cookie::SAMESITE_STRICT);
|
||||
}
|
||||
ini_set('session.cookie_path', (string)GeneralUtility::getIndpEnv('TYPO3_SITE_PATH'));
|
||||
// Always call the garbage collector to clean up stale session files
|
||||
ini_set('session.gc_probability', (string)100);
|
||||
ini_set('session.gc_divisor', (string)100);
|
||||
ini_set('session.gc_maxlifetime', (string)$this->expireTimeInMinutes * 2 * 60);
|
||||
if ($this->isSessionAutoStartEnabled()) {
|
||||
$sessionCreationError = 'Error: session.auto-start is enabled.<br />';
|
||||
$sessionCreationError .= 'The PHP option session.auto-start is enabled. Disable this option in php.ini or .htaccess:<br />';
|
||||
$sessionCreationError .= '<pre>php_value session.auto_start Off</pre>';
|
||||
throw new \TYPO3\CMS\Install\Exception($sessionCreationError, 1294587485);
|
||||
}
|
||||
if (session_status() === PHP_SESSION_ACTIVE) {
|
||||
$sessionCreationError = 'Session already started by session_start().<br />';
|
||||
$sessionCreationError .= 'Make sure no installed extension is starting a session in its ext_localconf.php or ext_tables.php.';
|
||||
throw new \TYPO3\CMS\Install\Exception($sessionCreationError, 1294587486);
|
||||
}
|
||||
session_start();
|
||||
if (!$this->hasSameSiteCookieSupport()) {
|
||||
$this->resendCookieHeader([$this->cookieName]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the path where to store our session files
|
||||
*
|
||||
* @throws \TYPO3\CMS\Install\Exception
|
||||
* @return string Session save path
|
||||
*/
|
||||
private function getSessionSavePath()
|
||||
{
|
||||
if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
|
||||
throw new \TYPO3\CMS\Install\Exception(
|
||||
'No encryption key set to secure session',
|
||||
1371243449
|
||||
);
|
||||
}
|
||||
$sessionSavePath = sprintf(
|
||||
$this->basePath . $this->sessionPath,
|
||||
GeneralUtility::hmac('session:' . $GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])
|
||||
);
|
||||
$this->ensureSessionSavePathExists($sessionSavePath);
|
||||
return $sessionSavePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create directories for the session save path
|
||||
* and throw an exception if that fails.
|
||||
*
|
||||
* @param string $sessionSavePath The absolute path to the session files
|
||||
* @throws \TYPO3\CMS\Install\Exception
|
||||
*/
|
||||
private function ensureSessionSavePathExists($sessionSavePath)
|
||||
{
|
||||
if (!is_dir($sessionSavePath)) {
|
||||
try {
|
||||
GeneralUtility::mkdir_deep($sessionSavePath);
|
||||
} catch (\RuntimeException $exception) {
|
||||
throw new \TYPO3\CMS\Install\Exception(
|
||||
'Could not create session folder in ' . Environment::getVarPath() . '. Make sure it is writeable!',
|
||||
1294587484
|
||||
);
|
||||
}
|
||||
$htaccessContent = '
|
||||
# Apache < 2.3
|
||||
<IfModule !mod_authz_core.c>
|
||||
Order allow,deny
|
||||
Deny from all
|
||||
Satisfy All
|
||||
</IfModule>
|
||||
|
||||
# Apache ≥ 2.3
|
||||
<IfModule mod_authz_core.c>
|
||||
Require all denied
|
||||
</IfModule>
|
||||
';
|
||||
GeneralUtility::writeFile($sessionSavePath . '/.htaccess', $htaccessContent);
|
||||
$indexContent = '<!DOCTYPE html>';
|
||||
$indexContent .= '<html><head><title></title><meta http-equiv=Refresh Content="0; Url=../../"/>';
|
||||
$indexContent .= '</head></html>';
|
||||
GeneralUtility::writeFile($sessionSavePath . '/index.html', $indexContent);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts a new session
|
||||
*
|
||||
* @return string The session ID
|
||||
*/
|
||||
public function startSession()
|
||||
{
|
||||
$_SESSION['active'] = true;
|
||||
// Be sure to use our own session id, so create a new one
|
||||
return $this->renewSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys a session
|
||||
*/
|
||||
public function destroySession()
|
||||
{
|
||||
session_destroy();
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset session. Sets _SESSION to empty array.
|
||||
*/
|
||||
public function resetSession()
|
||||
{
|
||||
$_SESSION = [];
|
||||
$_SESSION['active'] = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a new session ID and sends it to the client.
|
||||
*
|
||||
* @return string the new session ID
|
||||
*/
|
||||
private function renewSession()
|
||||
{
|
||||
session_regenerate_id();
|
||||
if (!$this->hasSameSiteCookieSupport()) {
|
||||
$this->resendCookieHeader([$this->cookieName]);
|
||||
}
|
||||
return session_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks whether we already have an active session.
|
||||
*
|
||||
* @return bool TRUE if there is an active session, FALSE otherwise
|
||||
*/
|
||||
public function hasSession()
|
||||
{
|
||||
return $_SESSION['active'] === true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the session ID of the running session.
|
||||
*
|
||||
* @return string the session ID
|
||||
*/
|
||||
public function getSessionId()
|
||||
{
|
||||
return session_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a session hash, which can only be calculated by the server.
|
||||
* Used to store our session files without exposing the session ID.
|
||||
*
|
||||
* @param string $sessionId An alternative session ID. Defaults to our current session ID
|
||||
* @throws \TYPO3\CMS\Install\Exception
|
||||
* @return string the session hash
|
||||
*/
|
||||
private function getSessionHash($sessionId = '')
|
||||
{
|
||||
if (empty($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'])) {
|
||||
throw new \TYPO3\CMS\Install\Exception(
|
||||
'No encryption key set to secure session',
|
||||
1371243450
|
||||
);
|
||||
}
|
||||
if (!$sessionId) {
|
||||
$sessionId = $this->getSessionId();
|
||||
}
|
||||
return md5($GLOBALS['TYPO3_CONF_VARS']['SYS']['encryptionKey'] . '|' . $sessionId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this session as an "authorized" one (login successful).
|
||||
* Should only be called if:
|
||||
* a) we have a valid session running
|
||||
* b) the "password" or some other authorization mechanism really matched
|
||||
*/
|
||||
public function setAuthorized()
|
||||
{
|
||||
$_SESSION['authorized'] = true;
|
||||
$_SESSION['lastSessionId'] = time();
|
||||
$_SESSION['tstamp'] = time();
|
||||
$_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
|
||||
// Renew the session id to avoid session fixation
|
||||
$this->renewSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks this session as an "authorized by backend user" one.
|
||||
* This is called by BackendModuleController from backend context.
|
||||
*/
|
||||
public function setAuthorizedBackendSession()
|
||||
{
|
||||
$_SESSION['authorized'] = true;
|
||||
$_SESSION['lastSessionId'] = time();
|
||||
$_SESSION['tstamp'] = time();
|
||||
$_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
|
||||
$_SESSION['isBackendSession'] = true;
|
||||
// Renew the session id to avoid session fixation
|
||||
$this->renewSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have an already authorized session
|
||||
*
|
||||
* @return bool TRUE if this session has been authorized before (by a correct password)
|
||||
*/
|
||||
public function isAuthorized()
|
||||
{
|
||||
if (!$_SESSION['authorized']) {
|
||||
return false;
|
||||
}
|
||||
if ($_SESSION['expires'] < time()) {
|
||||
// This session has already expired
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if we have an authorized session from a system maintainer
|
||||
*
|
||||
* @return bool TRUE if this session has been authorized before and initialized by a backend system maintainer
|
||||
*/
|
||||
public function isAuthorizedBackendUserSession()
|
||||
{
|
||||
if (!$_SESSION['authorized'] || !$_SESSION['isBackendSession']) {
|
||||
return false;
|
||||
}
|
||||
if ($_SESSION['expires'] < time()) {
|
||||
// This session has already expired
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if our session is expired.
|
||||
* Useful only right after a FALSE "isAuthorized" to see if this is the
|
||||
* reason for not being authorized anymore.
|
||||
*
|
||||
* @return bool TRUE if an authorized session exists, but is expired
|
||||
*/
|
||||
public function isExpired()
|
||||
{
|
||||
if (!$_SESSION['authorized']) {
|
||||
// Session never existed, means it is not "expired"
|
||||
return false;
|
||||
}
|
||||
if ($_SESSION['expires'] < time()) {
|
||||
// This session was authorized before, but has expired
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Refreshes our session information, rising the expire time.
|
||||
* Also generates a new session ID every 5 minutes to minimize the risk of
|
||||
* session hijacking.
|
||||
*/
|
||||
public function refreshSession()
|
||||
{
|
||||
$_SESSION['tstamp'] = time();
|
||||
$_SESSION['expires'] = time() + $this->expireTimeInMinutes * 60;
|
||||
if (time() > $_SESSION['lastSessionId'] + $this->regenerateSessionIdTime * 60) {
|
||||
// Renew our session ID
|
||||
$_SESSION['lastSessionId'] = time();
|
||||
$this->renewSession();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a message to "Flash" message storage.
|
||||
*
|
||||
* @param FlashMessage $message A message to add
|
||||
*/
|
||||
public function addMessage(FlashMessage $message)
|
||||
{
|
||||
if (!is_array($_SESSION['messages'])) {
|
||||
$_SESSION['messages'] = [];
|
||||
}
|
||||
$_SESSION['messages'][] = $message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return stored session messages and flush.
|
||||
*
|
||||
* @return FlashMessage[] Messages
|
||||
*/
|
||||
public function getMessagesAndFlush()
|
||||
{
|
||||
$messages = [];
|
||||
if (is_array($_SESSION['messages'])) {
|
||||
$messages = $_SESSION['messages'];
|
||||
}
|
||||
$_SESSION['messages'] = [];
|
||||
return $messages;
|
||||
}
|
||||
|
||||
/*************************
|
||||
*
|
||||
* PHP session handling with "secure" session files (hashed session id)
|
||||
* see http://www.php.net/manual/en/function.session-set-save-handler.php
|
||||
*
|
||||
*************************/
|
||||
/**
|
||||
* Returns the file where to store our session data
|
||||
*
|
||||
* @param string $id
|
||||
* @return string A filename
|
||||
*/
|
||||
private function getSessionFile($id)
|
||||
{
|
||||
$sessionSavePath = $this->getSessionSavePath();
|
||||
return $sessionSavePath . '/hash_' . $this->getSessionHash($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open function. See @session_set_save_handler
|
||||
*
|
||||
* @param string $savePath
|
||||
* @param string $sessionName
|
||||
* @return bool
|
||||
*/
|
||||
public function open($savePath, $sessionName)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Close function. See @session_set_save_handler
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function close()
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read session data. See @session_set_save_handler
|
||||
*
|
||||
* @param string $id The session id
|
||||
* @return string
|
||||
*/
|
||||
public function read($id)
|
||||
{
|
||||
$sessionFile = $this->getSessionFile($id);
|
||||
$content = '';
|
||||
if (file_exists($sessionFile)) {
|
||||
if ($fd = fopen($sessionFile, 'rb')) {
|
||||
$lockres = flock($fd, LOCK_SH);
|
||||
if ($lockres) {
|
||||
$length = filesize($sessionFile);
|
||||
if ($length > 0) {
|
||||
$content = fread($fd, $length);
|
||||
}
|
||||
flock($fd, LOCK_UN);
|
||||
}
|
||||
fclose($fd);
|
||||
}
|
||||
}
|
||||
// Do a "test write" of the session file after opening it. The real session data is written in
|
||||
// __destruct() and we can not create a sane error message there anymore, so this test should fail
|
||||
// before if final session file can not be written due to permission problems.
|
||||
$this->write($id, $content);
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Write session data. See @session_set_save_handler
|
||||
*
|
||||
* @param string $id The session id
|
||||
* @param string $sessionData The data to be stored
|
||||
* @throws Exception
|
||||
* @return bool
|
||||
*/
|
||||
public function write($id, $sessionData)
|
||||
{
|
||||
$sessionFile = $this->getSessionFile($id);
|
||||
$result = false;
|
||||
$changePermissions = !@is_file($sessionFile);
|
||||
if ($fd = fopen($sessionFile, 'cb')) {
|
||||
if (flock($fd, LOCK_EX)) {
|
||||
ftruncate($fd, 0);
|
||||
$res = fwrite($fd, $sessionData);
|
||||
if ($res !== false) {
|
||||
fflush($fd);
|
||||
$result = true;
|
||||
}
|
||||
flock($fd, LOCK_UN);
|
||||
}
|
||||
fclose($fd);
|
||||
// Change the permissions only if the file has just been created
|
||||
if ($changePermissions) {
|
||||
GeneralUtility::fixPermissions($sessionFile);
|
||||
}
|
||||
}
|
||||
if (!$result) {
|
||||
throw new Exception(
|
||||
'Session file not writable. Please check permission on ' .
|
||||
Environment::getVarPath() . '/session and its subdirectories.',
|
||||
1424355157
|
||||
);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Destroys one session. See @session_set_save_handler
|
||||
*
|
||||
* @param string $id The session id
|
||||
* @return string
|
||||
*/
|
||||
public function destroy($id)
|
||||
{
|
||||
$sessionFile = $this->getSessionFile($id);
|
||||
return @unlink($sessionFile);
|
||||
}
|
||||
|
||||
/**
|
||||
* Garbage collect session info. See @session_set_save_handler
|
||||
*
|
||||
* @param int $maxLifeTime The setting of session.gc_maxlifetime
|
||||
* @return bool
|
||||
*/
|
||||
public function gc($maxLifeTime)
|
||||
{
|
||||
$sessionSavePath = $this->getSessionSavePath();
|
||||
$files = glob($sessionSavePath . '/hash_*');
|
||||
if (!is_array($files)) {
|
||||
return true;
|
||||
}
|
||||
foreach ($files as $filename) {
|
||||
if (filemtime($filename) + $this->expireTimeInMinutes * 60 < time()) {
|
||||
@unlink($filename);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes the session data at the end, to overcome a PHP APC bug.
|
||||
*
|
||||
* Writes the session data in a proper context that is not affected by the APC bug:
|
||||
* http://pecl.php.net/bugs/bug.php?id=16721.
|
||||
*
|
||||
* This behaviour was introduced in #17511, where self::write() made use of GeneralUtility
|
||||
* which due to the APC bug throws a "Fatal error: Class 'GeneralUtility' not found"
|
||||
* (and the session data is not saved). Calling session_write_close() at this point
|
||||
* seems to be the most easy solution, according to PHP author.
|
||||
*/
|
||||
public function __destruct()
|
||||
{
|
||||
session_write_close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if php session.auto_start is enabled
|
||||
*
|
||||
* @return bool TRUE if session.auto_start is enabled, FALSE if disabled
|
||||
*/
|
||||
protected function isSessionAutoStartEnabled()
|
||||
{
|
||||
return $this->getIniValueBoolean('session.auto_start');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cast an on/off php ini value to boolean
|
||||
*
|
||||
* @param string $configOption
|
||||
* @return bool TRUE if the given option is enabled, FALSE if disabled
|
||||
*/
|
||||
protected function getIniValueBoolean($configOption)
|
||||
{
|
||||
return filter_var(ini_get($configOption), FILTER_VALIDATE_BOOLEAN, [FILTER_REQUIRE_SCALAR, FILTER_NULL_ON_FAILURE]);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
namespace TYPO3\CMS\Install\Service;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use Symfony\Component\Finder\Finder;
|
||||
use Symfony\Component\Finder\SplFileInfo;
|
||||
use TYPO3\CMS\Core\Core\Environment;
|
||||
use TYPO3\CMS\Core\Resource\ProcessedFileRepository;
|
||||
use TYPO3\CMS\Core\Resource\StorageRepository;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
|
||||
/**
|
||||
* Service class to manage typo3temp/assets and FAL storage
|
||||
* processed file statistics / cleanup.
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
class Typo3tempFileService
|
||||
{
|
||||
/**
|
||||
* Returns a list of directory names in typo3temp/assets and their number of files
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getDirectoryStatistics(): array
|
||||
{
|
||||
return array_merge(
|
||||
$this->statsFromTypo3temp(),
|
||||
$this->statsFromStorages()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Directory statistics for typo3temp/assets folders with some
|
||||
* special handling for legacy processed file storage _processed_
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function statsFromTypo3temp(): array
|
||||
{
|
||||
$stats = [];
|
||||
$typo3TempAssetsPath = '/typo3temp/assets/';
|
||||
$basePath = Environment::getPublicPath() . $typo3TempAssetsPath;
|
||||
if (is_dir($basePath)) {
|
||||
$dirFinder = new Finder();
|
||||
$dirsInAssets = $dirFinder->directories()->in($basePath)->depth(0)->sortByName();
|
||||
foreach ($dirsInAssets as $dirInAssets) {
|
||||
/** @var SplFileInfo $dirInAssets */
|
||||
$fileFinder = new Finder();
|
||||
$fileCount = $fileFinder->files()->in($dirInAssets->getPathname())->count();
|
||||
$folderName = $dirInAssets->getFilename();
|
||||
$stat = [
|
||||
'directory' => $typo3TempAssetsPath . $folderName,
|
||||
'numberOfFiles' => $fileCount,
|
||||
];
|
||||
if ($folderName === '_processed_') {
|
||||
// The processed file storage for legacy files (eg. TCA type=group internal_type=file)
|
||||
// gets the storageUid set, so this one can be removed via FAL functionality
|
||||
$stat['storageUid'] = 0;
|
||||
}
|
||||
$stats[] = $stat;
|
||||
}
|
||||
}
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Directory statistics for configured FAL storages.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
protected function statsFromStorages(): array
|
||||
{
|
||||
$stats = [];
|
||||
$processedFileRepository = GeneralUtility::makeInstance(ProcessedFileRepository::class);
|
||||
$storages = GeneralUtility::makeInstance(StorageRepository::class)->findAll();
|
||||
foreach ($storages as $storage) {
|
||||
if ($storage->isOnline()) {
|
||||
$storageConfiguration = $storage->getConfiguration();
|
||||
$storageBasePath = rtrim($storageConfiguration['basePath'], '/');
|
||||
$processedPath = '/' . $storageBasePath . $storage->getProcessingFolder()->getIdentifier();
|
||||
$numberOfFiles = $processedFileRepository->countByStorage($storage);
|
||||
$stats[] = [
|
||||
'directory' => $processedPath,
|
||||
'numberOfFiles' => $numberOfFiles,
|
||||
'storageUid' => $storage->getUid()
|
||||
];
|
||||
}
|
||||
}
|
||||
return $stats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear processed files. The sys_file_processedfile table is cleared for
|
||||
* given storage uid and the physical files of local processed storages are deleted.
|
||||
*
|
||||
* @return int 0 if all went well, if >0 this number of files that could not be deleted
|
||||
*/
|
||||
public function clearProcessedFiles(int $storageUid): int
|
||||
{
|
||||
$repository = GeneralUtility::makeInstance(ProcessedFileRepository::class);
|
||||
return $repository->removeAll($storageUid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears files and folders in a typo3temp/assets/ folder (not _processed_!)
|
||||
*
|
||||
* @param string $folderName
|
||||
* @return bool TRUE if all went well
|
||||
* @throws \RuntimeException If folder path is not valid
|
||||
*/
|
||||
public function clearAssetsFolder(string $folderName)
|
||||
{
|
||||
$basePath = Environment::getPublicPath() . $folderName;
|
||||
if (empty($folderName)
|
||||
|| !GeneralUtility::isAllowedAbsPath($basePath)
|
||||
|| strpos($folderName, '/typo3temp/assets/') !== 0
|
||||
) {
|
||||
throw new \RuntimeException(
|
||||
'Path to folder ' . $folderName . ' not allowed.',
|
||||
1501781453
|
||||
);
|
||||
}
|
||||
if (!is_dir($basePath)) {
|
||||
throw new \RuntimeException(
|
||||
'Folder path ' . $basePath . ' does not exist or is no directory.',
|
||||
1501781454
|
||||
);
|
||||
}
|
||||
|
||||
// first remove directories
|
||||
foreach ((new Finder())->directories()->in($basePath)->depth(0) as $directory) {
|
||||
/** @var SplFileInfo $directory */
|
||||
GeneralUtility::rmdir($directory->getPathname(), true);
|
||||
}
|
||||
|
||||
// then remove files directly in the main dir
|
||||
foreach ((new Finder())->files()->in($basePath)->depth(0) as $file) {
|
||||
/** @var SplFileInfo $file */
|
||||
$path = $file->getPathname();
|
||||
@unlink($path);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,469 @@
|
||||
<?php
|
||||
declare(strict_types = 1);
|
||||
|
||||
namespace TYPO3\CMS\Install\Service;
|
||||
|
||||
/*
|
||||
* 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!
|
||||
*/
|
||||
|
||||
use Doctrine\DBAL\Platforms\MySqlPlatform;
|
||||
use Doctrine\DBAL\Schema\Column;
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
use Symfony\Component\Console\Output\Output;
|
||||
use Symfony\Component\Console\Output\StreamOutput;
|
||||
use TYPO3\CMS\Core\Database\ConnectionPool;
|
||||
use TYPO3\CMS\Core\Database\Schema\SchemaMigrator;
|
||||
use TYPO3\CMS\Core\Database\Schema\SqlReader;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessage;
|
||||
use TYPO3\CMS\Core\Messaging\FlashMessageQueue;
|
||||
use TYPO3\CMS\Core\Registry;
|
||||
use TYPO3\CMS\Core\Utility\GeneralUtility;
|
||||
use TYPO3\CMS\Install\Updates\ChattyInterface;
|
||||
use TYPO3\CMS\Install\Updates\ConfirmableInterface;
|
||||
use TYPO3\CMS\Install\Updates\RepeatableInterface;
|
||||
use TYPO3\CMS\Install\Updates\RowUpdater\RowUpdaterInterface;
|
||||
use TYPO3\CMS\Install\Updates\UpgradeWizardInterface;
|
||||
|
||||
/**
|
||||
* Service class helping managing upgrade wizards
|
||||
* @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API.
|
||||
*/
|
||||
class UpgradeWizardsService
|
||||
{
|
||||
private $output;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->output = new StreamOutput(fopen('php://temp', 'wb'), Output::VERBOSITY_NORMAL, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array List of wizards marked as done in registry
|
||||
*/
|
||||
public function listOfWizardsDone(): array
|
||||
{
|
||||
$wizardsDoneInRegistry = [];
|
||||
$registry = GeneralUtility::makeInstance(Registry::class);
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $className) {
|
||||
if ($registry->get('installUpdate', $className, false)) {
|
||||
$wizardInstance = GeneralUtility::makeInstance($className);
|
||||
$wizardsDoneInRegistry[] = [
|
||||
'class' => $className,
|
||||
'identifier' => $identifier,
|
||||
'title' => $wizardInstance->getTitle(),
|
||||
];
|
||||
}
|
||||
}
|
||||
return $wizardsDoneInRegistry;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array List of row updaters marked as done in registry
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function listOfRowUpdatersDone(): array
|
||||
{
|
||||
$registry = GeneralUtility::makeInstance(Registry::class);
|
||||
$rowUpdatersDoneClassNames = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
|
||||
$rowUpdatersDone = [];
|
||||
foreach ($rowUpdatersDoneClassNames as $rowUpdaterClassName) {
|
||||
// Silently skip non existing DatabaseRowsUpdateWizards
|
||||
if (!class_exists($rowUpdaterClassName)) {
|
||||
continue;
|
||||
}
|
||||
/** @var RowUpdaterInterface $rowUpdater */
|
||||
$rowUpdater = GeneralUtility::makeInstance($rowUpdaterClassName);
|
||||
if (!$rowUpdater instanceof RowUpdaterInterface) {
|
||||
throw new \RuntimeException(
|
||||
'Row updater must implement RowUpdaterInterface',
|
||||
1484152906
|
||||
);
|
||||
}
|
||||
$rowUpdatersDone[] = [
|
||||
'class' => $rowUpdaterClassName,
|
||||
'identifier' => $rowUpdaterClassName,
|
||||
'title' => $rowUpdater->getTitle(),
|
||||
];
|
||||
}
|
||||
return $rowUpdatersDone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark one wizard as undone. This can be a "casual" wizard
|
||||
* or a single "row updater".
|
||||
*
|
||||
* @param string $identifier Wizard or RowUpdater identifier
|
||||
* @return bool True if wizard has been marked as undone
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function markWizardUndone(string $identifier): bool
|
||||
{
|
||||
$this->assertIdentifierIsValid($identifier);
|
||||
|
||||
$registry = GeneralUtility::makeInstance(Registry::class);
|
||||
$aWizardHasBeenMarkedUndone = false;
|
||||
foreach ($this->listOfWizardsDone() as $wizard) {
|
||||
if ($wizard['identifier'] === $identifier) {
|
||||
$aWizardHasBeenMarkedUndone = true;
|
||||
$registry->set('installUpdate', $wizard['class'], 0);
|
||||
}
|
||||
}
|
||||
if (!$aWizardHasBeenMarkedUndone) {
|
||||
$rowUpdatersDoneList = $this->listOfRowUpdatersDone();
|
||||
$registryArray = $registry->get('installUpdateRows', 'rowUpdatersDone', []);
|
||||
foreach ($rowUpdatersDoneList as $rowUpdater) {
|
||||
if ($rowUpdater['identifier'] === $identifier) {
|
||||
$aWizardHasBeenMarkedUndone = true;
|
||||
foreach ($registryArray as $rowUpdaterMarkedAsDonePosition => $rowUpdaterMarkedAsDone) {
|
||||
if ($rowUpdaterMarkedAsDone === $rowUpdater['class']) {
|
||||
unset($registryArray[$rowUpdaterMarkedAsDonePosition]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
$registry->set('installUpdateRows', 'rowUpdatersDone', $registryArray);
|
||||
}
|
||||
}
|
||||
}
|
||||
return $aWizardHasBeenMarkedUndone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of tables, single columns and indexes to add.
|
||||
*
|
||||
* @return array Array with possible keys "tables", "columns", "indexes"
|
||||
*/
|
||||
public function getBlockingDatabaseAdds(): array
|
||||
{
|
||||
$sqlReader = GeneralUtility::makeInstance(SqlReader::class);
|
||||
$databaseDefinitions = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
|
||||
|
||||
$schemaMigrator = GeneralUtility::makeInstance(SchemaMigrator::class);
|
||||
$databaseDifferences = $schemaMigrator->getSchemaDiffs($databaseDefinitions);
|
||||
|
||||
$adds = [];
|
||||
foreach ($databaseDifferences as $schemaDiff) {
|
||||
foreach ($schemaDiff->newTables as $newTable) {
|
||||
/** @var Table $newTable */
|
||||
if (!is_array($adds['tables'])) {
|
||||
$adds['tables'] = [];
|
||||
}
|
||||
$adds['tables'][] = [
|
||||
'table' => $newTable->getName(),
|
||||
];
|
||||
}
|
||||
foreach ($schemaDiff->changedTables as $changedTable) {
|
||||
foreach ($changedTable->addedColumns as $addedColumn) {
|
||||
/** @var Column $addedColumn */
|
||||
if (!is_array($adds['columns'])) {
|
||||
$adds['columns'] = [];
|
||||
}
|
||||
$adds['columns'][] = [
|
||||
'table' => $changedTable->name,
|
||||
'field' => $addedColumn->getName(),
|
||||
];
|
||||
}
|
||||
foreach ($changedTable->addedIndexes as $addedIndex) {
|
||||
/** $var Index $addedIndex */
|
||||
if (!is_array($adds['indexes'])) {
|
||||
$adds['indexes'] = [];
|
||||
}
|
||||
$adds['indexes'][] = [
|
||||
'table' => $changedTable->name,
|
||||
'index' => $addedIndex->getName(),
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $adds;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add missing tables, indexes and fields to DB.
|
||||
*/
|
||||
public function addMissingTablesAndFields(): array
|
||||
{
|
||||
$sqlReader = GeneralUtility::makeInstance(SqlReader::class);
|
||||
$databaseDefinitions = $sqlReader->getCreateTableStatementArray($sqlReader->getTablesDefinitionString());
|
||||
$schemaMigrator = GeneralUtility::makeInstance(SchemaMigrator::class);
|
||||
return $schemaMigrator->install($databaseDefinitions, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* True if DB main charset on mysql is utf8
|
||||
*
|
||||
* @return bool True if charset is ok
|
||||
*/
|
||||
public function isDatabaseCharsetUtf8(): bool
|
||||
{
|
||||
/** @var \TYPO3\CMS\Core\Database\Connection $connection */
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
|
||||
|
||||
$isDefaultConnectionMysql = ($connection->getDatabasePlatform() instanceof MySqlPlatform);
|
||||
|
||||
if (!$isDefaultConnectionMysql) {
|
||||
// Not tested on non mysql
|
||||
$charsetOk = true;
|
||||
} else {
|
||||
$queryBuilder = $connection->createQueryBuilder();
|
||||
$charset = (string)$queryBuilder->select('DEFAULT_CHARACTER_SET_NAME')
|
||||
->from('information_schema.SCHEMATA')
|
||||
->where(
|
||||
$queryBuilder->expr()->eq(
|
||||
'SCHEMA_NAME',
|
||||
$queryBuilder->createNamedParameter($connection->getDatabase(), \PDO::PARAM_STR)
|
||||
)
|
||||
)
|
||||
->setMaxResults(1)
|
||||
->execute()
|
||||
->fetchColumn();
|
||||
// check if database charset is utf-8, also allows utf8mb4
|
||||
$charsetOk = strpos($charset, 'utf8') === 0;
|
||||
}
|
||||
return $charsetOk;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default connection MySQL database charset to utf8.
|
||||
* Should be called only *if* default database connection is actually MySQL
|
||||
*/
|
||||
public function setDatabaseCharsetUtf8()
|
||||
{
|
||||
$connection = GeneralUtility::makeInstance(ConnectionPool::class)
|
||||
->getConnectionByName(ConnectionPool::DEFAULT_CONNECTION_NAME);
|
||||
$sql = 'ALTER DATABASE ' . $connection->quoteIdentifier($connection->getDatabase()) . ' CHARACTER SET utf8';
|
||||
$connection->exec($sql);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of registered upgrade wizards not marked done.
|
||||
*
|
||||
* @return array List of upgrade wizards in correct order with detail information
|
||||
*/
|
||||
public function getUpgradeWizardsList(): array
|
||||
{
|
||||
$wizards = [];
|
||||
foreach ($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'] as $identifier => $class) {
|
||||
if ($this->isWizardDone($identifier)) {
|
||||
continue;
|
||||
}
|
||||
/** @var UpgradeWizardInterface $wizardInstance */
|
||||
$wizardInstance = GeneralUtility::makeInstance($class);
|
||||
$explanation = '';
|
||||
|
||||
// $explanation is changed by reference in Update objects!
|
||||
$shouldRenderWizard = false;
|
||||
if ($wizardInstance instanceof UpgradeWizardInterface) {
|
||||
if ($wizardInstance instanceof ChattyInterface) {
|
||||
$wizardInstance->setOutput($this->output);
|
||||
}
|
||||
$shouldRenderWizard = $wizardInstance->updateNecessary();
|
||||
$explanation = $wizardInstance->getDescription();
|
||||
}
|
||||
|
||||
$wizards[] = [
|
||||
'class' => $class,
|
||||
'identifier' => $identifier,
|
||||
'title' => $wizardInstance->getTitle(),
|
||||
'shouldRenderWizard' => $shouldRenderWizard,
|
||||
'explanation' => $explanation,
|
||||
];
|
||||
}
|
||||
return $wizards;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute the "get user input" step of a wizard
|
||||
*
|
||||
* @param string $identifier
|
||||
* @return array
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function getWizardUserInput(string $identifier): array
|
||||
{
|
||||
$this->assertIdentifierIsValid($identifier);
|
||||
|
||||
$class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
|
||||
$updateObject = GeneralUtility::makeInstance($class);
|
||||
$wizardHtml = '';
|
||||
if (method_exists($updateObject, 'getUserInput')) {
|
||||
$wizardHtml = $updateObject->getUserInput('install[values][' . htmlspecialchars($identifier) . ']');
|
||||
trigger_error(
|
||||
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use ConfirmableInterface directly.',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
} elseif ($updateObject instanceof UpgradeWizardInterface && $updateObject instanceof ConfirmableInterface) {
|
||||
$markup = [];
|
||||
$radioAttributes = [
|
||||
'type' => 'radio',
|
||||
'name' => 'install[values][' . $updateObject->getIdentifier() . '][install]',
|
||||
'value' => 0
|
||||
];
|
||||
$markup[] = '<div class="panel panel-danger">';
|
||||
$markup[] = ' <div class="panel-heading">';
|
||||
$markup[] = htmlspecialchars($updateObject->getConfirmation()->getTitle());
|
||||
$markup[] = ' </div>';
|
||||
$markup[] = ' <div class="panel-body">';
|
||||
$markup[] = ' <p>' . nl2br(htmlspecialchars($updateObject->getConfirmation()->getMessage())) . '</p>';
|
||||
$markup[] = ' <div class="btn-group" data-toggle="buttons">';
|
||||
if (!$updateObject->getConfirmation()->isRequired()) {
|
||||
$markup[] = ' <label class="btn btn-default active"><input ' . GeneralUtility::implodeAttributes($radioAttributes, true) . ' checked="checked" />' . $updateObject->getConfirmation()->getDeny() . '</label>';
|
||||
}
|
||||
$radioAttributes['value'] = 1;
|
||||
$markup[] = ' <label class="btn btn-default"><input ' . GeneralUtility::implodeAttributes($radioAttributes, true) . ' />' . $updateObject->getConfirmation()->getConfirm() . '</label>';
|
||||
$markup[] = ' </div>';
|
||||
$markup[] = ' </div>';
|
||||
$markup[] = '</div>';
|
||||
$wizardHtml = implode('', $markup);
|
||||
}
|
||||
|
||||
$result = [
|
||||
'identifier' => $identifier,
|
||||
'title' => $updateObject->getTitle(),
|
||||
'wizardHtml' => $wizardHtml,
|
||||
];
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single update wizard
|
||||
*
|
||||
* @param string $identifier
|
||||
* @return FlashMessageQueue
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function executeWizard(string $identifier): FlashMessageQueue
|
||||
{
|
||||
$this->assertIdentifierIsValid($identifier);
|
||||
|
||||
$class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
|
||||
$updateObject = GeneralUtility::makeInstance($class);
|
||||
|
||||
if ($updateObject instanceof ChattyInterface) {
|
||||
$updateObject->setOutput($this->output);
|
||||
}
|
||||
$messages = new FlashMessageQueue('install');
|
||||
// $wizardInputErrorMessage is given as reference to wizard object!
|
||||
$wizardInputErrorMessage = '';
|
||||
if (method_exists($updateObject, 'checkUserInput') &&
|
||||
!$updateObject->checkUserInput($wizardInputErrorMessage)) {
|
||||
trigger_error(
|
||||
'Deprecated since TYPO3 v9, will be removed in TYPO3 v10.0, use ConfirmableInterface.',
|
||||
E_USER_DEPRECATED
|
||||
);
|
||||
$messages->enqueue(
|
||||
new FlashMessage(
|
||||
$wizardInputErrorMessage ?: 'Something went wrong!',
|
||||
'Input parameter broken',
|
||||
FlashMessage::ERROR
|
||||
)
|
||||
);
|
||||
} else {
|
||||
if ($updateObject instanceof UpgradeWizardInterface) {
|
||||
$requestParams = GeneralUtility::_GP('install');
|
||||
if ($updateObject instanceof ConfirmableInterface) {
|
||||
// value is set in request but is empty
|
||||
$isSetButEmpty = isset($requestParams['values'][$updateObject->getIdentifier()]['install'])
|
||||
&& empty($requestParams['values'][$updateObject->getIdentifier()]['install']);
|
||||
|
||||
$checkValue = (int)$requestParams['values'][$updateObject->getIdentifier()]['install'];
|
||||
|
||||
if ($checkValue === 1) {
|
||||
// confirmation = yes, we do the update
|
||||
$performResult = $updateObject->executeUpdate();
|
||||
} elseif ($updateObject->getConfirmation()->isRequired()) {
|
||||
// confirmation = no, but is required, we do *not* the update and fail
|
||||
$performResult = false;
|
||||
} elseif ($isSetButEmpty) {
|
||||
// confirmation = no, but it is *not* required, we do *not* the update, but mark the wizard as done
|
||||
$this->output->writeln('No changes applied, marking wizard as done.');
|
||||
// confirmation was set to "no"
|
||||
$performResult = true;
|
||||
}
|
||||
} else {
|
||||
// confirmation yes or non-confirmable
|
||||
$performResult = $updateObject->executeUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
$stream = $this->output->getStream();
|
||||
rewind($stream);
|
||||
if ($performResult) {
|
||||
if ($updateObject instanceof UpgradeWizardInterface && !($updateObject instanceof RepeatableInterface)) {
|
||||
// mark wizard as done if it's not repeatable and was successful
|
||||
$this->markWizardAsDone($updateObject->getIdentifier());
|
||||
}
|
||||
$messages->enqueue(
|
||||
new FlashMessage(
|
||||
stream_get_contents($stream),
|
||||
'Update successful'
|
||||
)
|
||||
);
|
||||
} else {
|
||||
$messages->enqueue(
|
||||
new FlashMessage(
|
||||
stream_get_contents($stream),
|
||||
'Update failed!',
|
||||
FlashMessage::ERROR
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return $messages;
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks some wizard as being "seen" so that it not shown again.
|
||||
* Writes the info in LocalConfiguration.php
|
||||
*
|
||||
* @param string $identifier
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function markWizardAsDone(string $identifier): void
|
||||
{
|
||||
$this->assertIdentifierIsValid($identifier);
|
||||
|
||||
$class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
|
||||
GeneralUtility::makeInstance(Registry::class)->set('installUpdate', $class, 1);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if this wizard has been "done" before
|
||||
*
|
||||
* @param string $identifier
|
||||
* @return bool TRUE if wizard has been done before, FALSE otherwise
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
public function isWizardDone(string $identifier): bool
|
||||
{
|
||||
$this->assertIdentifierIsValid($identifier);
|
||||
|
||||
$class = $GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier];
|
||||
return (bool)GeneralUtility::makeInstance(Registry::class)->get('installUpdate', $class, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate identifier exists in upgrade wizard list
|
||||
*
|
||||
* @param string $identifier
|
||||
* @throws \RuntimeException
|
||||
*/
|
||||
protected function assertIdentifierIsValid(string $identifier): void
|
||||
{
|
||||
if ($identifier === '' || (!isset($GLOBALS['TYPO3_CONF_VARS']['SC_OPTIONS']['ext/install']['update'][$identifier]) && !is_subclass_of($identifier, RowUpdaterInterface::class))) {
|
||||
throw new \RuntimeException('No valid wizard identifier given', 1502721731);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user