<?php
declare(strict_types=1);

/**
 * DivisionDesk Browser Installer
 * Single-file bootstrap for users without SSH.
 * Credentials submitted for FTP/FTPS/SFTP are used only in-memory for this request and are never stored.
 */

const DD_INSTALLER_API='2';
const DD_MANIFEST_URL='https://divisiondesk.com/releases/core/latest.json';

session_start();
header('X-Content-Type-Options: nosniff');
header('Referrer-Policy: no-referrer');

function dd_e(mixed $v):string{return htmlspecialchars((string)$v,ENT_QUOTES,'UTF-8');}
function dd_fetch(string $url):string{
    if(function_exists('curl_init')){
        $ch=curl_init($url);curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_FOLLOWLOCATION=>true,CURLOPT_TIMEOUT=>30,CURLOPT_CONNECTTIMEOUT=>12,CURLOPT_FAILONERROR=>true,CURLOPT_USERAGENT=>'DivisionDesk Browser Installer/'.DD_INSTALLER_API]);
        $body=curl_exec($ch);$err=curl_error($ch);curl_close($ch);if($body===false)throw new RuntimeException('Download failed: '.$err);return $body;
    }
    $ctx=stream_context_create(['http'=>['timeout'=>30,'follow_location'=>1,'user_agent'=>'DivisionDesk Browser Installer/'.DD_INSTALLER_API],'ssl'=>['verify_peer'=>true,'verify_peer_name'=>true]]);
    $body=@file_get_contents($url,false,$ctx);if($body===false)throw new RuntimeException('Could not download '.$url);return $body;
}
function dd_manifest():array{
    $raw=dd_fetch(DD_MANIFEST_URL);$m=json_decode($raw,true);if(!is_array($m))throw new RuntimeException('Release manifest is invalid JSON.');
    $out=[
      'version'=>(string)($m['version']??''),'minimum_php'=>(string)($m['minimum_php']??'8.1'),
      'package_url'=>(string)($m['package_url']??($m['download']??'')),
      'sha256'=>(string)($m['sha256']??($m['checksum']['value']??'')),
      'package_size'=>(int)($m['package_size']??0),'installer'=>(array)($m['installer']??[])
    ];
    if($out['version']===''||!preg_match('#^https://#i',$out['package_url'])||!preg_match('/^[a-f0-9]{64}$/i',$out['sha256'])||$out['package_size']<1)throw new RuntimeException('Release manifest is incomplete.');
    if(version_compare(PHP_VERSION,$out['minimum_php'],'<'))throw new RuntimeException('Current Core requires PHP '.$out['minimum_php'].' or newer.');
    if(version_compare(DD_INSTALLER_API,(string)($out['installer']['minimum_api']??'1'),'<'))throw new RuntimeException('This browser installer is too old for the current Core release.');
    return $out;
}
function dd_tempdir():string{$d=sys_get_temp_dir().'/divisiondesk-install-'.bin2hex(random_bytes(6));if(!mkdir($d,0700,true)&&!is_dir($d))throw new RuntimeException('Could not create temporary directory.');return $d;}
function dd_rrmdir(string $dir):void{if(!is_dir($dir))return;foreach(scandir($dir)?:[] as $n){if($n==='.'||$n==='..')continue;$p=$dir.'/'.$n;if(is_dir($p)&&!is_link($p))dd_rrmdir($p);else @unlink($p);}@rmdir($dir);}
function dd_verify(string $zip,array $m):void{
    if(!is_file($zip))throw new RuntimeException('Core ZIP is missing.');
    if(filesize($zip)!==$m['package_size'])throw new RuntimeException('Core ZIP size does not match the release manifest.');
    $sha=hash_file('sha256',$zip);if(!hash_equals(strtolower($m['sha256']),strtolower($sha)))throw new RuntimeException('Core ZIP SHA-256 verification failed.');
}
function dd_extract(string $zip,string $dir):void{
    if(!class_exists('ZipArchive'))throw new RuntimeException('PHP ZipArchive is required for browser installation.');
    $z=new ZipArchive;if($z->open($zip)!==true)throw new RuntimeException('Could not open Core ZIP.');
    for($i=0;$i<$z->numFiles;$i++){
        $n=(string)$z->getNameIndex($i);
        if($n===''||str_contains($n,'../')||str_contains($n,'..\\')||str_starts_with($n,'/')||preg_match('/^[A-Za-z]:[\\\\\/]/',$n)){ $z->close(); throw new RuntimeException('Unsafe path in Core ZIP: '.$n); }
        $ops=0;$attr=0;if($z->getExternalAttributesIndex($i,$ops,$attr)&&$ops===ZipArchive::OPSYS_UNIX){$mode=($attr>>16)&0170000;if($mode===0120000){$z->close();throw new RuntimeException('Symlinks are not allowed in Core ZIP.');}}
    }
    if(!$z->extractTo($dir)){ $z->close(); throw new RuntimeException('Could not extract Core ZIP.'); }$z->close();
    if(!is_file($dir.'/manifest.json')||!is_file($dir.'/public/setup.php'))throw new RuntimeException('Core ZIP does not contain the expected DivisionDesk files.');
}
function dd_copytree(string $src,string $dst):void{
    if(!is_dir($dst)&&!mkdir($dst,0775,true)&&!is_dir($dst))throw new RuntimeException('Could not create destination directory.');
    foreach(scandir($src)?:[] as $n){if($n==='.'||$n==='..')continue;$s=$src.'/'.$n;$d=$dst.'/'.$n;if(is_dir($s)){dd_copytree($s,$d);}else{if(!@copy($s,$d))throw new RuntimeException('Could not write '.$d);@chmod($d,0664);}}
    @chmod($dst,0775);
}
function dd_prepare_setup(string $root,string $cleanupPath=''):string{
    $dir=$root.'/storage/setup';if(!is_dir($dir)&&!mkdir($dir,0775,true)&&!is_dir($dir))throw new RuntimeException('Could not prepare setup storage.');
    $token=bin2hex(random_bytes(24));if(file_put_contents($dir.'/token',$token,LOCK_EX)===false)throw new RuntimeException('Could not write setup token.');@chmod($dir.'/token',0600);
    if($cleanupPath!=='')@file_put_contents($dir.'/installer-cleanup.json',json_encode(['path'=>$cleanupPath,'created_at'=>date(DATE_ATOM)],JSON_UNESCAPED_SLASHES),LOCK_EX);
    return $token;
}
function dd_setup_url(string $token):string{
    $script=$_SERVER['SCRIPT_NAME']??'/divisiondesk-install.php';$base=rtrim(str_replace('\\','/',dirname($script)),'/');
    if($base==='/'||$base==='.')$base='';return $base.'/public/setup.php?token='.rawurlencode($token);
}
function dd_ftp_mkdir_recursive($ftp,string $path):void{
    $path=str_replace('\\','/',$path);$parts=array_values(array_filter(explode('/',$path),'strlen'));$prefix=str_starts_with($path,'/')?'/':'';
    foreach($parts as $part){$prefix.=($prefix==='/'?'':'/').$part;if(!@ftp_chdir($ftp,$prefix)){if(!@ftp_mkdir($ftp,$prefix))throw new RuntimeException('Could not create FTP directory '.$prefix);}}
}
function dd_ftp_upload_tree($ftp,string $src,string $remote):void{
    dd_ftp_mkdir_recursive($ftp,$remote);
    foreach(scandir($src)?:[] as $n){if($n==='.'||$n==='..')continue;$local=$src.'/'.$n;$target=rtrim($remote,'/').'/'.$n;if(is_dir($local))dd_ftp_upload_tree($ftp,$local,$target);else if(!ftp_put($ftp,$target,$local,FTP_BINARY))throw new RuntimeException('FTP upload failed for '.$target);}
}
function dd_sftp_upload_tree($sftp,string $src,string $remote):void{
    $uri='ssh2.sftp://'.intval($sftp);$parts=array_values(array_filter(explode('/',str_replace('\\','/',$remote)),'strlen'));$cur='';
    foreach($parts as $part){$cur.='/'.$part;if(!@ssh2_sftp_stat($sftp,$cur)&&!@ssh2_sftp_mkdir($sftp,$cur,0775,true))throw new RuntimeException('Could not create SFTP directory '.$cur);}
    foreach(scandir($src)?:[] as $n){if($n==='.'||$n==='..')continue;$local=$src.'/'.$n;$target=rtrim($remote,'/').'/'.$n;if(is_dir($local))dd_sftp_upload_tree($sftp,$local,$target);else{$in=fopen($local,'rb');$out=@fopen($uri.$target,'wb');if(!$in||!$out)throw new RuntimeException('SFTP upload failed for '.$target);stream_copy_to_stream($in,$out);fclose($in);fclose($out);}}
}

$message='';$error='';$manifest=null;$localWritable=is_writable(__DIR__)||is_writable(dirname(__DIR__));
try{$manifest=dd_manifest();}catch(Throwable $e){$error=$e->getMessage();}

if($_SERVER['REQUEST_METHOD']==='POST'&&isset($_POST['install'])){
    $tmp=dd_tempdir();
    try{
        $zip=$tmp.'/core.zip';
        $source=$_POST['source']??'download';
        if($source==='manual'){
            if(empty($_FILES['core_zip']['tmp_name'])||!is_uploaded_file($_FILES['core_zip']['tmp_name']))throw new RuntimeException('Choose the current DivisionDesk Core ZIP.');
            if(!move_uploaded_file($_FILES['core_zip']['tmp_name'],$zip))throw new RuntimeException('Could not accept uploaded ZIP.');
            if($manifest){dd_verify($zip,$manifest);}
            else{
                $manualSha=strtolower(trim((string)($_POST['manual_sha256']??'')));
                if(!preg_match('/^[a-f0-9]{64}$/',$manualSha))throw new RuntimeException('When the release manifest is unavailable, enter the official 64-character SHA-256 checksum supplied by divisiondesk.com.');
                if(!hash_equals($manualSha,strtolower(hash_file('sha256',$zip))))throw new RuntimeException('Manual ZIP SHA-256 verification failed.');
            }
        }else{
            if(!$manifest)throw new RuntimeException('The DivisionDesk release manifest is unavailable. Use Manual ZIP with the official SHA-256 checksum.');
            file_put_contents($zip,dd_fetch($manifest['package_url']));dd_verify($zip,$manifest);
        }
        $extract=$tmp.'/core';mkdir($extract,0700);dd_extract($zip,$extract);
        $internal=json_decode((string)file_get_contents($extract.'/manifest.json'),true)?:[];if(($internal['slug']??'')!=='divisiondesk-core')throw new RuntimeException('Uploaded ZIP is not a DivisionDesk Core package.');if(version_compare(PHP_VERSION,(string)($internal['minimum_php']??'8.1'),'<'))throw new RuntimeException('This Core package requires PHP '.($internal['minimum_php']??'8.1').' or newer.');
        $method=$_POST['method']??'local';
        if($method==='local'){
            if(!is_writable(__DIR__))throw new RuntimeException('PHP cannot write to this directory. Choose FTP/FTPS/SFTP or Manual ZIP with a writable destination.');
            dd_copytree($extract,__DIR__);$token=dd_prepare_setup(__DIR__,__FILE__);header('Location: '.dd_setup_url($token));exit;
        }
        $remote=trim($_POST['remote_path']??'');$host=trim($_POST['host']??'');$user=(string)($_POST['username']??'');$pass=(string)($_POST['password']??'');
        if($remote===''||$host===''||$user==='')throw new RuntimeException('Host, username and remote path are required for remote installation.');

        // Create setup token inside extracted files before transfer.
        $token=dd_prepare_setup($extract,'');
        if($method==='ftp'||$method==='ftps'){
            if(!function_exists('ftp_connect'))throw new RuntimeException('PHP FTP extension is unavailable.');
            $ftp=$method==='ftps'&&function_exists('ftp_ssl_connect')?@ftp_ssl_connect($host,(int)($_POST['port']??21),15):@ftp_connect($host,(int)($_POST['port']??21),15);
            if(!$ftp||!@ftp_login($ftp,$user,$pass))throw new RuntimeException(strtoupper($method).' login failed.');
            ftp_pasv($ftp,true);dd_ftp_upload_tree($ftp,$extract,$remote);
            // Self-disable without retaining credentials.
            $remoteInstaller=rtrim($remote,'/').'/'.basename(__FILE__);@ftp_rename($ftp,$remoteInstaller,$remoteInstaller.'.disabled');@ftp_close($ftp);
        }elseif($method==='sftp'){
            if(!function_exists('ssh2_connect'))throw new RuntimeException('PHP ssh2 extension is unavailable for SFTP.');
            $conn=@ssh2_connect($host,(int)($_POST['port']??22));if(!$conn||!@ssh2_auth_password($conn,$user,$pass))throw new RuntimeException('SFTP login failed.');
            $sftp=ssh2_sftp($conn);if(!$sftp)throw new RuntimeException('Could not initialize SFTP.');dd_sftp_upload_tree($sftp,$extract,$remote);
            @ssh2_sftp_rename($sftp,rtrim($remote,'/').'/'.basename(__FILE__),rtrim($remote,'/').'/'.basename(__FILE__).'.disabled');
        }else throw new RuntimeException('Unknown installation method.');
        header('Location: '.dd_setup_url($token));exit;
    }catch(Throwable $e){$error=$e->getMessage();}
    finally{dd_rrmdir($tmp);}
}
?><!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Install DivisionDesk</title>
<style>
body{margin:0;background:#f4f6f9;color:#172033;font:16px system-ui,-apple-system,Segoe UI,sans-serif}.wrap{max-width:900px;margin:45px auto;padding:20px}.brand{display:flex;align-items:center;gap:12px}.logo{width:48px;height:48px;border-radius:12px;background:#c7a24d;color:#101827;display:grid;place-items:center;font-weight:900}.card{background:white;border:1px solid #dde3eb;border-radius:16px;padding:24px;margin:18px 0;box-shadow:0 8px 26px rgba(23,32,51,.06)}h1{font-size:2.25rem;margin:.2em 0}h2{margin-top:0}.grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:13px}label{display:grid;gap:5px;font-weight:650}input,select{padding:11px;border:1px solid #cdd5df;border-radius:8px;font:inherit}button{padding:12px 18px;border:0;border-radius:9px;background:#1e3160;color:white;font-weight:750;cursor:pointer}.ok{color:#28653a}.warn{color:#946000}.error{background:#fff1f2;color:#92293a;border:1px solid #efc6cd;padding:13px;border-radius:9px}.muted{color:#667085}.method{border:1px solid #dfe5ed;border-radius:10px;padding:12px;margin:8px 0}.method input{margin-right:8px}.remote{margin-top:14px}@media(max-width:700px){.grid{grid-template-columns:1fr}}
</style></head><body><main class="wrap">
<div class="brand"><div class="logo">DD</div><div><strong>DivisionDesk</strong><div class="muted">Browser Installer</div></div></div>
<h1>Install DivisionDesk</h1>
<p class="muted">This installer uses credentials only for the current request. FTP/FTPS/SFTP credentials are never written to disk or saved in DivisionDesk.</p>
<?php if($error):?><div class="error"><?=dd_e($error)?></div><?php endif?>
<?php if($manifest):?>
<div class="card"><h2>Current Core Release</h2><div class="grid">
<div><strong>Version</strong><br><?=dd_e($manifest['version'])?></div>
<div><strong>Minimum PHP</strong><br><?=dd_e($manifest['minimum_php'])?></div>
<div><strong>Package size</strong><br><?=number_format($manifest['package_size'])?> bytes</div>
<div><strong>SHA-256</strong><br><code><?=dd_e(substr($manifest['sha256'],0,20))?>…</code></div>
</div><p class="<?=$localWritable?'ok':'warn'?>"><?=$localWritable?'✓ PHP appears able to write to this directory. Local install is recommended.':'⚠ PHP does not appear able to write here. Use FTP/FTPS/SFTP fallback.'?></p></div>
<?php else:?><div class="card"><h2>Release manifest unavailable</h2><p class="warn">Automatic download is unavailable. You can still use Manual ZIP if you enter the official SHA-256 checksum shown on divisiondesk.com.</p></div><?php endif?>

<form method="post" enctype="multipart/form-data" class="card">
<h2>1. Package source</h2>
<label class="method"><span><input type="radio" name="source" value="download" <?=$manifest?'checked':'disabled'?>> Download and verify the current Core directly from divisiondesk.com</span></label>
<label class="method"><span><input type="radio" name="source" value="manual" <?=!$manifest?'checked':''?>> Manual ZIP upload fallback</span><input type="file" name="core_zip" accept=".zip,application/zip"><input name="manual_sha256" placeholder="Official SHA-256 (required only if manifest is unavailable)" autocomplete="off"></label>

<h2>2. Installation method</h2>
<label class="method"><span><input type="radio" name="method" value="local" <?=$localWritable?'checked':''?>> Local filesystem install <?=$localWritable?'(recommended)':''?></span></label>
<label class="method"><span><input type="radio" name="method" value="ftp" <?=!$localWritable?'checked':''?>> FTP fallback</span></label>
<label class="method"><span><input type="radio" name="method" value="ftps"> FTPS fallback</span></label>
<label class="method"><span><input type="radio" name="method" value="sftp"> SFTP fallback</span></label>

<div class="remote"><h3>FTP / FTPS / SFTP credentials</h3><p class="muted">Ignored for local installation.</p><div class="grid">
<label>Host<input name="host" autocomplete="off"></label><label>Port<input type="number" name="port" placeholder="21 or 22"></label>
<label>Username<input name="username" autocomplete="off"></label><label>Password<input type="password" name="password" autocomplete="new-password"></label>
<label style="grid-column:1/-1">Remote installation path<input name="remote_path" placeholder="/public_html or /www/site"></label>
</div></div>
<input type="hidden" name="install" value="1"><p><button>Install DivisionDesk</button></p>
</form>
<div class="card"><h2>What happens next?</h2><p>The Core package is verified against the official release manifest before extraction. After files are installed, you are sent to DivisionDesk Website Setup to choose the database, create the administrator, and finish the site. The bootstrap installer disables/removes itself when practical.</p></div>
</main></body></html>
