394 lines
11 KiB
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);
|
|
}
|
|
} |