Files
Investidor-V2/tools/portal-stability-restore-rehearsal.php
T
nala 287e991486
tests / ci (push) Waiting to run
oportunidades 7
2026-10-13 11:40:29 +02:00

26 lines
5.3 KiB
PHP

<?php
// Real dump/file restoration restricted to a NEW QA database. Never writes port 5432.
use Symfony\Component\Process\Process;
ini_set('zend.exception_ignore_args','1');$root=dirname(__DIR__);require $root.'/vendor/autoload.php';
$dir=$root.'/storage/app/private/investmoz-v2/portal-audit';$p=json_decode(file_get_contents($dir.'/portal-stability-latest.json'),true,512,JSON_THROW_ON_ERROR);$source=$p['source'];
$link=json_decode(file_get_contents($dir.'/country-https-backup-latest.json'),true,512,JSON_THROW_ON_ERROR);$proof=json_decode(file_get_contents($link['path']),true,512,JSON_THROW_ON_ERROR);
throw_unless($proof['restore_verified']&&$proof['files_verified']&&hash_file('sha256',$proof['dump_path'])===$proof['dump_sha256']&&hash_file('sha256',$proof['files_path'])===$proof['files_sha256'],RuntimeException::class,'Corresponding verified backup required.');
$db='country_candidate_'.gmdate('Ymd_His').'_test';$pdo=new PDO('pgsql:host=127.0.0.1;port=55441;dbname=postgres','postgres','',[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);
throw_unless((int)$pdo->query('SELECT inet_server_port()')->fetchColumn()===55441,RuntimeException::class,'Actual QA server required.');$pdo->exec('CREATE DATABASE "'.$db.'"');
$process=new Process(['C:/Program Files/PostgreSQL/17/bin/pg_restore.exe','-h','127.0.0.1','-p','55441','-U','postgres','-d',$db,'--exit-on-error','--no-owner','--no-privileges',$proof['dump_path']],null,['PGPASSWORD'=>'']);$process->setTimeout(180);$process->run();throw_unless($process->isSuccessful(),RuntimeException::class,'QA restoration failed.');
$check=new PDO('pgsql:host=127.0.0.1;port=55441;dbname='.$db,'postgres','',[PDO::ATTR_ERRMODE=>PDO::ERRMODE_EXCEPTION]);$check->exec('BEGIN READ ONLY');
foreach($proof['tables'] as $table=>$state){$rows=$check->query('SELECT row_to_json(t)::text FROM '.$table.' t ORDER BY row_to_json(t)::text')->fetchAll(PDO::FETCH_COLUMN);throw_unless(count($rows)===$state['count']&&hash('sha256',implode("\n",$rows))===$state['sha256'],RuntimeException::class,'Restored table differs.');}
throw_unless($check->query("SELECT * FROM pg_sequences WHERE schemaname NOT IN ('pg_catalog','information_schema') ORDER BY schemaname,sequencename")->fetchAll(PDO::FETCH_ASSOC)===$proof['sequences'],RuntimeException::class,'Restored sequences differ.');
$zip=new ZipArchive;throw_unless($zip->open($proof['files_path'])===true,RuntimeException::class,'Verified archive required.');$fileDirectory=$dir.'/'.$db.'-restored-files';mkdir($fileDirectory,0700,true);$fileCount=0;
for($i=0;$i<$zip->numFiles;$i++){$name=$zip->getNameIndex($i);throw_unless(is_string($name)&&!str_contains($name,':')&&!str_starts_with($name,'/')&&!array_intersect(explode('/',$name),['.','..']),RuntimeException::class,'Archive member refused.');$bytes=$zip->getFromIndex($i);throw_unless(is_string($bytes),RuntimeException::class,'Archive member unreadable.');$destination=$fileDirectory.'/'.$name;if(!is_dir(dirname($destination)))mkdir(dirname($destination),0700,true);file_put_contents($destination,$bytes);throw_unless(hash_file('sha256',$destination)===hash('sha256',$bytes),RuntimeException::class,'Restored member differs.');$fileCount++;}
throw_unless($fileCount===$proof['file_count'],RuntimeException::class,'File inventory differs.');
$runtime=json_decode($zip->getFromName('runtime/effective-runtime.json'),true,512,JSON_THROW_ON_ERROR);$key=$runtime['app_key'];$key=str_starts_with($key,'base64:')?base64_decode(substr($key,7),true):$key;$crypt=new Illuminate\Encryption\Encrypter($key,$runtime['cipher']);$mfa=0;
foreach($check->query('SELECT two_factor_secret FROM users WHERE two_factor_secret IS NOT NULL')->fetchAll(PDO::FETCH_COLUMN) as $secret){$plain=$crypt->decrypt($secret,false);throw_unless(is_string($plain)&&($plain!==''),RuntimeException::class,'Existing MFA envelope unreadable.');$mfa++;}$zip->close();$check->exec('ROLLBACK');
$mode=$argv[1]??'prepare-country';$receipt=['result'=>'restored_exact','at'=>gmdate('c'),'mode'=>$mode,'target'=>['database'=>$db,'port'=>55441],'source_backup'=>$link['path'],'dump_sha256'=>$proof['dump_sha256'],'tables'=>count($proof['tables']),'sequences'=>count($proof['sequences']),'files'=>$fileCount,'original_MFA_envelopes_readable'=>$mfa,'file_directory'=>$fileDirectory,'active_writes'=>0];
if($mode==='prepare-country'){
$env=file_get_contents($source.'/.env');foreach(['DB_DATABASE'=>$db,'PORTAL_ORGANIZATION_COUNTRY_ENABLED'=>'false'] as $name=>$value){$env=preg_replace('/^'.$name.'=.*$/m',$name.'='.$value,$env,1,$count);throw_unless($count===1,RuntimeException::class,'Unique QA setting required.');}file_put_contents($source.'/.env',$env);
$settings=json_decode(file_get_contents($source.'/stability-qa.json'),true,512,JSON_THROW_ON_ERROR);$settings['target']=$receipt['target'];file_put_contents($source.'/stability-qa.json',json_encode($settings,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));$p['qa_environment']=$settings;file_put_contents($dir.'/portal-stability-latest.json',json_encode($p,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
file_put_contents($dir.'/portal-stability-country-final-restore.json',json_encode($receipt,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));
}else{throw_unless($mode==='disaster-restore',RuntimeException::class,'Unknown QA rehearsal mode.');file_put_contents($dir.'/portal-stability-disaster-restore.json',json_encode($receipt,JSON_PRETTY_PRINT|JSON_THROW_ON_ERROR));}
echo json_encode($receipt).PHP_EOL;