blue-twilight/installer/Installer.php

394 lines
11 KiB
PHP

<?php
namespace AppInstaller;
use App\Services\GiteaService;
use Illuminate\Support\Facades\Artisan;
/**
* This class handles the downloading and extracting of the vendors directory.
* Because Laravel and other vendors are not yet available, it uses "raw" PHP and the odd few classes within Blue
* Twilight, such as GiteaService.
*
* @package AppInstaller
*/
class Installer
{
/**
* Path to /app/config
* @var string
*/
private $configDir;
/**
* Path to /installer
* @var string
*/
private $installerDir;
/**
* True if we're upgrading Blue Twilight, false if it's a new install
* @var bool
*/
private $isUpgrade;
/**
* Path to / - the app's root
* @var string
*/
private $rootDir;
/**
* Path to /installer/temp
* @var string
*/
private $tempDir;
/**
* Path to /vendor
* @var string
*/
private $vendorDir;
/**
* @var string
*/
private $versionNumber;
/**
* Path to /installer/views
* @var string
*/
private $viewsDir;
public function __construct()
{
$this->installerDir = __DIR__;
$this->rootDir = dirname($this->installerDir);
$this->configDir = sprintf('%s/config', $this->rootDir);
$this->tempDir = sprintf('%s/temp', $this->installerDir);
$this->vendorDir = sprintf('%s/vendor', $this->rootDir);
$this->viewsDir = sprintf('%s/views', $this->installerDir);
$appConfig = require_once sprintf('%s/app.php', $this->configDir);
$this->versionNumber = $appConfig['version'];
}
public function handleRequest()
{
if (!isset($_GET['act']))
{
$this->checkInstallation();
}
else
{
switch (trim($_GET['act']))
{
case 'download':
$this->download();
return;
case 'extract':
$this->extract();
return;
case 'finalise':
$this->finalise();
return;
default:
throw new \Exception(sprintf('Action \'%s\' was not recognised.', $_GET['act']));
}
}
}
/**
* @param bool $isUpgrade
*/
public function setIsUpgrade(bool $isUpgrade): void
{
$this->isUpgrade = $isUpgrade;
}
protected function checkInstallation()
{
$requirements = [
'core:php72OrLater',
'php:hasCurlLibrary',
'php:hasMySqlClientLibrary',
'php:hasGdLibrary',
'config:maxUploadSize',
'config:maxPostRequestSize'
];
$requirementsGrouped = [];
$canInstall = true;
foreach ($requirements as $requirement)
{
$requirementSplit = explode(':', $requirement);
$groupName = $requirementSplit[0];
$functionName = $requirementSplit[1];
$status = null;
$result = call_user_func_array([AppRequirements::class, $functionName], [&$status]);
if ($result == AppRequirements::STATUS_NOT_MET)
{
$canInstall = false;
}
if (!array_key_exists($groupName, $requirementsGrouped))
{
$requirementsGrouped[$groupName] = [];
}
if (!array_key_exists($functionName, $requirementsGrouped[$groupName]))
{
$requirementsGrouped[$groupName][$functionName] = [
'result' => $result,
'status' => $status
];
}
}
$requirementGroupNames = [
'config' => 'PHP configuration',
'core' => 'Core requirements',
'php' => 'Required PHP modules'
];
$requirementNames = [
'hasCurlLibrary' => 'cURL Web Requests Library',
'hasGdLibrary' => 'GD Graphics Procesisng Library',
'hasMySqlClientLibrary' => 'MySQL PDO Data Access Library',
'maxPostRequestSize' => 'Maximum POST request size (recommended: 4 MB)',
'maxUploadSize' => 'Maximum file size allowed to upload (recommended: 4 MB)',
'php72OrLater' => 'Requires PHP 7.2.0 minimum',
];
$this->view('index', [
'appName' => 'Blue Twilight Installer',
'canInstall' => $canInstall,
'isUpgrade' => $this->isUpgrade,
'requirementGroupNames' => $requirementGroupNames,
'requirementNames' => $requirementNames,
'statusNotMet' => AppRequirements::STATUS_NOT_MET,
'statusOK' => AppRequirements::STATUS_OK,
'statusWarning' => AppRequirements::STATUS_WARNING,
'systemRequirements' => $requirementsGrouped
]);
}
protected function download()
{
$servicesConfig = require_once sprintf('%s/services.php', $this->configDir);
$versionNumber = sprintf('v%s', $this->versionNumber);
$gitea = new GiteaService($servicesConfig['gitea'], $versionNumber);
$releaseInfo = $gitea->getSpecificRelease($versionNumber);
if (is_null($releaseInfo))
{
throw new \Exception(sprintf('No release info found in Gitea for Blue Twilight version \'%s\'', $versionNumber));
}
else if (!isset($releaseInfo->assets))
{
throw new \Exception(sprintf('No assets found in Gitea for Blue Twilight version \'%s\'', $versionNumber));
}
$vendorsPrefix = 'vendors';
$vendorsSuffix = '.tar.gz';
$selectedAsset = null;
foreach ($releaseInfo->assets as $asset)
{
/*
Ignore anything that is not "vendors<something>.tar.gz" were the <something> is also optional - e.g.
vendors_2.1.2.tar.gz
vendors.tar.gz
but NOT 2.1.2_vendors.zip
*/
if (!starts_with($asset->name, $vendorsPrefix) || !ends_with($asset->name, $vendorsSuffix))
{
continue;
}
$selectedAsset = $asset;
break;
}
if (is_null($selectedAsset))
{
throw new \Exception(sprintf('No vendors.tar.gz found in Gitea for Blue Twilight version \'%s\'', $versionNumber));
}
$targetFileName = $this->getVendorsTempFileName();
$this->downloadFile($selectedAsset->browser_download_url, $targetFileName);
$this->json([
'result' => true,
'fileName' => $targetFileName
]);
}
protected function extract()
{
$targetFileName = $this->getVendorsTempFileName();
if (!file_exists($targetFileName) || !is_readable($targetFileName))
{
throw new \Exception(sprintf('The file \'%s\' does not exist or is not readable', $targetFileName));
}
$phar = new \PharData($targetFileName);
$phar->extractTo($this->rootDir, null, true);
// We should always have a vendor/autoload.php
$vendorsTestFile = $this->getVendorsAutoloadFileName();
if (file_exists($vendorsTestFile))
{
$this->writeVersionFile();
$this->json([
'result' => true,
'testFile' => $vendorsTestFile
]);
}
else
{
throw new \Exception('The extraction failed');
}
}
protected function finalise()
{
$result = [
'cacheFilesRemoved' => 0,
'envFileCreated' => false
];
$result['cacheFilesRemoved'] = $this->clearCacheIfExists();
$result['envFileCreated'] = !$this->isUpgrade && $this->createEnvFileIfNotExist();
require sprintf('%s/bootstrap/autoload.php', $this->rootDir);
$app = require_once sprintf('%s/bootstrap/app.php', $this->rootDir);
$kernel = $app->make(\Illuminate\Contracts\Console\Kernel::class);
$kernel->bootstrap();
if ($result['envFileCreated'])
{
Artisan::call('key:generate');
}
$kernel->terminate(null, null);
$this->json($result);
}
private function downloadFile($sourceURL, $targetFilename)
{
$urlHandle = @fopen($sourceURL, 'r');
$tempFilename = @fopen($targetFilename, 'w');
if ($urlHandle === false)
{
throw new \Exception(sprintf('Failed downloading the file from %s', $sourceURL));
}
else if ($tempFilename === false)
{
throw new \Exception(sprintf('Failed opening the file \'%s\' for writing', $targetFilename));
}
while (!feof($urlHandle))
{
$buffer = fread($urlHandle, 8192);
fwrite($tempFilename, $buffer);
}
@fclose($urlHandle);
@fclose($tempFilename);
}
private function clearCacheIfExists()
{
$filesDeleted = 0;
$cacheDir = sprintf('%s/bootstrap/cache', $this->rootDir);
$di = new \DirectoryIterator($cacheDir);
foreach ($di as $fileInfo)
{
if (@unlink($fileInfo->getRealPath()))
{
$filesDeleted++;
}
}
return $filesDeleted;
}
private function createEnvFileIfNotExist()
{
$envFile = $this->getEnvFileName();
if (!file_exists($envFile))
{
copy($this->getEnvExampleFileName(), $envFile);
return true;
}
return false;
}
private function getEnvExampleFileName()
{
return sprintf('%s/.env.example', $this->rootDir);
}
private function getEnvFileName()
{
return sprintf('%s/.env', $this->rootDir);
}
private function getVendorsAutoloadFileName()
{
return sprintf('%s/autoload.php', $this->vendorDir);
}
private function getVendorsTempFileName()
{
return sprintf('%s/vendors.tar.gz', $this->tempDir);
}
private function getVendorsVersionFileName()
{
return sprintf('%s/version.txt', $this->vendorDir);
}
private function json($data)
{
echo json_encode($data);
}
private function view($name, array $viewData = [])
{
$viewFile = sprintf('%s/%s.php', $this->viewsDir, $name);
if (!file_exists($viewFile) || !is_readable($viewFile))
{
throw new \Exception(sprintf('ERROR: View file \'%s\' does not exist.', $viewFile));
}
// Provide keys as variables - e.g. $viewData['something'] becomes accessible via $something
extract($viewData);
require_once $viewFile;
}
private function writeVersionFile()
{
file_put_contents($this->getVendorsVersionFileName(), $this->versionNumber . PHP_EOL);
}
}