xref: /webtrees/app/Http/RequestHandlers/UpgradeWizardStep.php (revision 42c9eab810dbc56868c0b71b6aada8e30cd293a8)
14b3ef6caSGreg Roach<?php
24b3ef6caSGreg Roach
34b3ef6caSGreg Roach/**
44b3ef6caSGreg Roach * webtrees: online genealogy
54b3ef6caSGreg Roach * Copyright (C) 2021 webtrees development team
64b3ef6caSGreg Roach * This program is free software: you can redistribute it and/or modify
74b3ef6caSGreg Roach * it under the terms of the GNU General Public License as published by
84b3ef6caSGreg Roach * the Free Software Foundation, either version 3 of the License, or
94b3ef6caSGreg Roach * (at your option) any later version.
104b3ef6caSGreg Roach * This program is distributed in the hope that it will be useful,
114b3ef6caSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
124b3ef6caSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
134b3ef6caSGreg Roach * GNU General Public License for more details.
144b3ef6caSGreg Roach * You should have received a copy of the GNU General Public License
154b3ef6caSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
164b3ef6caSGreg Roach */
174b3ef6caSGreg Roach
184b3ef6caSGreg Roachdeclare(strict_types=1);
194b3ef6caSGreg Roach
204b3ef6caSGreg Roachnamespace Fisharebest\Webtrees\Http\RequestHandlers;
214b3ef6caSGreg Roach
224b3ef6caSGreg Roachuse Fig\Http\Message\StatusCodeInterface;
234b3ef6caSGreg Roachuse Fisharebest\Flysystem\Adapter\ChrootAdapter;
244b3ef6caSGreg Roachuse Fisharebest\Webtrees\Exceptions\HttpServerErrorException;
254b3ef6caSGreg Roachuse Fisharebest\Webtrees\I18N;
264b3ef6caSGreg Roachuse Fisharebest\Webtrees\Registry;
274b3ef6caSGreg Roachuse Fisharebest\Webtrees\Services\GedcomExportService;
284b3ef6caSGreg Roachuse Fisharebest\Webtrees\Services\TreeService;
294b3ef6caSGreg Roachuse Fisharebest\Webtrees\Services\UpgradeService;
304b3ef6caSGreg Roachuse Fisharebest\Webtrees\Tree;
314b3ef6caSGreg Roachuse Fisharebest\Webtrees\Webtrees;
324b3ef6caSGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
334b3ef6caSGreg Roachuse Illuminate\Support\Collection;
344b3ef6caSGreg Roachuse League\Flysystem\Filesystem;
354b3ef6caSGreg Roachuse League\Flysystem\FilesystemInterface;
364b3ef6caSGreg Roachuse Psr\Http\Message\ResponseInterface;
374b3ef6caSGreg Roachuse Psr\Http\Message\ServerRequestInterface;
384b3ef6caSGreg Roachuse Psr\Http\Server\RequestHandlerInterface;
394b3ef6caSGreg Roachuse RuntimeException;
404b3ef6caSGreg Roachuse Throwable;
414b3ef6caSGreg Roach
424b3ef6caSGreg Roachuse function assert;
434b3ef6caSGreg Roachuse function date;
444b3ef6caSGreg Roachuse function e;
454b3ef6caSGreg Roachuse function fclose;
464b3ef6caSGreg Roachuse function fopen;
474b3ef6caSGreg Roachuse function fseek;
484b3ef6caSGreg Roachuse function intdiv;
494b3ef6caSGreg Roachuse function microtime;
504b3ef6caSGreg Roachuse function response;
514b3ef6caSGreg Roachuse function route;
524b3ef6caSGreg Roachuse function version_compare;
534b3ef6caSGreg Roachuse function view;
544b3ef6caSGreg Roach
554b3ef6caSGreg Roach/**
564b3ef6caSGreg Roach * Upgrade to a new version of webtrees.
574b3ef6caSGreg Roach */
584b3ef6caSGreg Roachclass UpgradeWizardStep implements RequestHandlerInterface
594b3ef6caSGreg Roach{
604b3ef6caSGreg Roach    // We make the upgrade in a number of small steps to keep within server time limits.
614b3ef6caSGreg Roach    private const STEP_CHECK    = 'Check';
624b3ef6caSGreg Roach    private const STEP_PREPARE  = 'Prepare';
634b3ef6caSGreg Roach    private const STEP_PENDING  = 'Pending';
644b3ef6caSGreg Roach    private const STEP_EXPORT   = 'Export';
654b3ef6caSGreg Roach    private const STEP_DOWNLOAD = 'Download';
664b3ef6caSGreg Roach    private const STEP_UNZIP    = 'Unzip';
674b3ef6caSGreg Roach    private const STEP_COPY     = 'Copy';
684b3ef6caSGreg Roach
694b3ef6caSGreg Roach    // Where to store our temporary files.
704b3ef6caSGreg Roach    private const UPGRADE_FOLDER = 'data/tmp/upgrade/';
714b3ef6caSGreg Roach
724b3ef6caSGreg Roach    // Where to store the downloaded ZIP archive.
734b3ef6caSGreg Roach    private const ZIP_FILENAME = 'data/tmp/webtrees.zip';
744b3ef6caSGreg Roach
754b3ef6caSGreg Roach    // The ZIP archive stores everything inside this top-level folder.
764b3ef6caSGreg Roach    private const ZIP_FILE_PREFIX = 'webtrees';
774b3ef6caSGreg Roach
784b3ef6caSGreg Roach    // Cruft can accumulate after upgrades.
794b3ef6caSGreg Roach    private const FOLDERS_TO_CLEAN = [
804b3ef6caSGreg Roach        'app',
814b3ef6caSGreg Roach        'resources',
824b3ef6caSGreg Roach        'vendor',
834b3ef6caSGreg Roach    ];
844b3ef6caSGreg Roach
854b3ef6caSGreg Roach    /** @var GedcomExportService */
864b3ef6caSGreg Roach    private $gedcom_export_service;
874b3ef6caSGreg Roach
884b3ef6caSGreg Roach    /** @var UpgradeService */
894b3ef6caSGreg Roach    private $upgrade_service;
904b3ef6caSGreg Roach
914b3ef6caSGreg Roach    /** @var TreeService */
924b3ef6caSGreg Roach    private $tree_service;
934b3ef6caSGreg Roach
944b3ef6caSGreg Roach    /**
954b3ef6caSGreg Roach     * UpgradeController constructor.
964b3ef6caSGreg Roach     *
974b3ef6caSGreg Roach     * @param GedcomExportService $gedcom_export_service
984b3ef6caSGreg Roach     * @param TreeService         $tree_service
994b3ef6caSGreg Roach     * @param UpgradeService      $upgrade_service
1004b3ef6caSGreg Roach     */
1014b3ef6caSGreg Roach    public function __construct(
1024b3ef6caSGreg Roach        GedcomExportService $gedcom_export_service,
1034b3ef6caSGreg Roach        TreeService $tree_service,
1044b3ef6caSGreg Roach        UpgradeService $upgrade_service
1054b3ef6caSGreg Roach    ) {
1064b3ef6caSGreg Roach        $this->gedcom_export_service = $gedcom_export_service;
1074b3ef6caSGreg Roach        $this->tree_service          = $tree_service;
1084b3ef6caSGreg Roach        $this->upgrade_service       = $upgrade_service;
1094b3ef6caSGreg Roach    }
1104b3ef6caSGreg Roach
1114b3ef6caSGreg Roach    /**
1124b3ef6caSGreg Roach     * Perform one step of the wizard
1134b3ef6caSGreg Roach     *
1144b3ef6caSGreg Roach     * @param ServerRequestInterface $request
1154b3ef6caSGreg Roach     *
1164b3ef6caSGreg Roach     * @return ResponseInterface
1174b3ef6caSGreg Roach     */
1184b3ef6caSGreg Roach    public function handle(ServerRequestInterface $request): ResponseInterface
1194b3ef6caSGreg Roach    {
1204b3ef6caSGreg Roach        $root_filesystem = Registry::filesystem()->root();
1214b3ef6caSGreg Roach        $data_filesystem = Registry::filesystem()->data();
1224b3ef6caSGreg Roach
1234b3ef6caSGreg Roach        // Somewhere to unpack a .ZIP file
1244b3ef6caSGreg Roach        $temporary_filesystem = new Filesystem(new ChrootAdapter($root_filesystem, self::UPGRADE_FOLDER));
1254b3ef6caSGreg Roach
1264b3ef6caSGreg Roach        $zip_file   = Webtrees::ROOT_DIR . self::ZIP_FILENAME;
1274b3ef6caSGreg Roach        $zip_folder = Webtrees::ROOT_DIR . self::UPGRADE_FOLDER;
1284b3ef6caSGreg Roach
1294b3ef6caSGreg Roach
1304b3ef6caSGreg Roach        $step = $request->getQueryParams()['step'] ?? self::STEP_CHECK;
1314b3ef6caSGreg Roach
1324b3ef6caSGreg Roach        switch ($step) {
1334b3ef6caSGreg Roach            case self::STEP_CHECK:
1344b3ef6caSGreg Roach                return $this->wizardStepCheck();
1354b3ef6caSGreg Roach
1364b3ef6caSGreg Roach            case self::STEP_PREPARE:
1374b3ef6caSGreg Roach                return $this->wizardStepPrepare($root_filesystem);
1384b3ef6caSGreg Roach
1394b3ef6caSGreg Roach            case self::STEP_PENDING:
1404b3ef6caSGreg Roach                return $this->wizardStepPending();
1414b3ef6caSGreg Roach
1424b3ef6caSGreg Roach            case self::STEP_EXPORT:
1434b3ef6caSGreg Roach                $tree_name = $request->getQueryParams()['tree'] ?? '';
1444b3ef6caSGreg Roach                $tree      = $this->tree_service->all()[$tree_name];
1454b3ef6caSGreg Roach                assert($tree instanceof Tree);
1464b3ef6caSGreg Roach
1474b3ef6caSGreg Roach                return $this->wizardStepExport($tree, $data_filesystem);
1484b3ef6caSGreg Roach
1494b3ef6caSGreg Roach            case self::STEP_DOWNLOAD:
1504b3ef6caSGreg Roach                return $this->wizardStepDownload($root_filesystem);
1514b3ef6caSGreg Roach
1524b3ef6caSGreg Roach            case self::STEP_UNZIP:
1534b3ef6caSGreg Roach                return $this->wizardStepUnzip($zip_file, $zip_folder);
1544b3ef6caSGreg Roach
1554b3ef6caSGreg Roach            case self::STEP_COPY:
1564b3ef6caSGreg Roach                return $this->wizardStepCopyAndCleanUp($zip_file, $root_filesystem, $temporary_filesystem);
1574b3ef6caSGreg Roach
1584b3ef6caSGreg Roach            default:
1594b3ef6caSGreg Roach                return response('', StatusCodeInterface::STATUS_NO_CONTENT);
1604b3ef6caSGreg Roach        }
1614b3ef6caSGreg Roach    }
1624b3ef6caSGreg Roach
1634b3ef6caSGreg Roach    /**
1644b3ef6caSGreg Roach     * @return ResponseInterface
1654b3ef6caSGreg Roach     */
1664b3ef6caSGreg Roach    private function wizardStepCheck(): ResponseInterface
1674b3ef6caSGreg Roach    {
1684b3ef6caSGreg Roach        $latest_version = $this->upgrade_service->latestVersion();
1694b3ef6caSGreg Roach
1704b3ef6caSGreg Roach        if ($latest_version === '') {
1714b3ef6caSGreg Roach            throw new HttpServerErrorException(I18N::translate('No upgrade information is available.'));
1724b3ef6caSGreg Roach        }
1734b3ef6caSGreg Roach
1744b3ef6caSGreg Roach        if (version_compare(Webtrees::VERSION, $latest_version) >= 0) {
1754b3ef6caSGreg Roach            $message = I18N::translate('This is the latest version of webtrees. No upgrade is available.');
1764b3ef6caSGreg Roach            throw new HttpServerErrorException($message);
1774b3ef6caSGreg Roach        }
1784b3ef6caSGreg Roach
1794b3ef6caSGreg Roach        /* I18N: %s is a version number, such as 1.2.3 */
1804b3ef6caSGreg Roach        $alert = I18N::translate('Upgrade to webtrees %s.', e($latest_version));
1814b3ef6caSGreg Roach
1824b3ef6caSGreg Roach        return response(view('components/alert-success', [
1834b3ef6caSGreg Roach            'alert' => $alert,
1844b3ef6caSGreg Roach        ]));
1854b3ef6caSGreg Roach    }
1864b3ef6caSGreg Roach
1874b3ef6caSGreg Roach    /**
1884b3ef6caSGreg Roach     * Make sure the temporary folder exists.
1894b3ef6caSGreg Roach     *
1904b3ef6caSGreg Roach     * @param FilesystemInterface $root_filesystem
1914b3ef6caSGreg Roach     *
1924b3ef6caSGreg Roach     * @return ResponseInterface
1934b3ef6caSGreg Roach     */
1944b3ef6caSGreg Roach    private function wizardStepPrepare(FilesystemInterface $root_filesystem): ResponseInterface
1954b3ef6caSGreg Roach    {
1964b3ef6caSGreg Roach        $root_filesystem->deleteDir(self::UPGRADE_FOLDER);
1974b3ef6caSGreg Roach        $root_filesystem->createDir(self::UPGRADE_FOLDER);
1984b3ef6caSGreg Roach
1994b3ef6caSGreg Roach        return response(view('components/alert-success', [
2004b3ef6caSGreg Roach            'alert' => I18N::translate('The folder %s has been created.', e(self::UPGRADE_FOLDER)),
2014b3ef6caSGreg Roach        ]));
2024b3ef6caSGreg Roach    }
2034b3ef6caSGreg Roach
2044b3ef6caSGreg Roach    /**
2054b3ef6caSGreg Roach     * @return ResponseInterface
2064b3ef6caSGreg Roach     */
2074b3ef6caSGreg Roach    private function wizardStepPending(): ResponseInterface
2084b3ef6caSGreg Roach    {
2094b3ef6caSGreg Roach        $changes = DB::table('change')->where('status', '=', 'pending')->exists();
2104b3ef6caSGreg Roach
2114b3ef6caSGreg Roach        if ($changes) {
212*42c9eab8SGreg Roach            return response(view('components/alert-danger', [
213*42c9eab8SGreg Roach                'alert' => I18N::translate('You should accept or reject all pending changes before upgrading.'),
214*42c9eab8SGreg Roach            ]), StatusCodeInterface::STATUS_INTERNAL_SERVER_ERROR);
2154b3ef6caSGreg Roach        }
2164b3ef6caSGreg Roach
2174b3ef6caSGreg Roach        return response(view('components/alert-success', [
2184b3ef6caSGreg Roach            'alert' => I18N::translate('There are no pending changes.'),
2194b3ef6caSGreg Roach        ]));
2204b3ef6caSGreg Roach    }
2214b3ef6caSGreg Roach
2224b3ef6caSGreg Roach    /**
2234b3ef6caSGreg Roach     * @param Tree                $tree
2244b3ef6caSGreg Roach     * @param FilesystemInterface $data_filesystem
2254b3ef6caSGreg Roach     *
2264b3ef6caSGreg Roach     * @return ResponseInterface
2274b3ef6caSGreg Roach     */
2284b3ef6caSGreg Roach    private function wizardStepExport(Tree $tree, FilesystemInterface $data_filesystem): ResponseInterface
2294b3ef6caSGreg Roach    {
2304b3ef6caSGreg Roach        // We store the data in PHP temporary storage.
2314b3ef6caSGreg Roach        $stream = fopen('php://temp', 'wb+');
2324b3ef6caSGreg Roach
2334b3ef6caSGreg Roach        if ($stream === false) {
2344b3ef6caSGreg Roach            throw new RuntimeException('Failed to create temporary stream');
2354b3ef6caSGreg Roach        }
2364b3ef6caSGreg Roach
2374b3ef6caSGreg Roach        $filename = $tree->name() . date('-Y-m-d') . '.ged';
2384b3ef6caSGreg Roach
2394b3ef6caSGreg Roach        $this->gedcom_export_service->export($tree, $stream);
2404b3ef6caSGreg Roach
2414b3ef6caSGreg Roach        fseek($stream, 0);
2424b3ef6caSGreg Roach        $data_filesystem->putStream($tree->name() . date('-Y-m-d') . '.ged', $stream);
2434b3ef6caSGreg Roach        fclose($stream);
2444b3ef6caSGreg Roach
2454b3ef6caSGreg Roach        return response(view('components/alert-success', [
2464b3ef6caSGreg Roach            'alert' => I18N::translate('The family tree has been exported to %s.', e($filename)),
2474b3ef6caSGreg Roach        ]));
2484b3ef6caSGreg Roach    }
2494b3ef6caSGreg Roach
2504b3ef6caSGreg Roach    /**
2514b3ef6caSGreg Roach     * @param FilesystemInterface $root_filesystem
2524b3ef6caSGreg Roach     *
2534b3ef6caSGreg Roach     * @return ResponseInterface
2544b3ef6caSGreg Roach     */
2554b3ef6caSGreg Roach    private function wizardStepDownload(FilesystemInterface $root_filesystem): ResponseInterface
2564b3ef6caSGreg Roach    {
2574b3ef6caSGreg Roach        $start_time   = microtime(true);
2584b3ef6caSGreg Roach        $download_url = $this->upgrade_service->downloadUrl();
2594b3ef6caSGreg Roach
2604b3ef6caSGreg Roach        try {
2614b3ef6caSGreg Roach            $bytes = $this->upgrade_service->downloadFile($download_url, $root_filesystem, self::ZIP_FILENAME);
2624b3ef6caSGreg Roach        } catch (Throwable $exception) {
2634b3ef6caSGreg Roach            throw new HttpServerErrorException($exception->getMessage());
2644b3ef6caSGreg Roach        }
2654b3ef6caSGreg Roach
2664b3ef6caSGreg Roach        $kb       = I18N::number(intdiv($bytes + 1023, 1024));
2674b3ef6caSGreg Roach        $end_time = microtime(true);
2684b3ef6caSGreg Roach        $seconds  = I18N::number($end_time - $start_time, 2);
2694b3ef6caSGreg Roach
2704b3ef6caSGreg Roach        return response(view('components/alert-success', [
2714b3ef6caSGreg Roach            'alert' => I18N::translate('%1$s KB were downloaded in %2$s seconds.', $kb, $seconds),
2724b3ef6caSGreg Roach        ]));
2734b3ef6caSGreg Roach    }
2744b3ef6caSGreg Roach
2754b3ef6caSGreg Roach    /**
2764b3ef6caSGreg Roach     * For performance reasons, we use direct filesystem access for this step.
2774b3ef6caSGreg Roach     *
2784b3ef6caSGreg Roach     * @param string $zip_file
2794b3ef6caSGreg Roach     * @param string $zip_folder
2804b3ef6caSGreg Roach     *
2814b3ef6caSGreg Roach     * @return ResponseInterface
2824b3ef6caSGreg Roach     */
2834b3ef6caSGreg Roach    private function wizardStepUnzip(string $zip_file, string $zip_folder): ResponseInterface
2844b3ef6caSGreg Roach    {
2854b3ef6caSGreg Roach        $start_time = microtime(true);
2864b3ef6caSGreg Roach        $this->upgrade_service->extractWebtreesZip($zip_file, $zip_folder);
2874b3ef6caSGreg Roach        $count    = $this->upgrade_service->webtreesZipContents($zip_file)->count();
2884b3ef6caSGreg Roach        $end_time = microtime(true);
2894b3ef6caSGreg Roach        $seconds  = I18N::number($end_time - $start_time, 2);
2904b3ef6caSGreg Roach
2914b3ef6caSGreg Roach        /* I18N: …from the .ZIP file, %2$s is a (fractional) number of seconds */
2924b3ef6caSGreg 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);
2934b3ef6caSGreg Roach
2944b3ef6caSGreg Roach        return response(view('components/alert-success', [
2954b3ef6caSGreg Roach            'alert' => $alert,
2964b3ef6caSGreg Roach        ]));
2974b3ef6caSGreg Roach    }
2984b3ef6caSGreg Roach
2994b3ef6caSGreg Roach    /**
3004b3ef6caSGreg Roach     * @param string              $zip_file
3014b3ef6caSGreg Roach     * @param FilesystemInterface $root_filesystem
3024b3ef6caSGreg Roach     * @param FilesystemInterface $temporary_filesystem
3034b3ef6caSGreg Roach     *
3044b3ef6caSGreg Roach     * @return ResponseInterface
3054b3ef6caSGreg Roach     */
3064b3ef6caSGreg Roach    private function wizardStepCopyAndCleanUp(
3074b3ef6caSGreg Roach        string $zip_file,
3084b3ef6caSGreg Roach        FilesystemInterface $root_filesystem,
3094b3ef6caSGreg Roach        FilesystemInterface $temporary_filesystem
3104b3ef6caSGreg Roach    ): ResponseInterface {
3114b3ef6caSGreg Roach        $source_filesystem = new Filesystem(new ChrootAdapter($temporary_filesystem, self::ZIP_FILE_PREFIX));
3124b3ef6caSGreg Roach
3134b3ef6caSGreg Roach        $this->upgrade_service->startMaintenanceMode();
3144b3ef6caSGreg Roach        $this->upgrade_service->moveFiles($source_filesystem, $root_filesystem);
3154b3ef6caSGreg Roach        $this->upgrade_service->endMaintenanceMode();
3164b3ef6caSGreg Roach
3174b3ef6caSGreg Roach        // While we have time, clean up any old files.
3184b3ef6caSGreg Roach        $files_to_keep    = $this->upgrade_service->webtreesZipContents($zip_file);
3194b3ef6caSGreg Roach        $folders_to_clean = new Collection(self::FOLDERS_TO_CLEAN);
3204b3ef6caSGreg Roach
3214b3ef6caSGreg Roach        $this->upgrade_service->cleanFiles($root_filesystem, $folders_to_clean, $files_to_keep);
3224b3ef6caSGreg Roach
3234b3ef6caSGreg Roach        $url    = route(ControlPanel::class);
3244b3ef6caSGreg Roach        $alert  = I18N::translate('The upgrade is complete.');
3254b3ef6caSGreg Roach        $button = '<a href="' . e($url) . '" class="btn btn-primary">' . I18N::translate('continue') . '</a>';
3264b3ef6caSGreg Roach
3274b3ef6caSGreg Roach        return response(view('components/alert-success', [
3284b3ef6caSGreg Roach            'alert' => $alert . ' ' . $button,
3294b3ef6caSGreg Roach        ]));
3304b3ef6caSGreg Roach    }
3314b3ef6caSGreg Roach}
332