Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
62.89% covered (warning)
62.89%
61 / 97
60.00% covered (warning)
60.00%
3 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Migration320Beta
62.89% covered (warning)
62.89%
61 / 97
60.00% covered (warning)
60.00%
3 / 5
24.02
0.00% covered (danger)
0.00%
0 / 1
 getVersion
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDependencies
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 getDescription
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 up
47.46% covered (danger)
47.46%
28 / 59
0.00% covered (danger)
0.00%
0 / 1
14.11
 rebuildTableWithoutColumns
85.71% covered (success)
85.71%
30 / 35
0.00% covered (danger)
0.00%
0 / 1
4.05
1<?php
2
3/**
4 * Migration for phpMyFAQ 3.2.0-beta.
5 *
6 * This Source Code Form is subject to the terms of the Mozilla Public License,
7 * v. 2.0. If a copy of the MPL was not distributed with this file, You can
8 * obtain one at https://mozilla.org/MPL/2.0/.
9 *
10 * @package   phpMyFAQ
11 * @author    Thorsten Rinne <thorsten@phpmyfaq.de>
12 * @copyright 2023-2026 phpMyFAQ Team
13 * @license   https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
14 * @link      https://www.phpmyfaq.de
15 * @since     2026-01-25
16 */
17
18declare(strict_types=1);
19
20namespace phpMyFAQ\Setup\Migration\Versions;
21
22use phpMyFAQ\Setup\Migration\AbstractMigration;
23use phpMyFAQ\Setup\Migration\Operations\OperationRecorder;
24
25readonly class Migration320Beta extends AbstractMigration
26{
27    public function getVersion(): string
28    {
29        return '3.2.0-beta';
30    }
31
32    public function getDependencies(): array
33    {
34        return ['3.2.0-alpha'];
35    }
36
37    public function getDescription(): string
38    {
39        return 'SMTP TLS config, remove link verification, config value as TEXT column';
40    }
41
42    public function up(OperationRecorder $recorder): void
43    {
44        $recorder->addConfig('mail.remoteSMTPDisableTLSPeerVerification', false);
45        $recorder->deleteConfig('main.enableLinkVerification');
46
47        // Delete link verification columns - use portable syntax
48        if ($this->isSqlite()) {
49            // SQLite requires table rebuild for dropping columns
50            $this->rebuildTableWithoutColumns($recorder, 'faqdata');
51            $this->rebuildTableWithoutColumns($recorder, 'faqdata_revisions');
52        }
53        if (!$this->isSqlite()) {
54            // MySQL, PostgreSQL, SQL Server - use separate DROP COLUMN statements
55            $recorder->addSql(
56                sprintf('ALTER TABLE %sfaqdata DROP COLUMN links_state', $this->tablePrefix),
57                'Remove links_state column from faqdata',
58            );
59            $recorder->addSql(
60                sprintf('ALTER TABLE %sfaqdata DROP COLUMN links_check_date', $this->tablePrefix),
61                'Remove links_check_date column from faqdata',
62            );
63
64            $recorder->addSql(
65                sprintf('ALTER TABLE %sfaqdata_revisions DROP COLUMN links_state', $this->tablePrefix),
66                'Remove links_state column from faqdata_revisions',
67            );
68            $recorder->addSql(
69                sprintf('ALTER TABLE %sfaqdata_revisions DROP COLUMN links_check_date', $this->tablePrefix),
70                'Remove links_check_date column from faqdata_revisions',
71            );
72        }
73
74        // Configuration values in a TEXT column
75        if ($this->isMySql()) {
76            $recorder->addSql(
77                sprintf('ALTER TABLE %sfaqconfig MODIFY config_value TEXT DEFAULT NULL', $this->tablePrefix),
78                'Change faqconfig.config_value to TEXT (MySQL)',
79            );
80            return;
81        }
82
83        if ($this->isPostgreSql()) {
84            $recorder->addSql(
85                sprintf('ALTER TABLE %sfaqconfig ALTER COLUMN config_value TYPE TEXT', $this->tablePrefix),
86                'Change faqconfig.config_value to TEXT (PostgreSQL)',
87            );
88            return;
89        }
90
91        if ($this->isSqlite()) {
92            // SQLite requires table rebuild
93            $recorder->addSql(sprintf('CREATE TABLE %sfaqconfig_new (
94                    config_name VARCHAR(255) NOT NULL default \'\',
95                    config_value TEXT DEFAULT NULL, PRIMARY KEY (config_name)
96                 )', $this->tablePrefix), 'Create new faqconfig table (SQLite)');
97
98            $recorder->addSql(
99                sprintf(
100                    'INSERT INTO %sfaqconfig_new SELECT config_name, config_value FROM %sfaqconfig',
101                    $this->tablePrefix,
102                    $this->tablePrefix,
103                ),
104                'Copy data to new faqconfig table (SQLite)',
105            );
106
107            $recorder->addSql(
108                sprintf('DROP TABLE %sfaqconfig', $this->tablePrefix),
109                'Drop old faqconfig table (SQLite)',
110            );
111
112            $recorder->addSql(
113                sprintf('ALTER TABLE %sfaqconfig_new RENAME TO %sfaqconfig', $this->tablePrefix, $this->tablePrefix),
114                'Rename new faqconfig table (SQLite)',
115            );
116            return;
117        }
118
119        if ($this->isSqlServer()) {
120            $recorder->addSql(
121                sprintf('ALTER TABLE %sfaqconfig ALTER COLUMN config_value NVARCHAR(MAX)', $this->tablePrefix),
122                'Change faqconfig.config_value to NVARCHAR(MAX) (SQL Server)',
123            );
124        }
125    }
126
127    /**
128     * Rebuilds a table without specified columns (for SQLite).
129     *
130     * Note: This method uses hardcoded schema definitions for faqdata and faqdata_revisions
131     * tables due to SQLite limitations with ALTER TABLE DROP COLUMN.
132     */
133    private function rebuildTableWithoutColumns(OperationRecorder $recorder, string $tableName): void
134    {
135        $allowedTables = ['faqdata', 'faqdata_revisions'];
136        if (!in_array($tableName, $allowedTables, strict: true)) {
137            throw new \LogicException(sprintf(
138                'rebuildTableWithoutColumns() only supports [%s], got "%s"',
139                implode(', ', $allowedTables),
140                $tableName,
141            ));
142        }
143
144        $fullTableName = $this->tablePrefix . $tableName;
145
146        // For faqdata and faqdata_revisions, we need to define the schema without the removed columns
147        if ($tableName === 'faqdata') {
148            $recorder->addSql(
149                sprintf('CREATE TABLE %s_new (
150                        id INTEGER NOT NULL,
151                        lang VARCHAR(5) NOT NULL,
152                        solution_id INTEGER NOT NULL,
153                        revision_id INTEGER NOT NULL DEFAULT 0,
154                        active char(3) NOT NULL,
155                        sticky INTEGER NOT NULL,
156                        keywords text DEFAULT NULL,
157                        thema text NOT NULL,
158                        content text DEFAULT NULL,
159                        author VARCHAR(255) NOT NULL,
160                        email VARCHAR(255) NOT NULL,
161                        comment char(1) default \'y\',
162                        updated VARCHAR(15) NOT NULL,
163                        date_start VARCHAR(14) NOT NULL DEFAULT \'00000000000000\',
164                        date_end VARCHAR(14) NOT NULL DEFAULT \'99991231235959\',
165                        created DATETIME DEFAULT CURRENT_TIMESTAMP,
166                        notes text DEFAULT NULL,
167                        sticky_order INTEGER DEFAULT NULL,
168                        PRIMARY KEY (id, lang)
169                    )', $fullTableName),
170                sprintf('Create new %s table without link verification columns (SQLite)', $tableName),
171            );
172
173            $recorder->addSql(
174                sprintf('INSERT INTO %s_new
175                     SELECT id, lang, solution_id, revision_id, active, sticky, keywords, thema, content,
176                            author, email, comment, updated, date_start, date_end, created, notes, sticky_order
177                     FROM %s', $fullTableName, $fullTableName),
178                sprintf('Copy data to new %s table (SQLite)', $tableName),
179            );
180        }
181
182        if ($tableName === 'faqdata_revisions') {
183            $recorder->addSql(
184                sprintf('CREATE TABLE %s_new (
185                        id INTEGER NOT NULL,
186                        lang VARCHAR(5) NOT NULL,
187                        solution_id INTEGER NOT NULL,
188                        revision_id INTEGER NOT NULL DEFAULT 0,
189                        active char(3) NOT NULL,
190                        sticky INTEGER NOT NULL,
191                        keywords text DEFAULT NULL,
192                        thema text NOT NULL,
193                        content text DEFAULT NULL,
194                        author VARCHAR(255) NOT NULL,
195                        email VARCHAR(255) NOT NULL,
196                        comment char(1) default \'y\',
197                        updated VARCHAR(15) NOT NULL,
198                        date_start VARCHAR(14) NOT NULL DEFAULT \'00000000000000\',
199                        date_end VARCHAR(14) NOT NULL DEFAULT \'99991231235959\',
200                        created DATETIME DEFAULT CURRENT_TIMESTAMP,
201                        notes text DEFAULT NULL,
202                        sticky_order INTEGER DEFAULT NULL,
203                        PRIMARY KEY (id, lang, solution_id, revision_id)
204                    )', $fullTableName),
205                sprintf('Create new %s table without link verification columns (SQLite)', $tableName),
206            );
207
208            $recorder->addSql(
209                sprintf('INSERT INTO %s_new
210                     SELECT id, lang, solution_id, revision_id, active, sticky, keywords, thema, content,
211                            author, email, comment, updated, date_start, date_end, created, notes, sticky_order
212                     FROM %s', $fullTableName, $fullTableName),
213                sprintf('Copy data to new %s table (SQLite)', $tableName),
214            );
215        }
216
217        $recorder->addSql(sprintf('DROP TABLE %s', $fullTableName), sprintf('Drop old %s table (SQLite)', $tableName));
218
219        $recorder->addSql(
220            sprintf('ALTER TABLE %s_new RENAME TO %s', $fullTableName, $fullTableName),
221            sprintf('Rename new %s table (SQLite)', $tableName),
222        );
223    }
224}