1<?php 2declare(strict_types = 1); 3namespace TYPO3\CMS\Install\Updates; 4 5/* 6 * This file is part of the TYPO3 CMS project. 7 * 8 * It is free software; you can redistribute it and/or modify it under 9 * the terms of the GNU General Public License, either version 2 10 * of the License, or any later version. 11 * 12 * For the full copyright and license information, please read the 13 * LICENSE.txt file that was distributed with this source code. 14 * 15 * The TYPO3 project - inspiring people to share! 16 */ 17 18use TYPO3\CMS\Core\Database\ConnectionPool; 19use TYPO3\CMS\Core\Utility\GeneralUtility; 20 21/** 22 * Installs EXT:redirect if sys_domain.redirectTo is filled, and migrates the values from redirectTo 23 * to a proper sys_redirect entry. 24 * @internal This class is only meant to be used within EXT:install and is not part of the TYPO3 Core API. 25 */ 26class RedirectsExtensionUpdate extends AbstractDownloadExtensionUpdate 27{ 28 /** 29 * @var \TYPO3\CMS\Install\Updates\Confirmation 30 */ 31 protected $confirmation; 32 33 public function __construct() 34 { 35 $this->extension = new ExtensionModel( 36 'redirects', 37 'Redirects', 38 '9.2', 39 'typo3/cms-redirects', 40 'Manage redirects for your TYPO3-based website' 41 ); 42 43 $this->confirmation = new Confirmation( 44 'Are you sure?', 45 'You should install the "redirects" extension only if needed. ' . $this->extension->getDescription(), 46 true 47 ); 48 } 49 50 /** 51 * Return a confirmation message instance 52 * 53 * @return \TYPO3\CMS\Install\Updates\Confirmation 54 */ 55 public function getConfirmation(): Confirmation 56 { 57 return $this->confirmation; 58 } 59 60 /** 61 * Return the identifier for this wizard 62 * This should be the same string as used in the ext_localconf class registration 63 * 64 * @return string 65 */ 66 public function getIdentifier(): string 67 { 68 return 'redirects'; 69 } 70 71 /** 72 * Return the speaking name of this wizard 73 * 74 * @return string 75 */ 76 public function getTitle(): string 77 { 78 return 'Install system extension "redirects" if a sys_domain entry with redirectTo is necessary'; 79 } 80 81 /** 82 * Return the description for this wizard 83 * 84 * @return string 85 */ 86 public function getDescription(): string 87 { 88 return 'The extension "redirects" includes functionality to handle any kind of redirects. ' 89 . 'The functionality superseds sys_domain entries with the only purpose of redirecting to a different domain or entry. ' 90 . 'This upgrade wizard installs the redirect extension if necessary and migrates the sys_domain entries to standard redirects.'; 91 } 92 93 /** 94 * Is an update necessary? 95 * Is used to determine whether a wizard needs to be run. 96 * 97 * @return bool 98 */ 99 public function updateNecessary(): bool 100 { 101 return $this->checkIfWizardIsRequired(); 102 } 103 104 /** 105 * Performs the update: 106 * - Install EXT:redirect 107 * - Migrate DB records 108 * 109 * @return bool 110 */ 111 public function executeUpdate(): bool 112 { 113 // Install the EXT:redirects extension if not happened yet 114 $installationSuccessful = $this->installExtension($this->extension); 115 if ($installationSuccessful) { 116 // Migrate the database entries 117 $this->migrateRedirectDomainsToSysRedirect(); 118 } 119 return $installationSuccessful; 120 } 121 122 /** 123 * Check if the database field "sys_domain.redirectTo" exists and if so, if there are entries in the DB table with the field filled. 124 * 125 * @return bool 126 * @throws \InvalidArgumentException 127 */ 128 protected function checkIfWizardIsRequired(): bool 129 { 130 $connectionPool = GeneralUtility::makeInstance(ConnectionPool::class); 131 $connection = $connectionPool->getConnectionByName('Default'); 132 $columns = $connection->getSchemaManager()->listTableColumns('sys_domain'); 133 if (isset($columns['redirectto'])) { 134 // table is available, now check if there are entries in it 135 $queryBuilder = GeneralUtility::makeInstance(ConnectionPool::class) 136 ->getQueryBuilderForTable('sys_domain'); 137 $queryBuilder->getRestrictions()->removeAll(); 138 $numberOfEntries = $queryBuilder->count('*') 139 ->from('sys_domain') 140 ->where( 141 $queryBuilder->expr()->neq('redirectTo', $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)) 142 ) 143 ->execute() 144 ->fetchColumn(); 145 return (bool)$numberOfEntries; 146 } 147 148 return false; 149 } 150 151 /** 152 * Move all sys_domain records with a "redirectTo" value filled (also deleted) to "sys_redirect" record 153 */ 154 protected function migrateRedirectDomainsToSysRedirect() 155 { 156 $connDomains = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_domain'); 157 $connRedirects = GeneralUtility::makeInstance(ConnectionPool::class)->getConnectionForTable('sys_redirect'); 158 159 $queryBuilder = $connDomains->createQueryBuilder(); 160 $queryBuilder->getRestrictions()->removeAll(); 161 $domainEntries = $queryBuilder->select('*') 162 ->from('sys_domain') 163 ->where( 164 $queryBuilder->expr()->neq('redirectTo', $queryBuilder->createNamedParameter('', \PDO::PARAM_STR)) 165 ) 166 ->execute() 167 ->fetchAll(); 168 169 foreach ($domainEntries as $domainEntry) { 170 $domainName = $domainEntry['domainName']; 171 $target = $domainEntry['redirectTo']; 172 $sourceDetails = parse_url($domainName); 173 $targetDetails = parse_url($target); 174 $redirectRecord = [ 175 'deleted' => (int)$domainEntry['deleted'], 176 'disabled' => (int)$domainEntry['hidden'], 177 'createdon' => (int)$domainEntry['crdate'], 178 'createdby' => (int)$domainEntry['cruser_id'], 179 'updatedon' => (int)$domainEntry['tstamp'], 180 'source_host' => $sourceDetails['host'] . ($sourceDetails['port'] ? ':' . $sourceDetails['port'] : ''), 181 'keep_query_parameters' => (int)$domainEntry['prepend_params'], 182 'target_statuscode' => (int)$domainEntry['redirectHttpStatusCode'], 183 'target' => $target 184 ]; 185 186 if (isset($targetDetails['scheme']) && $targetDetails['scheme'] === 'https') { 187 $redirectRecord['force_https'] = 1; 188 } 189 190 if (empty($sourceDetails['path']) || $sourceDetails['path'] === '/') { 191 $redirectRecord['source_path'] = '#.*#'; 192 $redirectRecord['is_regexp'] = 1; 193 } else { 194 // Remove the / and add a "/" always before, and at the very end, if path is not empty 195 $sourceDetails['path'] = trim($sourceDetails['path'], '/'); 196 $redirectRecord['source_path'] = '/' . ($sourceDetails['path'] ? $sourceDetails['path'] . '/' : ''); 197 } 198 199 // Add the redirect record 200 $connRedirects->insert('sys_redirect', $redirectRecord); 201 202 // Remove the sys_domain record (hard) 203 $connDomains->delete('sys_domain', ['uid' => (int)$domainEntry['uid']]); 204 } 205 } 206 207 /** 208 * Returns an array of class names of Prerequisite classes 209 * This way a wizard can define dependencies like "database up-to-date" or 210 * "reference index updated" 211 * 212 * @return string[] 213 */ 214 public function getPrerequisites(): array 215 { 216 return [ 217 DatabaseUpdatedPrerequisite::class 218 ]; 219 } 220} 221