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