1*4b3ef6caSGreg Roach<?php 2*4b3ef6caSGreg Roach 3*4b3ef6caSGreg Roach/** 4*4b3ef6caSGreg Roach * webtrees: online genealogy 5*4b3ef6caSGreg Roach * Copyright (C) 2021 webtrees development team 6*4b3ef6caSGreg Roach * This program is free software: you can redistribute it and/or modify 7*4b3ef6caSGreg Roach * it under the terms of the GNU General Public License as published by 8*4b3ef6caSGreg Roach * the Free Software Foundation, either version 3 of the License, or 9*4b3ef6caSGreg Roach * (at your option) any later version. 10*4b3ef6caSGreg Roach * This program is distributed in the hope that it will be useful, 11*4b3ef6caSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of 12*4b3ef6caSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13*4b3ef6caSGreg Roach * GNU General Public License for more details. 14*4b3ef6caSGreg Roach * You should have received a copy of the GNU General Public License 15*4b3ef6caSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>. 16*4b3ef6caSGreg Roach */ 17*4b3ef6caSGreg Roach 18*4b3ef6caSGreg Roachdeclare(strict_types=1); 19*4b3ef6caSGreg Roach 20*4b3ef6caSGreg Roachnamespace Fisharebest\Webtrees\Http\RequestHandlers; 21*4b3ef6caSGreg Roach 22*4b3ef6caSGreg Roachuse Fig\Http\Message\StatusCodeInterface; 23*4b3ef6caSGreg Roachuse Fisharebest\Flysystem\Adapter\ChrootAdapter; 24*4b3ef6caSGreg Roachuse Fisharebest\Webtrees\Exceptions\HttpServerErrorException; 25*4b3ef6caSGreg Roachuse Fisharebest\Webtrees\I18N; 26*4b3ef6caSGreg Roachuse Fisharebest\Webtrees\Registry; 27*4b3ef6caSGreg Roachuse Fisharebest\Webtrees\Services\GedcomExportService; 28*4b3ef6caSGreg Roachuse Fisharebest\Webtrees\Services\TreeService; 29*4b3ef6caSGreg Roachuse Fisharebest\Webtrees\Services\UpgradeService; 30*4b3ef6caSGreg Roachuse Fisharebest\Webtrees\Tree; 31*4b3ef6caSGreg Roachuse Fisharebest\Webtrees\Webtrees; 32*4b3ef6caSGreg Roachuse Illuminate\Database\Capsule\Manager as DB; 33*4b3ef6caSGreg Roachuse Illuminate\Support\Collection; 34*4b3ef6caSGreg Roachuse League\Flysystem\Filesystem; 35*4b3ef6caSGreg Roachuse League\Flysystem\FilesystemInterface; 36*4b3ef6caSGreg Roachuse Psr\Http\Message\ResponseInterface; 37*4b3ef6caSGreg Roachuse Psr\Http\Message\ServerRequestInterface; 38*4b3ef6caSGreg Roachuse Psr\Http\Server\RequestHandlerInterface; 39*4b3ef6caSGreg Roachuse RuntimeException; 40*4b3ef6caSGreg Roachuse Throwable; 41*4b3ef6caSGreg Roach 42*4b3ef6caSGreg Roachuse function assert; 43*4b3ef6caSGreg Roachuse function date; 44*4b3ef6caSGreg Roachuse function e; 45*4b3ef6caSGreg Roachuse function fclose; 46*4b3ef6caSGreg Roachuse function fopen; 47*4b3ef6caSGreg Roachuse function fseek; 48*4b3ef6caSGreg Roachuse function intdiv; 49*4b3ef6caSGreg Roachuse function microtime; 50*4b3ef6caSGreg Roachuse function response; 51*4b3ef6caSGreg Roachuse function route; 52*4b3ef6caSGreg Roachuse function version_compare; 53*4b3ef6caSGreg Roachuse function view; 54*4b3ef6caSGreg Roach 55*4b3ef6caSGreg Roach/** 56*4b3ef6caSGreg Roach * Upgrade to a new version of webtrees. 57*4b3ef6caSGreg Roach */ 58*4b3ef6caSGreg Roachclass UpgradeWizardStep implements RequestHandlerInterface 59*4b3ef6caSGreg Roach{ 60*4b3ef6caSGreg Roach // We make the upgrade in a number of small steps to keep within server time limits. 61*4b3ef6caSGreg Roach private const STEP_CHECK = 'Check'; 62*4b3ef6caSGreg Roach private const STEP_PREPARE = 'Prepare'; 63*4b3ef6caSGreg Roach private const STEP_PENDING = 'Pending'; 64*4b3ef6caSGreg Roach private const STEP_EXPORT = 'Export'; 65*4b3ef6caSGreg Roach private const STEP_DOWNLOAD = 'Download'; 66*4b3ef6caSGreg Roach private const STEP_UNZIP = 'Unzip'; 67*4b3ef6caSGreg Roach private const STEP_COPY = 'Copy'; 68*4b3ef6caSGreg Roach 69*4b3ef6caSGreg Roach // Where to store our temporary files. 70*4b3ef6caSGreg Roach private const UPGRADE_FOLDER = 'data/tmp/upgrade/'; 71*4b3ef6caSGreg Roach 72*4b3ef6caSGreg Roach // Where to store the downloaded ZIP archive. 73*4b3ef6caSGreg Roach private const ZIP_FILENAME = 'data/tmp/webtrees.zip'; 74*4b3ef6caSGreg Roach 75*4b3ef6caSGreg Roach // The ZIP archive stores everything inside this top-level folder. 76*4b3ef6caSGreg Roach private const ZIP_FILE_PREFIX = 'webtrees'; 77*4b3ef6caSGreg Roach 78*4b3ef6caSGreg Roach // Cruft can accumulate after upgrades. 79*4b3ef6caSGreg Roach private const FOLDERS_TO_CLEAN = [ 80*4b3ef6caSGreg Roach 'app', 81*4b3ef6caSGreg Roach 'resources', 82*4b3ef6caSGreg Roach 'vendor', 83*4b3ef6caSGreg Roach ]; 84*4b3ef6caSGreg Roach 85*4b3ef6caSGreg Roach /** @var GedcomExportService */ 86*4b3ef6caSGreg Roach private $gedcom_export_service; 87*4b3ef6caSGreg Roach 88*4b3ef6caSGreg Roach /** @var UpgradeService */ 89*4b3ef6caSGreg Roach private $upgrade_service; 90*4b3ef6caSGreg Roach 91*4b3ef6caSGreg Roach /** @var TreeService */ 92*4b3ef6caSGreg Roach private $tree_service; 93*4b3ef6caSGreg Roach 94*4b3ef6caSGreg Roach /** 95*4b3ef6caSGreg Roach * UpgradeController constructor. 96*4b3ef6caSGreg Roach * 97*4b3ef6caSGreg Roach * @param GedcomExportService $gedcom_export_service 98*4b3ef6caSGreg Roach * @param TreeService $tree_service 99*4b3ef6caSGreg Roach * @param UpgradeService $upgrade_service 100*4b3ef6caSGreg Roach */ 101*4b3ef6caSGreg Roach public function __construct( 102*4b3ef6caSGreg Roach GedcomExportService $gedcom_export_service, 103*4b3ef6caSGreg Roach TreeService $tree_service, 104*4b3ef6caSGreg Roach UpgradeService $upgrade_service 105*4b3ef6caSGreg Roach ) { 106*4b3ef6caSGreg Roach $this->gedcom_export_service = $gedcom_export_service; 107*4b3ef6caSGreg Roach $this->tree_service = $tree_service; 108*4b3ef6caSGreg Roach $this->upgrade_service = $upgrade_service; 109*4b3ef6caSGreg Roach } 110*4b3ef6caSGreg Roach 111*4b3ef6caSGreg Roach /** 112*4b3ef6caSGreg Roach * Perform one step of the wizard 113*4b3ef6caSGreg Roach * 114*4b3ef6caSGreg Roach * @param ServerRequestInterface $request 115*4b3ef6caSGreg Roach * 116*4b3ef6caSGreg Roach * @return ResponseInterface 117*4b3ef6caSGreg Roach */ 118*4b3ef6caSGreg Roach public function handle(ServerRequestInterface $request): ResponseInterface 119*4b3ef6caSGreg Roach { 120*4b3ef6caSGreg Roach $root_filesystem = Registry::filesystem()->root(); 121*4b3ef6caSGreg Roach $data_filesystem = Registry::filesystem()->data(); 122*4b3ef6caSGreg Roach 123*4b3ef6caSGreg Roach // Somewhere to unpack a .ZIP file 124*4b3ef6caSGreg Roach $temporary_filesystem = new Filesystem(new ChrootAdapter($root_filesystem, self::UPGRADE_FOLDER)); 125*4b3ef6caSGreg Roach 126*4b3ef6caSGreg Roach $zip_file = Webtrees::ROOT_DIR . self::ZIP_FILENAME; 127*4b3ef6caSGreg Roach $zip_folder = Webtrees::ROOT_DIR . self::UPGRADE_FOLDER; 128*4b3ef6caSGreg Roach 129*4b3ef6caSGreg Roach 130*4b3ef6caSGreg Roach $step = $request->getQueryParams()['step'] ?? self::STEP_CHECK; 131*4b3ef6caSGreg Roach 132*4b3ef6caSGreg Roach switch ($step) { 133*4b3ef6caSGreg Roach case self::STEP_CHECK: 134*4b3ef6caSGreg Roach return $this->wizardStepCheck(); 135*4b3ef6caSGreg Roach 136*4b3ef6caSGreg Roach case self::STEP_PREPARE: 137*4b3ef6caSGreg Roach return $this->wizardStepPrepare($root_filesystem); 138*4b3ef6caSGreg Roach 139*4b3ef6caSGreg Roach case self::STEP_PENDING: 140*4b3ef6caSGreg Roach return $this->wizardStepPending(); 141*4b3ef6caSGreg Roach 142*4b3ef6caSGreg Roach case self::STEP_EXPORT: 143*4b3ef6caSGreg Roach $tree_name = $request->getQueryParams()['tree'] ?? ''; 144*4b3ef6caSGreg Roach $tree = $this->tree_service->all()[$tree_name]; 145*4b3ef6caSGreg Roach assert($tree instanceof Tree); 146*4b3ef6caSGreg Roach 147*4b3ef6caSGreg Roach return $this->wizardStepExport($tree, $data_filesystem); 148*4b3ef6caSGreg Roach 149*4b3ef6caSGreg Roach case self::STEP_DOWNLOAD: 150*4b3ef6caSGreg Roach return $this->wizardStepDownload($root_filesystem); 151*4b3ef6caSGreg Roach 152*4b3ef6caSGreg Roach case self::STEP_UNZIP: 153*4b3ef6caSGreg Roach return $this->wizardStepUnzip($zip_file, $zip_folder); 154*4b3ef6caSGreg Roach 155*4b3ef6caSGreg Roach case self::STEP_COPY: 156*4b3ef6caSGreg Roach return $this->wizardStepCopyAndCleanUp($zip_file, $root_filesystem, $temporary_filesystem); 157*4b3ef6caSGreg Roach 158*4b3ef6caSGreg Roach default: 159*4b3ef6caSGreg Roach return response('', StatusCodeInterface::STATUS_NO_CONTENT); 160*4b3ef6caSGreg Roach } 161*4b3ef6caSGreg Roach } 162*4b3ef6caSGreg Roach 163*4b3ef6caSGreg Roach /** 164*4b3ef6caSGreg Roach * @return ResponseInterface 165*4b3ef6caSGreg Roach */ 166*4b3ef6caSGreg Roach private function wizardStepCheck(): ResponseInterface 167*4b3ef6caSGreg Roach { 168*4b3ef6caSGreg Roach $latest_version = $this->upgrade_service->latestVersion(); 169*4b3ef6caSGreg Roach 170*4b3ef6caSGreg Roach if ($latest_version === '') { 171*4b3ef6caSGreg Roach throw new HttpServerErrorException(I18N::translate('No upgrade information is available.')); 172*4b3ef6caSGreg Roach } 173*4b3ef6caSGreg Roach 174*4b3ef6caSGreg Roach if (version_compare(Webtrees::VERSION, $latest_version) >= 0) { 175*4b3ef6caSGreg Roach $message = I18N::translate('This is the latest version of webtrees. No upgrade is available.'); 176*4b3ef6caSGreg Roach throw new HttpServerErrorException($message); 177*4b3ef6caSGreg Roach } 178*4b3ef6caSGreg Roach 179*4b3ef6caSGreg Roach /* I18N: %s is a version number, such as 1.2.3 */ 180*4b3ef6caSGreg Roach $alert = I18N::translate('Upgrade to webtrees %s.', e($latest_version)); 181*4b3ef6caSGreg Roach 182*4b3ef6caSGreg Roach return response(view('components/alert-success', [ 183*4b3ef6caSGreg Roach 'alert' => $alert, 184*4b3ef6caSGreg Roach ])); 185*4b3ef6caSGreg Roach } 186*4b3ef6caSGreg Roach 187*4b3ef6caSGreg Roach /** 188*4b3ef6caSGreg Roach * Make sure the temporary folder exists. 189*4b3ef6caSGreg Roach * 190*4b3ef6caSGreg Roach * @param FilesystemInterface $root_filesystem 191*4b3ef6caSGreg Roach * 192*4b3ef6caSGreg Roach * @return ResponseInterface 193*4b3ef6caSGreg Roach */ 194*4b3ef6caSGreg Roach private function wizardStepPrepare(FilesystemInterface $root_filesystem): ResponseInterface 195*4b3ef6caSGreg Roach { 196*4b3ef6caSGreg Roach $root_filesystem->deleteDir(self::UPGRADE_FOLDER); 197*4b3ef6caSGreg Roach $root_filesystem->createDir(self::UPGRADE_FOLDER); 198*4b3ef6caSGreg Roach 199*4b3ef6caSGreg Roach return response(view('components/alert-success', [ 200*4b3ef6caSGreg Roach 'alert' => I18N::translate('The folder %s has been created.', e(self::UPGRADE_FOLDER)), 201*4b3ef6caSGreg Roach ])); 202*4b3ef6caSGreg Roach } 203*4b3ef6caSGreg Roach 204*4b3ef6caSGreg Roach /** 205*4b3ef6caSGreg Roach * @return ResponseInterface 206*4b3ef6caSGreg Roach */ 207*4b3ef6caSGreg Roach private function wizardStepPending(): ResponseInterface 208*4b3ef6caSGreg Roach { 209*4b3ef6caSGreg Roach $changes = DB::table('change')->where('status', '=', 'pending')->exists(); 210*4b3ef6caSGreg Roach 211*4b3ef6caSGreg Roach if ($changes) { 212*4b3ef6caSGreg Roach throw new HttpServerErrorException(I18N::translate('You should accept or reject all pending changes before upgrading.')); 213*4b3ef6caSGreg Roach } 214*4b3ef6caSGreg Roach 215*4b3ef6caSGreg Roach return response(view('components/alert-success', [ 216*4b3ef6caSGreg Roach 'alert' => I18N::translate('There are no pending changes.'), 217*4b3ef6caSGreg Roach ])); 218*4b3ef6caSGreg Roach } 219*4b3ef6caSGreg Roach 220*4b3ef6caSGreg Roach /** 221*4b3ef6caSGreg Roach * @param Tree $tree 222*4b3ef6caSGreg Roach * @param FilesystemInterface $data_filesystem 223*4b3ef6caSGreg Roach * 224*4b3ef6caSGreg Roach * @return ResponseInterface 225*4b3ef6caSGreg Roach */ 226*4b3ef6caSGreg Roach private function wizardStepExport(Tree $tree, FilesystemInterface $data_filesystem): ResponseInterface 227*4b3ef6caSGreg Roach { 228*4b3ef6caSGreg Roach // We store the data in PHP temporary storage. 229*4b3ef6caSGreg Roach $stream = fopen('php://temp', 'wb+'); 230*4b3ef6caSGreg Roach 231*4b3ef6caSGreg Roach if ($stream === false) { 232*4b3ef6caSGreg Roach throw new RuntimeException('Failed to create temporary stream'); 233*4b3ef6caSGreg Roach } 234*4b3ef6caSGreg Roach 235*4b3ef6caSGreg Roach $filename = $tree->name() . date('-Y-m-d') . '.ged'; 236*4b3ef6caSGreg Roach 237*4b3ef6caSGreg Roach $this->gedcom_export_service->export($tree, $stream); 238*4b3ef6caSGreg Roach 239*4b3ef6caSGreg Roach fseek($stream, 0); 240*4b3ef6caSGreg Roach $data_filesystem->putStream($tree->name() . date('-Y-m-d') . '.ged', $stream); 241*4b3ef6caSGreg Roach fclose($stream); 242*4b3ef6caSGreg Roach 243*4b3ef6caSGreg Roach return response(view('components/alert-success', [ 244*4b3ef6caSGreg Roach 'alert' => I18N::translate('The family tree has been exported to %s.', e($filename)), 245*4b3ef6caSGreg Roach ])); 246*4b3ef6caSGreg Roach } 247*4b3ef6caSGreg Roach 248*4b3ef6caSGreg Roach /** 249*4b3ef6caSGreg Roach * @param FilesystemInterface $root_filesystem 250*4b3ef6caSGreg Roach * 251*4b3ef6caSGreg Roach * @return ResponseInterface 252*4b3ef6caSGreg Roach */ 253*4b3ef6caSGreg Roach private function wizardStepDownload(FilesystemInterface $root_filesystem): ResponseInterface 254*4b3ef6caSGreg Roach { 255*4b3ef6caSGreg Roach $start_time = microtime(true); 256*4b3ef6caSGreg Roach $download_url = $this->upgrade_service->downloadUrl(); 257*4b3ef6caSGreg Roach 258*4b3ef6caSGreg Roach try { 259*4b3ef6caSGreg Roach $bytes = $this->upgrade_service->downloadFile($download_url, $root_filesystem, self::ZIP_FILENAME); 260*4b3ef6caSGreg Roach } catch (Throwable $exception) { 261*4b3ef6caSGreg Roach throw new HttpServerErrorException($exception->getMessage()); 262*4b3ef6caSGreg Roach } 263*4b3ef6caSGreg Roach 264*4b3ef6caSGreg Roach $kb = I18N::number(intdiv($bytes + 1023, 1024)); 265*4b3ef6caSGreg Roach $end_time = microtime(true); 266*4b3ef6caSGreg Roach $seconds = I18N::number($end_time - $start_time, 2); 267*4b3ef6caSGreg Roach 268*4b3ef6caSGreg Roach return response(view('components/alert-success', [ 269*4b3ef6caSGreg Roach 'alert' => I18N::translate('%1$s KB were downloaded in %2$s seconds.', $kb, $seconds), 270*4b3ef6caSGreg Roach ])); 271*4b3ef6caSGreg Roach } 272*4b3ef6caSGreg Roach 273*4b3ef6caSGreg Roach /** 274*4b3ef6caSGreg Roach * For performance reasons, we use direct filesystem access for this step. 275*4b3ef6caSGreg Roach * 276*4b3ef6caSGreg Roach * @param string $zip_file 277*4b3ef6caSGreg Roach * @param string $zip_folder 278*4b3ef6caSGreg Roach * 279*4b3ef6caSGreg Roach * @return ResponseInterface 280*4b3ef6caSGreg Roach */ 281*4b3ef6caSGreg Roach private function wizardStepUnzip(string $zip_file, string $zip_folder): ResponseInterface 282*4b3ef6caSGreg Roach { 283*4b3ef6caSGreg Roach $start_time = microtime(true); 284*4b3ef6caSGreg Roach $this->upgrade_service->extractWebtreesZip($zip_file, $zip_folder); 285*4b3ef6caSGreg Roach $count = $this->upgrade_service->webtreesZipContents($zip_file)->count(); 286*4b3ef6caSGreg Roach $end_time = microtime(true); 287*4b3ef6caSGreg Roach $seconds = I18N::number($end_time - $start_time, 2); 288*4b3ef6caSGreg Roach 289*4b3ef6caSGreg Roach /* I18N: …from the .ZIP file, %2$s is a (fractional) number of seconds */ 290*4b3ef6caSGreg Roach $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); 291*4b3ef6caSGreg Roach 292*4b3ef6caSGreg Roach return response(view('components/alert-success', [ 293*4b3ef6caSGreg Roach 'alert' => $alert, 294*4b3ef6caSGreg Roach ])); 295*4b3ef6caSGreg Roach } 296*4b3ef6caSGreg Roach 297*4b3ef6caSGreg Roach /** 298*4b3ef6caSGreg Roach * @param string $zip_file 299*4b3ef6caSGreg Roach * @param FilesystemInterface $root_filesystem 300*4b3ef6caSGreg Roach * @param FilesystemInterface $temporary_filesystem 301*4b3ef6caSGreg Roach * 302*4b3ef6caSGreg Roach * @return ResponseInterface 303*4b3ef6caSGreg Roach */ 304*4b3ef6caSGreg Roach private function wizardStepCopyAndCleanUp( 305*4b3ef6caSGreg Roach string $zip_file, 306*4b3ef6caSGreg Roach FilesystemInterface $root_filesystem, 307*4b3ef6caSGreg Roach FilesystemInterface $temporary_filesystem 308*4b3ef6caSGreg Roach ): ResponseInterface { 309*4b3ef6caSGreg Roach $source_filesystem = new Filesystem(new ChrootAdapter($temporary_filesystem, self::ZIP_FILE_PREFIX)); 310*4b3ef6caSGreg Roach 311*4b3ef6caSGreg Roach $this->upgrade_service->startMaintenanceMode(); 312*4b3ef6caSGreg Roach $this->upgrade_service->moveFiles($source_filesystem, $root_filesystem); 313*4b3ef6caSGreg Roach $this->upgrade_service->endMaintenanceMode(); 314*4b3ef6caSGreg Roach 315*4b3ef6caSGreg Roach // While we have time, clean up any old files. 316*4b3ef6caSGreg Roach $files_to_keep = $this->upgrade_service->webtreesZipContents($zip_file); 317*4b3ef6caSGreg Roach $folders_to_clean = new Collection(self::FOLDERS_TO_CLEAN); 318*4b3ef6caSGreg Roach 319*4b3ef6caSGreg Roach $this->upgrade_service->cleanFiles($root_filesystem, $folders_to_clean, $files_to_keep); 320*4b3ef6caSGreg Roach 321*4b3ef6caSGreg Roach $url = route(ControlPanel::class); 322*4b3ef6caSGreg Roach $alert = I18N::translate('The upgrade is complete.'); 323*4b3ef6caSGreg Roach $button = '<a href="' . e($url) . '" class="btn btn-primary">' . I18N::translate('continue') . '</a>'; 324*4b3ef6caSGreg Roach 325*4b3ef6caSGreg Roach return response(view('components/alert-success', [ 326*4b3ef6caSGreg Roach 'alert' => $alert . ' ' . $button, 327*4b3ef6caSGreg Roach ])); 328*4b3ef6caSGreg Roach } 329*4b3ef6caSGreg Roach} 330