Skip to content
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,4 @@ demo/
.build-cache.json
.bashatimerc
.wakatime-project
release.zip
10 changes: 10 additions & 0 deletions app/BlueprintFramework/Doctor/CheckInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Pterodactyl\BlueprintFramework\Doctor;

interface CheckInterface
{
public function name(): string;
public function title(): string;
public function run(): CheckResult;
}
70 changes: 70 additions & 0 deletions app/BlueprintFramework/Doctor/CheckResult.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Pterodactyl\BlueprintFramework\Doctor;

final class CheckResult
{
/** @var array<int,array{label:string,value:string,status:?Severity}> */
private array $rows = [];

/** @var Issue[] */
private array $issues = [];

public function __construct(
public readonly string $name,
public readonly string $title,
) {
}

public function row(string $label, string $value, ?Severity $status = null): self
{
$this->rows[] = ['label' => $label, 'value' => $value, 'status' => $status];
return $this;
}

public function issue(Issue $issue): self
{
$this->issues[] = $issue;
return $this;
}

public function ok(string $label, string $value): self
{
return $this->row($label, $value, Severity::INFO);
}

public function warn(string $label, string $value): self
{
return $this->row($label, $value, Severity::WARNING);
}

public function err(string $label, string $value): self
{
return $this->row($label, $value, Severity::ERROR);
}

public function rows(): array
{
return $this->rows;
}

/** @return Issue[] */
public function issues(): array
{
return $this->issues;
}

public function toArray(): array
{
return [
'name' => $this->name,
'title' => $this->title,
'rows' => array_map(fn ($r) => [
'label' => $r['label'],
'value' => $r['value'],
'status' => $r['status']?->value,
], $this->rows),
'issues' => array_map(fn (Issue $i) => $i->toArray(), $this->issues),
];
}
}
70 changes: 70 additions & 0 deletions app/BlueprintFramework/Doctor/Checks/BlueprintCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
<?php

namespace Pterodactyl\BlueprintFramework\Doctor\Checks;

use Pterodactyl\BlueprintFramework\Doctor\CheckInterface;
use Pterodactyl\BlueprintFramework\Doctor\CheckResult;
use Pterodactyl\BlueprintFramework\Doctor\Issue;
use Pterodactyl\BlueprintFramework\Doctor\Severity;
use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Console\BlueprintConsoleLibrary as BlueprintLibrary;
use Pterodactyl\BlueprintFramework\Services\PlaceholderService\BlueprintPlaceholderService;

class BlueprintCheck implements CheckInterface
{
public function __construct(
private BlueprintPlaceholderService $placeholder,
private BlueprintLibrary $blueprint,
) {
}

public function name(): string { return 'blueprint'; }
public function title(): string { return 'Blueprint'; }

public function run(): CheckResult
{
$r = new CheckResult($this->name(), $this->title());

$version = $this->placeholder->version();
if ($version === 'unknown' || $version === '') {
$r->err('Version', 'unknown');
$r->issue(new Issue(
Severity::CRITICAL,
'Blueprint version could not be determined',
'The BlueprintPlaceholderService did not return a version.',
'Rerun the installer: blueprint -rerun-install',
));
} else {
$r->ok('Version', $version);
}

$isInstalled = file_exists(base_path('.blueprint/extensions/blueprint/private/db/is_installed'));
if ($isInstalled) {
$r->ok('Installation', 'Valid');
} else {
$r->err('Installation', 'Incomplete');
$r->issue(new Issue(
Severity::ERROR,
'Blueprint installation incomplete',
'The is_installed marker file is missing.',
'Run: blueprint -rerun-install',
true,
));
}

$seeded = $this->blueprint->dbGet('blueprint', 'internal:seed', false);
if ($seeded) {
$r->ok('Configuration', 'Valid');
} else {
$r->warn('Configuration', 'Not seeded');
$r->issue(new Issue(
Severity::WARNING,
'Blueprint configuration not seeded',
'Database seed records are missing.',
'Run: php artisan db:seed --class=BlueprintSeeder --force',
true,
));
}

return $r;
}
}
57 changes: 57 additions & 0 deletions app/BlueprintFramework/Doctor/Checks/CacheCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<?php

namespace Pterodactyl\BlueprintFramework\Doctor\Checks;

use Pterodactyl\BlueprintFramework\Doctor\CheckInterface;
use Pterodactyl\BlueprintFramework\Doctor\CheckResult;
use Pterodactyl\BlueprintFramework\Doctor\Issue;
use Pterodactyl\BlueprintFramework\Doctor\Severity;
use Pterodactyl\BlueprintFramework\Libraries\ExtensionLibrary\Console\BlueprintConsoleLibrary as BlueprintLibrary;

class CacheCheck implements CheckInterface
{
public function __construct(private BlueprintLibrary $blueprint) {}

public function name(): string { return 'cache'; }
public function title(): string { return 'Cache'; }

public function run(): CheckResult
{
$r = new CheckResult($this->name(), $this->title());

$cache = $this->blueprint->dbGet('blueprint', 'internal:cache', null);
if ($cache !== null) {
$r->ok('Blueprint cache', "v={$cache}");
} else {
$r->warn('Blueprint cache', 'Missing');
$r->issue(new Issue(
Severity::WARNING,
'Blueprint cache not initialised',
'Extensions use the cache value to bust stylesheet/script cache.',
'Run: php artisan bp:cache',
true,
));
}

$configCache = base_path('bootstrap/cache/config.php');
$envFile = base_path('.env');
if (file_exists($configCache) && file_exists($envFile)) {
if (filemtime($configCache) < filemtime($envFile)) {
$r->warn('Config cache', 'Stale');
$r->issue(new Issue(
Severity::WARNING,
'Configuration cache is stale',
'.env was modified after the config cache was built.',
'Run: php artisan config:clear && php artisan config:cache',
true,
));
} else {
$r->ok('Config cache', 'Fresh');
}
} else {
$r->ok('Config cache', 'Not cached');
}

return $r;
}
}
60 changes: 60 additions & 0 deletions app/BlueprintFramework/Doctor/Checks/ConfigurationCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

namespace Pterodactyl\BlueprintFramework\Doctor\Checks;

use Pterodactyl\BlueprintFramework\Doctor\CheckInterface;
use Pterodactyl\BlueprintFramework\Doctor\CheckResult;
use Pterodactyl\BlueprintFramework\Doctor\Issue;
use Pterodactyl\BlueprintFramework\Doctor\Severity;

class ConfigurationCheck implements CheckInterface
{
public function name(): string { return 'configuration'; }
public function title(): string { return 'Configuration'; }

public function run(): CheckResult
{
$r = new CheckResult($this->name(), $this->title());

$env = base_path('.env');
if (file_exists($env)) {
$r->ok('.env', 'Present');
} else {
$r->err('.env', 'Missing');
$r->issue(new Issue(
Severity::CRITICAL,
'.env file missing',
'The Pterodactyl environment file is missing.',
'Restore .env from .env.example.',
));
}

$key = config('app.key');
if ($key && $key !== 'base64:') {
$r->ok('APP_KEY', 'Set');
} else {
$r->err('APP_KEY', 'Not set');
$r->issue(new Issue(
Severity::CRITICAL,
'APP_KEY not set',
'Pterodactyl cannot encrypt sessions.',
'Run: php artisan key:generate',
));
}

$url = config('app.url');
if ($url) {
$r->ok('APP_URL', (string) $url);
} else {
$r->warn('APP_URL', 'Not set');
$r->issue(new Issue(
Severity::WARNING,
'APP_URL not set',
'URLs generated by Blueprint may be wrong.',
'Set APP_URL in .env.',
));
}

return $r;
}
}
91 changes: 91 additions & 0 deletions app/BlueprintFramework/Doctor/Checks/DatabaseCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
<?php

namespace Pterodactyl\BlueprintFramework\Doctor\Checks;

use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schema;
use Pterodactyl\BlueprintFramework\Doctor\CheckInterface;
use Pterodactyl\BlueprintFramework\Doctor\CheckResult;
use Pterodactyl\BlueprintFramework\Doctor\Issue;
use Pterodactyl\BlueprintFramework\Doctor\Severity;

class DatabaseCheck implements CheckInterface
{
public function name(): string { return 'database'; }
public function title(): string { return 'Database'; }

public function run(): CheckResult
{
$r = new CheckResult($this->name(), $this->title());

try {
DB::connection()->getPdo();
$r->ok('Connection', 'OK');
} catch (\Throwable $e) {
$r->err('Connection', 'Failed');
$r->issue(new Issue(
Severity::CRITICAL,
'Database connection failed',
$e->getMessage(),
'Check your .env database credentials.',
));
return $r;
}

$required = ['settings', 'extension_cached_metadata'];
$missing = [];
foreach ($required as $table) {
if (!Schema::hasTable($table)) {
$missing[] = $table;
}
}

if (empty($missing)) {
$r->ok('Schema', 'OK');
} else {
$r->err('Schema', 'Missing: ' . implode(', ', $missing));
$r->issue(new Issue(
Severity::ERROR,
'Required database tables missing',
'Missing: ' . implode(', ', $missing),
'Run: php artisan migrate --force',
true,
));
}

try {
$pending = $this->pendingMigrationCount();
if ($pending === 0) {
$r->ok('Migrations', 'Up to date');
} else {
$r->warn('Migrations', "{$pending} pending");
$r->issue(new Issue(
Severity::WARNING,
'Pending database migrations',
"{$pending} Laravel migrations have not been run.",
'Run: php artisan migrate --force',
true,
));
}
} catch (\Throwable $e) {
$r->warn('Migrations', 'unable to determine');
}

return $r;
}

private function pendingMigrationCount(): int
{
try {
$migrator = app('migrator');
$ran = $migrator->getRepository()->getRan();
$files = array_map(
fn ($f) => basename($f),
array_keys($migrator->getMigrationFiles([database_path('migrations')]))
);
return count(array_diff($files, $ran));
} catch (\Throwable $e) {
return 0;
}
}
}
Loading