38 lines
7.0 KiB
PHP
38 lines
7.0 KiB
PHP
<?php
|
|
declare(strict_types=1);
|
|
require __DIR__.'/backend.php';
|
|
use PrelandAdmin\{Config,Jobs,Sets,IndpicSetStorage,Auth,Problem};
|
|
function rejects(callable $call,string $message): void {try{$call();}catch(\Throwable $e){check(true,$message);return;}throw new RuntimeException('Accepted invalid input: '.$message);}
|
|
rejects(static fn()=>Config::validate(Config::decode('{"utm":{"reg":{"default":"1","weights":{"1":100}},"registration":{"default":"before","weights":{"before":100}}}}')),'old/new UTM alias conflict rejected');
|
|
check(Config::validate(Config::decode('{"utm":{"theme":{"default":"3","weights":{}}}}'))===[],'manual numeric theme variant accepted');
|
|
rejects(static fn()=>Config::validate(Config::decode('{"utm":{"degr":{"default":"1","weights":{"1":-1}}}}')),'negative UTM weight rejected');
|
|
check(Config::validate(Config::decode('{"utm":{"indpic":{"default":"1","weights":{"1":0}}}}'))===[],'config editor does not require a physical set');
|
|
check(Config::validate(Config::decode('{"utm":{"theme":{"default":"dark","weights":{"dark":0,"light":0}}}}'))===[],'legacy semantic enum remains valid without rewriting');
|
|
$current=$transport->readConfig();$job=$jobs->create('apply',['targets'=>[['id'=>'test','version'=>$current['version']]],'changes'=>[['path'=>['custom','empty'],'value'=>(object)[]]]]);$jobs->run($job['id']);
|
|
check($transport->readConfig()['config']->custom->empty instanceof stdClass,'empty JSON object survives persisted job');
|
|
$state=$transport->readConfig();$id=$jobs->create('apply',['targets'=>[['id'=>'test','version'=>$state['version']]],'changes'=>[['path'=>['host'],'value'=>'resumed.example']]])['id'];
|
|
$record=$store->read('jobs/'.$id.'.json');$record['status']='running';$record['results'][0]['status']='running';$desired=Config::encode(Config::patch($state['config'],[['path'=>['host'],'value'=>'resumed.example']]));$record['results'][0]['desired_hash']=hash('sha256',$desired);$store->write('jobs/'.$id.'.json',$record);$transport->writeConfig($state['version'],$desired);(new Jobs($store))->run($id);
|
|
check($jobs->get($id)['results'][0]['status']==='success','restart after write reconciles desired hash without overwrite');
|
|
$race=$transport->readConfig();$processes=[];foreach(['race-a.example','race-b.example'] as $host){$pid=pcntl_fork();if($pid===0){try{$transport->writeConfig($race['version'],Config::encode(Config::patch($race['config'],[['path'=>['host'],'value'=>$host]])));exit(0);}catch(PrelandAdmin\Conflict){exit(7);}}$processes[]=$pid;}$codes=[];foreach($processes as $pid){pcntl_waitpid($pid,$status);$codes[]=pcntl_wexitstatus($status);}sort($codes);check($codes===[0,7],'two simultaneous writes produce one success and one conflict');
|
|
$sets=new Sets($store);$zipPath=$root.'/bad.zip';$zip=new ZipArchive();$zip->open($zipPath,ZipArchive::CREATE);$zip->addFromString('1/../../evil','bad');$zip->close();rejects(fn()=>$sets->prepareZip($zipPath,1),'ZIP traversal rejected');
|
|
$zipPath=$root.'/valid.zip';$zip=new ZipArchive();$zip->open($zipPath,ZipArchive::CREATE);$cache=[];
|
|
foreach(IndpicSetStorage::expectedDimensions() as $name=>$dimension){$ext=pathinfo($name,PATHINFO_EXTENSION);$key=implode('x',$dimension).'.'.$ext;if(!isset($cache[$key])){$image=new Imagick();$image->newImage($dimension['width'],$dimension['height'],'#345678');$image->setImageFormat($ext);$cache[$key]=$image->getImagesBlob();$image->clear();}$zip->addFromString('1/'.$name,$cache[$key]);}$zip->close();
|
|
$prepared=$sets->prepareZip($zipPath,1);check(is_dir($prepared.'/1'),'90 images with CPanel sizes accepted');
|
|
$missing=$root.'/missing.zip';copy($zipPath,$missing);$z=new ZipArchive();$z->open($missing);$z->deleteName('1/01_desk_1-source.jpg');$z->close();rejects(fn()=>$sets->prepareZip($missing,1),'ZIP missing required image rejected');
|
|
$extra=$root.'/extra.zip';copy($zipPath,$extra);$z=new ZipArchive();$z->open($extra);$z->addFromString('1/extra.jpg','bad');$z->close();rejects(fn()=>$sets->prepareZip($extra,1),'ZIP extra image rejected');
|
|
$wrong=$root.'/wrong-size.zip';copy($zipPath,$wrong);$z=new ZipArchive();$z->open($wrong);$image=new Imagick();$image->newImage(10,10,'red');$image->setImageFormat('jpg');$z->addFromString('1/01_desk_1-source.jpg',$image->getImagesBlob());$image->clear();$z->close();rejects(fn()=>$sets->prepareZip($wrong,1),'ZIP incorrect dimensions rejected');
|
|
$oversize=$root.'/oversize.zip';$sparse=fopen($oversize,'w');ftruncate($sparse,67108865);fclose($sparse);rejects(fn()=>$sets->prepareZip($oversize,1),'ZIP compressed upload limit enforced before extraction');
|
|
function rawZip(array $entries): string {$local='';$central='';foreach($entries as [$name,$data,$declared]){$offset=strlen($local);$size=strlen($data);$crc=crc32($data);$local.=pack('VvvvvvVVVvv',0x04034b50,20,0,0,0,0,$crc,$size,$declared,strlen($name),0).$name.$data;$central.=pack('VvvvvvvVVVvvvvvVV',0x02014b50,20,20,0,0,0,0,$crc,$size,$declared,strlen($name),0,0,0,0,0,$offset).$name;}return $local.$central.pack('VvvvvVVv',0x06054b50,0,0,count($entries),count($entries),strlen($central),strlen($local),0);}
|
|
$duplicate=$root.'/duplicate.zip';file_put_contents($duplicate,rawZip([['1/01_desk_1-source.jpg','a',1],['1/01_desk_1-source.jpg','b',1]]));rejects(fn()=>$sets->prepareZip($duplicate,1),'duplicate ZIP entry rejected');
|
|
$inflated=$root.'/inflated.zip';file_put_contents($inflated,rawZip([['1/01_desk_1-source.jpg','a',8388609]]));rejects(fn()=>$sets->prepareZip($inflated,1),'declared ZIP expansion limit rejected before image decoding');
|
|
$result=$sets->upload($target,1,$prepared,'Тестовый набор');check(str_contains($result['message'],'90'),'local set publication revalidates delivered files');
|
|
$sets->check($target,1);check(is_file($root.'/site/1/01_desk_1-source.jpg'),'set directory published');
|
|
check((fileperms($root.'/site/1')&0777)===0755,'published set directory is readable by independent web-server UID');
|
|
rejects(fn()=>Config::validate(Config::decode('{"utm":{"indpic":{"default":"2","weights":{"1":100}}}}')),'default without weight variant rejected instead of runtime fallback');
|
|
$kept=hash_file('sha256',$root.'/site/1/01_desk_1-source.jpg');$badFiles=[];foreach(array_keys(IndpicSetStorage::expectedDimensions()) as $filename)$badFiles[$filename]=['data'=>base64_encode(file_get_contents($prepared.'/1/'.$filename)),'sha256'=>str_repeat('0',64)];
|
|
rejects(fn()=>$transport->call(['op'=>'set-write','number'=>1,'files'=>$badFiles,'expected_manifest'=>$sets->manifest($target,1)]),'delivery hash mismatch rejects activation');check(hash_file('sha256',$root.'/site/1/01_desk_1-source.jpg')===$kept,'failed set publication preserves previous working image');
|
|
// Настоящий формат должен совпадать с расширением, даже если размеры верны.
|
|
$jpg=$prepared.'/1/01_desk_1-source.jpg';$image=new Imagick();$image->newImage(1696,920,'red');$image->setImageFormat('png');file_put_contents($jpg,$image->getImagesBlob());$image->clear();
|
|
rejects(fn()=>(new IndpicSetStorage($prepared))->validateDirectory(1),'extension and decoded image format mismatch rejected');
|
|
echo "Security contract complete\n";
|