xref: /webtrees/app/Services/UpgradeService.php (revision bfd5083c43249850ef0c24d61c0b772ad5e76685)
10d11ac7eSGreg Roach<?php
23976b470SGreg Roach
30d11ac7eSGreg Roach/**
40d11ac7eSGreg Roach * webtrees: online genealogy
55bfc6897SGreg Roach * Copyright (C) 2022 webtrees development team
60d11ac7eSGreg Roach * This program is free software: you can redistribute it and/or modify
70d11ac7eSGreg Roach * it under the terms of the GNU General Public License as published by
80d11ac7eSGreg Roach * the Free Software Foundation, either version 3 of the License, or
90d11ac7eSGreg Roach * (at your option) any later version.
100d11ac7eSGreg Roach * This program is distributed in the hope that it will be useful,
110d11ac7eSGreg Roach * but WITHOUT ANY WARRANTY; without even the implied warranty of
120d11ac7eSGreg Roach * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
130d11ac7eSGreg Roach * GNU General Public License for more details.
140d11ac7eSGreg Roach * You should have received a copy of the GNU General Public License
1589f7189bSGreg Roach * along with this program. If not, see <https://www.gnu.org/licenses/>.
160d11ac7eSGreg Roach */
17fcfa147eSGreg Roach
18e7f56f2aSGreg Roachdeclare(strict_types=1);
19e7f56f2aSGreg Roach
200d11ac7eSGreg Roachnamespace Fisharebest\Webtrees\Services;
210d11ac7eSGreg Roach
226ccdf4f0SGreg Roachuse Fig\Http\Message\StatusCodeInterface;
23*bfd5083cSGreg Roachuse Fisharebest\Webtrees\Contracts\TimestampInterface;
2481b729d3SGreg Roachuse Fisharebest\Webtrees\Http\Exceptions\HttpServerErrorException;
257def76c7SGreg Roachuse Fisharebest\Webtrees\I18N;
262e464181SGreg Roachuse Fisharebest\Webtrees\Registry;
270d11ac7eSGreg Roachuse Fisharebest\Webtrees\Site;
288d0ebef0SGreg Roachuse Fisharebest\Webtrees\Webtrees;
290d11ac7eSGreg Roachuse GuzzleHttp\Client;
304022f21aSGreg Roachuse GuzzleHttp\Exception\GuzzleException;
31c4cbcd7bSGreg Roachuse Illuminate\Database\Capsule\Manager as DB;
327def76c7SGreg Roachuse Illuminate\Support\Collection;
337def76c7SGreg Roachuse League\Flysystem\Filesystem;
34f7cf8a15SGreg Roachuse League\Flysystem\FilesystemException;
35f7cf8a15SGreg Roachuse League\Flysystem\FilesystemOperator;
36782714c2SGreg Roachuse League\Flysystem\FilesystemReader;
37f7cf8a15SGreg Roachuse League\Flysystem\StorageAttributes;
38f0448b68SGreg Roachuse League\Flysystem\UnableToDeleteFile;
39f7cf8a15SGreg Roachuse League\Flysystem\ZipArchive\FilesystemZipArchiveProvider;
407def76c7SGreg Roachuse League\Flysystem\ZipArchive\ZipArchiveAdapter;
41783b32e3SGreg Roachuse RuntimeException;
427def76c7SGreg Roachuse ZipArchive;
433976b470SGreg Roach
443551ba8dSGreg Roachuse function explode;
453551ba8dSGreg Roachuse function fclose;
463551ba8dSGreg Roachuse function file_exists;
473551ba8dSGreg Roachuse function file_put_contents;
483551ba8dSGreg Roachuse function fopen;
493551ba8dSGreg Roachuse function ftell;
503551ba8dSGreg Roachuse function fwrite;
51fbb4b472SGreg Roachuse function rewind;
52783b32e3SGreg Roachuse function strlen;
53d97083feSGreg Roachuse function time;
543551ba8dSGreg Roachuse function unlink;
553551ba8dSGreg Roachuse function version_compare;
563551ba8dSGreg Roach
573551ba8dSGreg Roachuse const PHP_VERSION;
580d11ac7eSGreg Roach
590d11ac7eSGreg Roach/**
600d11ac7eSGreg Roach * Automatic upgrades.
610d11ac7eSGreg Roach */
620d11ac7eSGreg Roachclass UpgradeService
630d11ac7eSGreg Roach{
647def76c7SGreg Roach    // Options for fetching files using GuzzleHTTP
657def76c7SGreg Roach    private const GUZZLE_OPTIONS = [
667def76c7SGreg Roach        'connect_timeout' => 25,
677def76c7SGreg Roach        'read_timeout'    => 25,
687def76c7SGreg Roach        'timeout'         => 55,
697def76c7SGreg Roach    ];
707def76c7SGreg Roach
717def76c7SGreg Roach    // Transfer stream data in blocks of this number of bytes.
727def76c7SGreg Roach    private const READ_BLOCK_SIZE = 65535;
737def76c7SGreg Roach
747def76c7SGreg Roach    // Only check the webtrees server once per day.
7516d6367aSGreg Roach    private const CHECK_FOR_UPDATE_INTERVAL = 24 * 60 * 60;
760d11ac7eSGreg Roach
770d11ac7eSGreg Roach    // Fetch information about upgrades from here.
780d11ac7eSGreg Roach    // Note: earlier versions of webtrees used svn.webtrees.net, so we must maintain both URLs.
7916d6367aSGreg Roach    private const UPDATE_URL = 'https://dev.webtrees.net/build/latest-version.txt';
800d11ac7eSGreg Roach
810d11ac7eSGreg Roach    // If the update server doesn't respond after this time, give up.
8216d6367aSGreg Roach    private const HTTP_TIMEOUT = 3.0;
830d11ac7eSGreg Roach
8443f2f523SGreg Roach    private TimeoutService $timeout_service;
857def76c7SGreg Roach
867def76c7SGreg Roach    /**
877def76c7SGreg Roach     * UpgradeService constructor.
887def76c7SGreg Roach     *
897def76c7SGreg Roach     * @param TimeoutService $timeout_service
907def76c7SGreg Roach     */
917def76c7SGreg Roach    public function __construct(TimeoutService $timeout_service)
927def76c7SGreg Roach    {
937def76c7SGreg Roach        $this->timeout_service = $timeout_service;
947def76c7SGreg Roach    }
957def76c7SGreg Roach
967def76c7SGreg Roach    /**
977def76c7SGreg Roach     * Unpack webtrees.zip.
987def76c7SGreg Roach     *
997def76c7SGreg Roach     * @param string $zip_file
1007def76c7SGreg Roach     * @param string $target_folder
10125d7fe95SGreg Roach     *
10225d7fe95SGreg Roach     * @return void
1037def76c7SGreg Roach     */
10425d7fe95SGreg Roach    public function extractWebtreesZip(string $zip_file, string $target_folder): void
1057def76c7SGreg Roach    {
1067def76c7SGreg Roach        // The Flysystem ZIP archive adapter is painfully slow, so use the native PHP library.
1077def76c7SGreg Roach        $zip = new ZipArchive();
1087def76c7SGreg Roach
109320f6a24SGreg Roach        if ($zip->open($zip_file) === true) {
1107def76c7SGreg Roach            $zip->extractTo($target_folder);
1117def76c7SGreg Roach            $zip->close();
1127def76c7SGreg Roach        } else {
113d501c45dSGreg Roach            throw new HttpServerErrorException('Cannot read ZIP file. Is it corrupt?');
1147def76c7SGreg Roach        }
1157def76c7SGreg Roach    }
1167def76c7SGreg Roach
1177def76c7SGreg Roach    /**
1187def76c7SGreg Roach     * Create a list of all the files in a webtrees .ZIP archive
1197def76c7SGreg Roach     *
1204db4b4a9SGreg Roach     * @param string $zip_file
1214db4b4a9SGreg Roach     *
12236779af1SGreg Roach     * @return Collection<int,string>
123f7cf8a15SGreg Roach     * @throws FilesystemException
1247def76c7SGreg Roach     */
1254db4b4a9SGreg Roach    public function webtreesZipContents(string $zip_file): Collection
12623de9ab7SGreg Roach    {
127f7cf8a15SGreg Roach        $zip_provider   = new FilesystemZipArchiveProvider($zip_file, 0755);
128f7cf8a15SGreg Roach        $zip_adapter    = new ZipArchiveAdapter($zip_provider, 'webtrees');
129f7cf8a15SGreg Roach        $zip_filesystem = new Filesystem($zip_adapter);
1307def76c7SGreg Roach
131782714c2SGreg Roach        $files = $zip_filesystem->listContents('', FilesystemReader::LIST_DEEP)
132f7cf8a15SGreg Roach            ->filter(static function (StorageAttributes $attributes): bool {
133f7cf8a15SGreg Roach                return $attributes->isFile();
1347def76c7SGreg Roach            })
135f7cf8a15SGreg Roach            ->map(static function (StorageAttributes $attributes): string {
136f7cf8a15SGreg Roach                return $attributes->path();
1377def76c7SGreg Roach            });
138f7cf8a15SGreg Roach
139f7cf8a15SGreg Roach        return new Collection($files);
1407def76c7SGreg Roach    }
1417def76c7SGreg Roach
1427def76c7SGreg Roach    /**
1437def76c7SGreg Roach     * Fetch a file from a URL and save it in a filesystem.
1447def76c7SGreg Roach     * Use streams so that we can copy files larger than our available memory.
1457def76c7SGreg Roach     *
1467def76c7SGreg Roach     * @param string             $url
147f7cf8a15SGreg Roach     * @param FilesystemOperator $filesystem
1487def76c7SGreg Roach     * @param string             $path
1497def76c7SGreg Roach     *
1507def76c7SGreg Roach     * @return int The number of bytes downloaded
1514022f21aSGreg Roach     * @throws GuzzleException
152f0448b68SGreg Roach     * @throws FilesystemException
1537def76c7SGreg Roach     */
154f7cf8a15SGreg Roach    public function downloadFile(string $url, FilesystemOperator $filesystem, string $path): int
1557def76c7SGreg Roach    {
1567def76c7SGreg Roach        // We store the data in PHP temporary storage.
157ea517a3bSGreg Roach        $tmp = fopen('php://memory', 'wb+');
1587def76c7SGreg Roach
1597def76c7SGreg Roach        // Read from the URL
1607def76c7SGreg Roach        $client   = new Client();
1617def76c7SGreg Roach        $response = $client->get($url, self::GUZZLE_OPTIONS);
1627def76c7SGreg Roach        $stream   = $response->getBody();
1637def76c7SGreg Roach
1647def76c7SGreg Roach        // Download the file to temporary storage.
1657def76c7SGreg Roach        while (!$stream->eof()) {
166783b32e3SGreg Roach            $data = $stream->read(self::READ_BLOCK_SIZE);
167783b32e3SGreg Roach
168783b32e3SGreg Roach            $bytes_written = fwrite($tmp, $data);
169783b32e3SGreg Roach
170783b32e3SGreg Roach            if ($bytes_written !== strlen($data)) {
171783b32e3SGreg Roach                throw new RuntimeException('Unable to write to stream.  Perhaps the disk is full?');
172783b32e3SGreg Roach            }
1737def76c7SGreg Roach
174fbb4b472SGreg Roach            if ($this->timeout_service->isTimeNearlyUp()) {
1753551ba8dSGreg Roach                $stream->close();
176d501c45dSGreg Roach                throw new HttpServerErrorException(I18N::translate('The server’s time limit has been reached.'));
1777def76c7SGreg Roach            }
1787def76c7SGreg Roach        }
1797def76c7SGreg Roach
1803551ba8dSGreg Roach        $stream->close();
1817def76c7SGreg Roach
1827def76c7SGreg Roach        // Copy from temporary storage to the file.
183fbb4b472SGreg Roach        $bytes = ftell($tmp);
1847def76c7SGreg Roach        rewind($tmp);
1857def76c7SGreg Roach        $filesystem->writeStream($path, $tmp);
1867def76c7SGreg Roach        fclose($tmp);
1877def76c7SGreg Roach
1887def76c7SGreg Roach        return $bytes;
1897def76c7SGreg Roach    }
1907def76c7SGreg Roach
1917def76c7SGreg Roach    /**
1927def76c7SGreg Roach     * Move (copy and delete) all files from one filesystem to another.
1937def76c7SGreg Roach     *
194f7cf8a15SGreg Roach     * @param FilesystemOperator $source
195f7cf8a15SGreg Roach     * @param FilesystemOperator $destination
19625d7fe95SGreg Roach     *
19725d7fe95SGreg Roach     * @return void
198f7cf8a15SGreg Roach     * @throws FilesystemException
1997def76c7SGreg Roach     */
200f7cf8a15SGreg Roach    public function moveFiles(FilesystemOperator $source, FilesystemOperator $destination): void
20123de9ab7SGreg Roach    {
202782714c2SGreg Roach        foreach ($source->listContents('', FilesystemReader::LIST_DEEP) as $attributes) {
203f7cf8a15SGreg Roach            if ($attributes->isFile()) {
204f7cf8a15SGreg Roach                $destination->write($attributes->path(), $source->read($attributes->path()));
205f7cf8a15SGreg Roach                $source->delete($attributes->path());
2067def76c7SGreg Roach
207fbb4b472SGreg Roach                if ($this->timeout_service->isTimeNearlyUp()) {
208d501c45dSGreg Roach                    throw new HttpServerErrorException(I18N::translate('The server’s time limit has been reached.'));
2097def76c7SGreg Roach                }
2107def76c7SGreg Roach            }
2117def76c7SGreg Roach        }
2127def76c7SGreg Roach    }
2137def76c7SGreg Roach
2140d11ac7eSGreg Roach    /**
215fbb4b472SGreg Roach     * Delete files in $destination that aren't in $source.
216fbb4b472SGreg Roach     *
217f7cf8a15SGreg Roach     * @param FilesystemOperator $filesystem
21836779af1SGreg Roach     * @param Collection<int,string> $folders_to_clean
21936779af1SGreg Roach     * @param Collection<int,string> $files_to_keep
22025d7fe95SGreg Roach     *
22125d7fe95SGreg Roach     * @return void
222fbb4b472SGreg Roach     */
223f7cf8a15SGreg Roach    public function cleanFiles(FilesystemOperator $filesystem, Collection $folders_to_clean, Collection $files_to_keep): void
224fbb4b472SGreg Roach    {
225fbb4b472SGreg Roach        foreach ($folders_to_clean as $folder_to_clean) {
226f0448b68SGreg Roach            try {
227782714c2SGreg Roach                foreach ($filesystem->listContents($folder_to_clean, FilesystemReader::LIST_DEEP) as $path) {
228fbb4b472SGreg Roach                    if ($path['type'] === 'file' && !$files_to_keep->contains($path['path'])) {
229f0448b68SGreg Roach                        try {
230fbb4b472SGreg Roach                            $filesystem->delete($path['path']);
23128d026adSGreg Roach                        } catch (FilesystemException | UnableToDeleteFile) {
232f0448b68SGreg Roach                            // Skip to the next file.
233f0448b68SGreg Roach                        }
234fbb4b472SGreg Roach                    }
235fbb4b472SGreg Roach
236fbb4b472SGreg Roach                    // If we run out of time, then just stop.
237fbb4b472SGreg Roach                    if ($this->timeout_service->isTimeNearlyUp()) {
238fbb4b472SGreg Roach                        return;
239fbb4b472SGreg Roach                    }
240fbb4b472SGreg Roach                }
24128d026adSGreg Roach            } catch (FilesystemException) {
242f0448b68SGreg Roach                // Skip to the next folder.
243f0448b68SGreg Roach            }
244fbb4b472SGreg Roach        }
245fbb4b472SGreg Roach    }
246fbb4b472SGreg Roach
247fbb4b472SGreg Roach    /**
248*bfd5083cSGreg Roach     * @param bool $force
249*bfd5083cSGreg Roach     *
2500d11ac7eSGreg Roach     * @return bool
2510d11ac7eSGreg Roach     */
252*bfd5083cSGreg Roach    public function isUpgradeAvailable(bool $force = false): bool
2530d11ac7eSGreg Roach    {
25423927444SGreg Roach        // If the latest version is unavailable, we will have an empty string which equates to version 0.
2550d11ac7eSGreg Roach
256*bfd5083cSGreg Roach        return version_compare(Webtrees::VERSION, $this->fetchLatestVersion($force)) < 0;
2570d11ac7eSGreg Roach    }
2580d11ac7eSGreg Roach
2590d11ac7eSGreg Roach    /**
2600d11ac7eSGreg Roach     * What is the latest version of webtrees.
2610d11ac7eSGreg Roach     *
2620d11ac7eSGreg Roach     * @return string
2630d11ac7eSGreg Roach     */
2640d11ac7eSGreg Roach    public function latestVersion(): string
2650d11ac7eSGreg Roach    {
266*bfd5083cSGreg Roach        $latest_version = $this->fetchLatestVersion(false);
2670d11ac7eSGreg Roach
26865e02381SGreg Roach        [$version] = explode('|', $latest_version);
2690d11ac7eSGreg Roach
2700d11ac7eSGreg Roach        return $version;
2710d11ac7eSGreg Roach    }
2720d11ac7eSGreg Roach
2730d11ac7eSGreg Roach    /**
274*bfd5083cSGreg Roach     * What, if any, error did we have when fetching the latest version of webtrees.
275*bfd5083cSGreg Roach     *
276*bfd5083cSGreg Roach     * @return string
277*bfd5083cSGreg Roach     */
278*bfd5083cSGreg Roach    public function latestVersionError(): string
279*bfd5083cSGreg Roach    {
280*bfd5083cSGreg Roach        return Site::getPreference('LATEST_WT_VERSION_ERROR');
281*bfd5083cSGreg Roach    }
282*bfd5083cSGreg Roach
283*bfd5083cSGreg Roach    /**
284*bfd5083cSGreg Roach     * When did we last try to fetch the latest version of webtrees.
285*bfd5083cSGreg Roach     *
286*bfd5083cSGreg Roach     * @return TimestampInterface
287*bfd5083cSGreg Roach     */
288*bfd5083cSGreg Roach    public function latestVersionTimestamp(): TimestampInterface
289*bfd5083cSGreg Roach    {
290*bfd5083cSGreg Roach        $latest_version_wt_timestamp = (int) Site::getPreference('LATEST_WT_VERSION_TIMESTAMP');
291*bfd5083cSGreg Roach
292*bfd5083cSGreg Roach        return Registry::timestampFactory()->make($latest_version_wt_timestamp);
293*bfd5083cSGreg Roach    }
294*bfd5083cSGreg Roach
295*bfd5083cSGreg Roach    /**
2960d11ac7eSGreg Roach     * Where can we download the latest version of webtrees.
2970d11ac7eSGreg Roach     *
2980d11ac7eSGreg Roach     * @return string
2990d11ac7eSGreg Roach     */
3000d11ac7eSGreg Roach    public function downloadUrl(): string
3010d11ac7eSGreg Roach    {
302*bfd5083cSGreg Roach        $latest_version = $this->fetchLatestVersion(false);
3030d11ac7eSGreg Roach
30465e02381SGreg Roach        [, , $url] = explode('|', $latest_version . '||');
3050d11ac7eSGreg Roach
3060d11ac7eSGreg Roach        return $url;
3070d11ac7eSGreg Roach    }
3080d11ac7eSGreg Roach
30992a78a2fSGreg Roach    /**
31092a78a2fSGreg Roach     * @return void
31192a78a2fSGreg Roach     */
3127def76c7SGreg Roach    public function startMaintenanceMode(): void
3137def76c7SGreg Roach    {
3147def76c7SGreg Roach        $message = I18N::translate('This website is being upgraded. Try again in a few minutes.');
3157def76c7SGreg Roach
316f397d0fdSGreg Roach        file_put_contents(Webtrees::OFFLINE_FILE, $message);
3177def76c7SGreg Roach    }
3187def76c7SGreg Roach
31992a78a2fSGreg Roach    /**
32092a78a2fSGreg Roach     * @return void
32192a78a2fSGreg Roach     */
3227def76c7SGreg Roach    public function endMaintenanceMode(): void
3237def76c7SGreg Roach    {
324f397d0fdSGreg Roach        if (file_exists(Webtrees::OFFLINE_FILE)) {
325f397d0fdSGreg Roach            unlink(Webtrees::OFFLINE_FILE);
3267def76c7SGreg Roach        }
3277def76c7SGreg Roach    }
3287def76c7SGreg Roach
3290d11ac7eSGreg Roach    /**
3300d11ac7eSGreg Roach     * Check with the webtrees.net server for the latest version of webtrees.
3310d11ac7eSGreg Roach     * Fetching the remote file can be slow, so check infrequently, and cache the result.
332*bfd5083cSGreg Roach     * Pass the current versions of webtrees, PHP and database, as the response
3330d11ac7eSGreg Roach     * may be different for each. The server logs are used to generate
334ad3143ccSGreg Roach     * installation statistics which can be found at https://dev.webtrees.net/statistics.html
3350d11ac7eSGreg Roach     *
336*bfd5083cSGreg Roach     * @param bool $force
337*bfd5083cSGreg Roach     *
3380d11ac7eSGreg Roach     * @return string
3390d11ac7eSGreg Roach     */
340*bfd5083cSGreg Roach    private function fetchLatestVersion(bool $force): string
3410d11ac7eSGreg Roach    {
3420d11ac7eSGreg Roach        $last_update_timestamp = (int) Site::getPreference('LATEST_WT_VERSION_TIMESTAMP');
3430d11ac7eSGreg Roach
344d97083feSGreg Roach        $current_timestamp = time();
345bf57b580SGreg Roach
346*bfd5083cSGreg Roach        if ($force || $last_update_timestamp < $current_timestamp - self::CHECK_FOR_UPDATE_INTERVAL) {
3470d11ac7eSGreg Roach            try {
3480d11ac7eSGreg Roach                $client = new Client([
3490d11ac7eSGreg Roach                    'timeout' => self::HTTP_TIMEOUT,
3500d11ac7eSGreg Roach                ]);
3510d11ac7eSGreg Roach
3520d11ac7eSGreg Roach                $response = $client->get(self::UPDATE_URL, [
3530d11ac7eSGreg Roach                    'query' => $this->serverParameters(),
3540d11ac7eSGreg Roach                ]);
3550d11ac7eSGreg Roach
3566ccdf4f0SGreg Roach                if ($response->getStatusCode() === StatusCodeInterface::STATUS_OK) {
3570d11ac7eSGreg Roach                    Site::setPreference('LATEST_WT_VERSION', $response->getBody()->getContents());
358bf57b580SGreg Roach                    Site::setPreference('LATEST_WT_VERSION_TIMESTAMP', (string) $current_timestamp);
359*bfd5083cSGreg Roach                    Site::setPreference('LATEST_WT_VERSION_ERROR', '');
360*bfd5083cSGreg Roach                } else {
361*bfd5083cSGreg Roach                    Site::setPreference('LATEST_WT_VERSION_ERROR', 'HTTP' . $response->getStatusCode());
3620d11ac7eSGreg Roach                }
3634022f21aSGreg Roach            } catch (GuzzleException $ex) {
364c6a0ce5cSGreg Roach                // Can't connect to the server?
365c6a0ce5cSGreg Roach                // Use the existing information about latest versions.
366*bfd5083cSGreg Roach                Site::setPreference('LATEST_WT_VERSION_ERROR', $ex->getMessage());
3670d11ac7eSGreg Roach            }
3680d11ac7eSGreg Roach        }
3690d11ac7eSGreg Roach
3700d11ac7eSGreg Roach        return Site::getPreference('LATEST_WT_VERSION');
3710d11ac7eSGreg Roach    }
3720d11ac7eSGreg Roach
3730d11ac7eSGreg Roach    /**
3740d11ac7eSGreg Roach     * The upgrade server needs to know a little about this server.
3755d4b7ec2SGreg Roach     *
3765d4b7ec2SGreg Roach     * @return array<string,string>
3770d11ac7eSGreg Roach     */
3788f53f488SRico Sonntag    private function serverParameters(): array
3790d11ac7eSGreg Roach    {
380c4cbcd7bSGreg Roach        $site_uuid = Site::getPreference('SITE_UUID');
381c4cbcd7bSGreg Roach
382c4cbcd7bSGreg Roach        if ($site_uuid === '') {
3832e464181SGreg Roach            $site_uuid = Registry::idFactory()->uuid();
384c4cbcd7bSGreg Roach            Site::setPreference('SITE_UUID', $site_uuid);
385c4cbcd7bSGreg Roach        }
386c4cbcd7bSGreg Roach
387c4cbcd7bSGreg Roach        $database_type = DB::connection()->getDriverName();
3880d11ac7eSGreg Roach
3890d11ac7eSGreg Roach        return [
3908d0ebef0SGreg Roach            'w' => Webtrees::VERSION,
3910d11ac7eSGreg Roach            'p' => PHP_VERSION,
392c4cbcd7bSGreg Roach            's' => $site_uuid,
393c4cbcd7bSGreg Roach            'd' => $database_type,
3940d11ac7eSGreg Roach        ];
3950d11ac7eSGreg Roach    }
3960d11ac7eSGreg Roach}
397