80 lines
7.0 KiB
PHP
80 lines
7.0 KiB
PHP
<?php
|
|
|
|
// Active inspection only. No application providers, sessions, Artisan or writes to PostgreSQL.
|
|
use App\Services\OrganizationCountryInstallation;
|
|
use Illuminate\Foundation\Bootstrap\LoadConfiguration;
|
|
use Illuminate\Foundation\Bootstrap\LoadEnvironmentVariables;
|
|
|
|
$root = dirname(__DIR__);
|
|
require $root.'/vendor/autoload.php';
|
|
ini_set('zend.exception_ignore_args', '1');
|
|
set_exception_handler(static function (Throwable $error): void {
|
|
fwrite(STDERR, 'COUNTRY02 preflight refused: '.$error->getMessage().PHP_EOL);
|
|
exit(1);
|
|
});
|
|
$app = require $root.'/bootstrap/app.php';
|
|
(new LoadEnvironmentVariables)->bootstrap($app);
|
|
(new LoadConfiguration)->bootstrap($app);
|
|
$read = static fn (string $path): array => json_decode(file_get_contents($path), true, 512, JSON_THROW_ON_ERROR);
|
|
$dir = $root.'/storage/app/private/investmoz-v2/portal-audit';
|
|
$proof = $read($read($dir.'/country-candidate-backup-latest.json')['path']);
|
|
$manifest = $read($root.'/outputs/investmoz-country02-candidate-20261011/manifesto-candidata.json');
|
|
$installer = new OrganizationCountryInstallation;
|
|
throw_unless($installer->fingerprint() === '36f7722ce5a51abc6a246116afcab22ebc8167fa3d06e493920ad18c7afbfca8'
|
|
&& $manifest['delivery_revision'] === 'c68ceef9d4a3bc10da7bf269c1261a7b1edece3c1e114d6fef8cc60ca9f4403c', RuntimeException::class, 'Approved code/delivery differs.');
|
|
$c = config('database.connections.'.config('database.default'));
|
|
throw_unless($c['driver'] === 'pgsql' && empty($c['url']) && (int) $c['port'] === 5432 && $c['database'] === 'investemoz', RuntimeException::class, 'Exact active target required.');
|
|
$pdo = new PDO('pgsql:host='.$c['host'].';port='.$c['port'].';dbname='.$c['database'], $c['username'], $c['password'], [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION]);
|
|
$pdo->exec('BEGIN ISOLATION LEVEL REPEATABLE READ READ ONLY');
|
|
$identity = $pdo->query("SELECT current_database() AS database,inet_server_port() AS port,current_setting('transaction_read_only') AS read_only")->fetch(PDO::FETCH_ASSOC);
|
|
throw_unless($identity['database'] === 'investemoz' && (int) $identity['port'] === 5432 && $identity['read_only'] === 'on', RuntimeException::class, 'Server target differs.');
|
|
$evidence = $installer->evidence((object) ['database' => $identity['database'], 'port' => (int) $identity['port']]);
|
|
throw_unless($evidence['ready'], RuntimeException::class, 'Backup/readiness invalid or expired.');
|
|
$installations = $pdo->query("SELECT migration FROM migrations WHERE migration IN ('2026_10_09_030000_extend_opportunity_resources','2026_10_10_040000_create_portal_profile_authorization','2026_10_09_190000_prepare_investor_path','2026_10_10_050000_extend_organization_country') ORDER BY migration")->fetchAll(PDO::FETCH_COLUMN);
|
|
$columns = $pdo->query("SELECT column_name FROM information_schema.columns WHERE table_schema='public' AND table_name='organizations' AND column_name IN ('incorporation_country_code','incorporation_country_provenance') ORDER BY column_name")->fetchAll(PDO::FETCH_COLUMN);
|
|
throw_unless($installations === $proof['source_installations']['migrations'] && $columns === $proof['source_installations']['country_columns'], RuntimeException::class, 'Schema/ledger diverges from reviewed pre-installation state.');
|
|
$runtime = ['config_cached' => $app->configurationIsCached(), 'cipher' => config('app.cipher'), 'session_driver' => config('session.driver'), 'app_name' => config('app.name'),
|
|
'organization_country_enabled' => (bool) config('portal.organization_country_enabled'), 'opportunity_dashboard_access_enabled' => (bool) config('portal.opportunity_dashboard_access_enabled'),
|
|
'profile_access_enabled' => (bool) config('portal.profile_access_enabled'), 'opportunity_specialized_drafts_enabled' => (bool) config('portal.opportunity_specialized_drafts_enabled'),
|
|
'opportunity_modular_profiles_qa_enabled' => (bool) config('portal.opportunity_modular_profiles_qa_enabled'), 'legacy_content_qa_enabled' => (bool) config('portal.legacy_content_qa_enabled'), 'investor_path_enabled' => (bool) config('investor-path.enabled')];
|
|
throw_unless($runtime === $proof['runtime'], RuntimeException::class, 'Runtime flags/configuration differs from reviewed state.');
|
|
$tables = [];
|
|
foreach ($pdo->query("SELECT schemaname,tablename FROM pg_tables WHERE schemaname NOT IN ('pg_catalog','information_schema') ORDER BY schemaname,tablename")->fetchAll(PDO::FETCH_ASSOC) as $t) {
|
|
$name = '"'.str_replace('"', '""', $t['schemaname']).'"."'.str_replace('"', '""', $t['tablename']).'"';
|
|
$rows = $pdo->query('SELECT row_to_json(t)::text FROM '.$name.' t ORDER BY row_to_json(t)::text')->fetchAll(PDO::FETCH_COLUMN);
|
|
$tables[$name] = ['count' => count($rows), 'sha256' => hash('sha256', implode("\n", $rows))];
|
|
}
|
|
$different = [];
|
|
foreach ($tables as $name => $v) {
|
|
if (($proof['tables'][$name] ?? null) !== $v) {
|
|
$different[] = $name;
|
|
}
|
|
}
|
|
throw_unless(array_keys($tables) === array_keys($proof['tables']), RuntimeException::class, 'Table inventory changed.');
|
|
$counts = [];
|
|
foreach (['users','organizations','memberships','migrations','audit_events','sessions'] as $table) {
|
|
$counts[$table] = (int) $pdo->query('SELECT count(*) FROM "'.$table.'"')->fetchColumn();
|
|
}
|
|
$pdo->exec('ROLLBACK');
|
|
$archive = new ZipArchive;
|
|
throw_unless($archive->open($proof['files_path']) === true, RuntimeException::class, 'Backup runtime archive unreadable.');
|
|
$archivedRuntime = json_decode($archive->getFromName('runtime/effective-runtime.json'), true);
|
|
if (! is_array($archivedRuntime)) {
|
|
// Archive entry names are private and are not echoed.
|
|
for ($i = 0; $i < $archive->numFiles; $i++) {
|
|
$entry = $archive->getNameIndex($i);
|
|
if (str_ends_with($entry, 'effective-runtime.json')) {
|
|
$archivedRuntime = json_decode($archive->getFromIndex($i), true);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
$archive->close();
|
|
throw_unless(is_array($archivedRuntime) && ($archivedRuntime['app_key'] ?? $archivedRuntime['key'] ?? null) === config('app.key'), RuntimeException::class, 'Stable runtime key cannot be verified against archive.');
|
|
$result = ['result' => $different === [] ? 'passed' : 'renew_backup_before_activation', 'at' => gmdate('c'), 'authorization' => ['scope' => 'COUNTRY02 only, 050000 only', 'delivery_revision' => $manifest['delivery_revision'], 'code_hash' => $installer->fingerprint()],
|
|
'target' => $identity, 'origin' => config('app.url'), 'runtime' => $runtime, 'evidence' => $evidence, 'backup_age_seconds' => time() - strtotime($proof['verified_at']),
|
|
'backup_code_build_match' => $proof['code_hash'] === $installer->fingerprint() && $proof['production_manifest_sha256'] === $manifest['production']['manifest_sha256'],
|
|
'stable_key_match' => true, 'counts' => $counts, 'tables' => $tables, 'changed_since_snapshot' => $different, 'installations' => $installations, 'country_columns' => $columns, 'active_database_writes' => 0];
|
|
file_put_contents($dir.'/country-activation-preflight-20261011.json', json_encode($result, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR));
|
|
echo json_encode(array_diff_key($result, ['tables' => true]), JSON_THROW_ON_ERROR).PHP_EOL;
|