Issues (2559)

app/Http/RequestHandlers/UpgradeWizardStep.php (1 issue)

Labels
Severity
1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 webtrees development team
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU General Public License as published by
8
 * the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 * This program is distributed in the hope that it will be useful,
11
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
 * GNU General Public License for more details.
14
 * You should have received a copy of the GNU General Public License
15
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16
 */
17
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Http\RequestHandlers;
21
22
use Fig\Http\Message\StatusCodeInterface;
23
use Fisharebest\Webtrees\DB;
24
use Fisharebest\Webtrees\Http\Exceptions\HttpServerErrorException;
25
use Fisharebest\Webtrees\I18N;
26
use Fisharebest\Webtrees\Registry;
27
use Fisharebest\Webtrees\Services\GedcomExportService;
28
use Fisharebest\Webtrees\Services\MaintenanceModeService;
29
use Fisharebest\Webtrees\Services\PendingChangesService;
30
use Fisharebest\Webtrees\Services\TreeService;
31
use Fisharebest\Webtrees\Services\UpgradeService;
32
use Fisharebest\Webtrees\Tree;
33
use Fisharebest\Webtrees\Validator;
34
use Fisharebest\Webtrees\Webtrees;
35
use Illuminate\Support\Collection;
36
use Psr\Http\Message\ResponseInterface;
37
use Psr\Http\Message\ServerRequestInterface;
38
use Psr\Http\Server\RequestHandlerInterface;
39
use Throwable;
40
41
use function assert;
42
use function date;
43
use function e;
44
use function fclose;
45
use function intdiv;
46
use function response;
47
use function route;
48
use function version_compare;
49
use function view;
50
51
/**
52
 * Upgrade to a new version of webtrees.
53
 */
54
readonly class UpgradeWizardStep implements RequestHandlerInterface
55
{
56
    // We make the upgrade in a number of small steps to keep within server time limits.
57
    private const string STEP_CHECK   = 'Check';
0 ignored issues
show
A parse error occurred: Syntax error, unexpected T_STRING, expecting '=' on line 57 at column 25
Loading history...
58
    private const string STEP_PREPARE = 'Prepare';
59
    private const string STEP_PENDING = 'Pending';
60
    private const string STEP_EXPORT   = 'Export';
61
    private const string STEP_DOWNLOAD = 'Download';
62
    private const string STEP_UNZIP = 'Unzip';
63
    private const string STEP_COPY  = 'Copy';
64
65
    // Where to store our temporary files.
66
    private const string UPGRADE_FOLDER = 'data/tmp/upgrade/';
67
68
    // Where to store the downloaded ZIP archive.
69
    private const string ZIP_FILENAME = 'data/tmp/webtrees.zip';
70
71
    // The ZIP archive stores everything inside this top-level folder.
72
    private const string ZIP_FILE_PREFIX = 'webtrees';
73
74
    // Cruft can accumulate after upgrades.
75
    private const array FOLDERS_TO_CLEAN = [
76
        'app',
77
        'resources',
78
        'vendor',
79
    ];
80
81
    public function __construct(
82
        private GedcomExportService $gedcom_export_service,
83
        private MaintenanceModeService $maintenance_mode_service,
84
        private PendingChangesService $pending_changes_service,
85
        private TreeService $tree_service,
86
        private UpgradeService $upgrade_service,
87
    ) {
88
    }
89
90
    public function handle(ServerRequestInterface $request): ResponseInterface
91
    {
92
        $zip_file   = Webtrees::ROOT_DIR . self::ZIP_FILENAME;
93
        $zip_folder = Webtrees::ROOT_DIR . self::UPGRADE_FOLDER;
94
95
        $step = Validator::queryParams($request)->string('step', self::STEP_CHECK);
96
97
        switch ($step) {
98
            case self::STEP_CHECK:
99
                return $this->wizardStepCheck();
100
101
            case self::STEP_PREPARE:
102
                return $this->wizardStepPrepare();
103
104
            case self::STEP_PENDING:
105
                return $this->wizardStepPending();
106
107
            case self::STEP_EXPORT:
108
                $tree_name = Validator::queryParams($request)->string('tree');
109
                $tree      = $this->tree_service->all()[$tree_name];
110
                assert($tree instanceof Tree);
111
112
                return $this->wizardStepExport($tree);
113
114
            case self::STEP_DOWNLOAD:
115
                return $this->wizardStepDownload();
116
117
            case self::STEP_UNZIP:
118
                return $this->wizardStepUnzip($zip_file, $zip_folder);
119
120
            case self::STEP_COPY:
121
                return $this->wizardStepCopyAndCleanUp($zip_file);
122
123
            default:
124
                return response('', StatusCodeInterface::STATUS_NO_CONTENT);
125
        }
126
    }
127
128
    private function wizardStepCheck(): ResponseInterface
129
    {
130
        $latest_version = $this->upgrade_service->latestVersion();
131
132
        if ($latest_version === '') {
133
            throw new HttpServerErrorException(I18N::translate('No upgrade information is available.'));
134
        }
135
136
        if (version_compare(Webtrees::VERSION, $latest_version) >= 0) {
137
            $message = I18N::translate('This is the latest version of webtrees. No upgrade is available.');
138
            throw new HttpServerErrorException($message);
139
        }
140
141
        /* I18N: %s is a version number, such as 1.2.3 */
142
        $alert = I18N::translate('Upgrade to webtrees %s.', e($latest_version));
143
144
        return response(view('components/alert-success', [
145
            'alert' => $alert,
146
        ]));
147
    }
148
149
    private function wizardStepPrepare(): ResponseInterface
150
    {
151
        $root_filesystem = Registry::filesystem()->root();
152
        $root_filesystem->deleteDirectory(self::UPGRADE_FOLDER);
153
        $root_filesystem->createDirectory(self::UPGRADE_FOLDER);
154
155
        return response(view('components/alert-success', [
156
            'alert' => I18N::translate('The folder %s has been created.', e(self::UPGRADE_FOLDER)),
157
        ]));
158
    }
159
160
    private function wizardStepPending(): ResponseInterface
161
    {
162
        if ($this->pending_changes_service->pendingChangesExist()) {
163
            return response(view('components/alert-danger', [
164
                'alert' => I18N::translate('You should accept or reject all pending changes before upgrading.'),
165
            ]), StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
166
        }
167
168
        return response(view('components/alert-success', [
169
            'alert' => I18N::translate('There are no pending changes.'),
170
        ]));
171
    }
172
173
    private function wizardStepExport(Tree $tree): ResponseInterface
174
    {
175
        $data_filesystem = Registry::filesystem()->data();
176
        $filename        = $tree->name() . date('-Y-m-d') . '.ged';
177
        $stream          = $this->gedcom_export_service->export($tree);
178
        $data_filesystem->writeStream($filename, $stream);
179
        fclose($stream);
180
181
        return response(view('components/alert-success', [
182
            'alert' => I18N::translate('The family tree has been exported to %s.', e($filename)),
183
        ]));
184
    }
185
186
    private function wizardStepDownload(): ResponseInterface
187
    {
188
        $root_filesystem = Registry::filesystem()->root();
189
        $start_time      = Registry::timeFactory()->now();
190
        $download_url    = $this->upgrade_service->downloadUrl();
191
192
        try {
193
            $bytes = $this->upgrade_service->downloadFile($download_url, $root_filesystem, self::ZIP_FILENAME);
194
        } catch (Throwable $exception) {
195
            throw new HttpServerErrorException($exception->getMessage());
196
        }
197
198
        $kb       = I18N::number(intdiv($bytes + 1023, 1024));
199
        $end_time = Registry::timeFactory()->now();
200
        $seconds  = I18N::number($end_time - $start_time, 2);
201
202
        return response(view('components/alert-success', [
203
            'alert' => I18N::translate('%1$s KB were downloaded in %2$s seconds.', $kb, $seconds),
204
        ]));
205
    }
206
207
    private function wizardStepUnzip(string $zip_file, string $zip_folder): ResponseInterface
208
    {
209
        $start_time = Registry::timeFactory()->now();
210
        $this->upgrade_service->extractWebtreesZip($zip_file, $zip_folder);
211
        $count    = $this->upgrade_service->webtreesZipContents($zip_file)->count();
212
        $end_time = Registry::timeFactory()->now();
213
        $seconds  = I18N::number($end_time - $start_time, 2);
214
215
        /* I18N: …from the .ZIP file, %2$s is a (fractional) number of seconds */
216
        $alert = I18N::plural('%1$s file was extracted in %2$s seconds.', '%1$s files were extracted in %2$s seconds.', $count, I18N::number($count), $seconds);
217
218
        return response(view('components/alert-success', [
219
            'alert' => $alert,
220
        ]));
221
    }
222
223
    private function wizardStepCopyAndCleanUp(string $zip_file): ResponseInterface
224
    {
225
        $source_filesystem = Registry::filesystem()->root(self::UPGRADE_FOLDER . self::ZIP_FILE_PREFIX);
226
        $root_filesystem   = Registry::filesystem()->root();
227
228
        $this->maintenance_mode_service->offline();
229
        $this->upgrade_service->moveFiles($source_filesystem, $root_filesystem);
230
        $this->maintenance_mode_service->online();
231
232
        // While we have time, clean up any old files.
233
        $files_to_keep    = $this->upgrade_service->webtreesZipContents($zip_file);
234
        $folders_to_clean = new Collection(self::FOLDERS_TO_CLEAN);
235
236
        $this->upgrade_service->cleanFiles($root_filesystem, $folders_to_clean, $files_to_keep);
237
238
        $url    = route(ControlPanel::class);
239
        $alert  = I18N::translate('The upgrade is complete.');
240
        $button = '<a href="' . e($url) . '" class="btn btn-primary">' . I18N::translate('continue') . '</a>';
241
242
        return response(view('components/alert-success', [
243
            'alert' => $alert . ' ' . $button,
244
        ]));
245
    }
246
}
247