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