<?php
declare(strict_types=1);

/**
 * DivisionDesk Browser Installer
 * Single-file bootstrap for users without SSH.
 * Installs through the local filesystem only. No FTP/FTPS/SFTP deployment capability is included.
 */

const DD_INSTALLER_API='2';
const DD_LICENSE_PREFLIGHT_URL='https://divisiondesk.com/api/license-preflight.php';

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_api_post(string $url,array $payload):array{
    $body=json_encode($payload,JSON_UNESCAPED_SLASHES);
    if($body===false)throw new RuntimeException('Could not encode licensing request.');
    $headers=['Accept: application/json','Content-Type: application/json','User-Agent: DivisionDesk Browser Installer/'.DD_INSTALLER_API];
    if(function_exists('curl_init')){
        $ch=curl_init($url);curl_setopt_array($ch,[CURLOPT_RETURNTRANSFER=>true,CURLOPT_POST=>true,CURLOPT_HTTPHEADER=>$headers,CURLOPT_POSTFIELDS=>$body,CURLOPT_TIMEOUT=>20,CURLOPT_CONNECTTIMEOUT=>10,CURLOPT_FOLLOWLOCATION=>false,CURLOPT_SSL_VERIFYPEER=>true,CURLOPT_SSL_VERIFYHOST=>2]);
        $raw=curl_exec($ch);$err=$raw===false?(string)curl_error($ch):'';$status=(int)curl_getinfo($ch,CURLINFO_RESPONSE_CODE);curl_close($ch);
    }else{
        $ctx=stream_context_create(['http'=>['method'=>'POST','timeout'=>20,'ignore_errors'=>true,'header'=>implode("\r\n",$headers)."\r\n",'content'=>$body],'ssl'=>['verify_peer'=>true,'verify_peer_name'=>true]]);$http_response_header=[];$raw=@file_get_contents($url,false,$ctx);$status=0;foreach($http_response_header as$line)if(preg_match('~^HTTP/\S+\s+(\d+)~',$line,$m))$status=(int)$m[1];$last=error_get_last();$err=$raw===false?(string)($last['message']??''):'';
    }
    if($raw===false)throw new RuntimeException('Could not validate the DivisionDesk license. '.trim($err));
    $d=json_decode($raw,true);if(!is_array($d))throw new RuntimeException('DivisionDesk Server returned invalid licensing data.');
    if($status>=400||empty($d['ok'])||empty($d['valid']))throw new RuntimeException((string)($d['error']??'The DivisionDesk license could not be validated.'));
    return$d;
}
function dd_license_preflight(string $key,string $domain,string $class='production'):array{
    $key=trim($key);$domain=trim($domain);
    if($key==='')throw new RuntimeException('Enter a DivisionDesk license key before installation.');
    if($domain==='')throw new RuntimeException('Enter the domain this DivisionDesk installation will use.');
    $d=dd_api_post(DD_LICENSE_PREFLIGHT_URL,['license_key'=>$key,'domain'=>$domain,'installation_class'=>$class]);
    $r=$d['release']??null;if(!is_array($r)||empty($r['version'])||empty($r['download_url'])||!preg_match('/^[a-f0-9]{64}$/i',(string)($r['sha256']??''))||(int)($r['package_size']??0)<1)throw new RuntimeException('DivisionDesk Server returned incomplete install authorization.');
    $out=['version'=>(string)$r['version'],'minimum_php'=>(string)($r['minimum_php']??'8.1'),'package_url'=>(string)$r['download_url'],'sha256'=>(string)$r['sha256'],'package_size'=>(int)$r['package_size'],'installer'=>(array)($r['installer']??[])];
    if(version_compare(PHP_VERSION,$out['minimum_php'],'<'))throw new RuntimeException('Current Core requires PHP '.$out['minimum_php'].' or newer.');
    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.'/setup.php?token='.rawurlencode($token);
}
$message='';$error='';$manifest=null;$localWritable=is_writable(__DIR__)||is_writable(dirname(__DIR__));

if($_SERVER['REQUEST_METHOD']==='POST'&&isset($_POST['install'])){
    $tmp='';
    try{
        $licenseKey=trim((string)($_POST['license_key']??''));
        $installationDomain=trim((string)($_POST['installation_domain']??''));
        $installationClass=strtolower(trim((string)($_POST['installation_class']??'production')));
        if(!in_array($installationClass,['production','staging','development','demo','review'],true))throw new RuntimeException('Choose a valid installation class.');
        $manifest=dd_license_preflight($licenseKey,$installationDomain,$installationClass);
        $tmp=dd_tempdir();
        $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{
            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.');
        $licenseDir=$extract.'/storage/setup';if(!is_dir($licenseDir)&&!mkdir($licenseDir,0775,true)&&!is_dir($licenseDir))throw new RuntimeException('Could not prepare licensing setup data.');
        if(file_put_contents($licenseDir.'/license-bootstrap.json',json_encode(['license_key'=>$licenseKey,'domain'=>$installationDomain,'installation_class'=>$installationClass,'validated_at'=>gmdate('c')],JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES),LOCK_EX)===false)throw new RuntimeException('Could not prepare licensing setup data.');
        @chmod($licenseDir.'/license-bootstrap.json',0600);
        $publicRoot=__DIR__;$appRoot=dirname(__DIR__);
        if(!is_writable($publicRoot)||!is_writable($appRoot))throw new RuntimeException('PHP must be able to write both the public web root and its parent application directory.');
        foreach(['app','config','database','themes','addons'] as $existing)if(file_exists($appRoot.'/'.$existing))throw new RuntimeException('The application directory already contains '.$existing.'. Browser fresh install refuses to overwrite an existing application.');
        foreach(scandir($extract)?:[] as $n){if($n==='.'||$n==='..'||$n==='public'||$n==='storage')continue;$src=$extract.'/'.$n;$dst=$appRoot.'/'.$n;if(is_dir($src))dd_copytree($src,$dst);elseif(!@copy($src,$dst))throw new RuntimeException('Could not deploy application file '.$n);}
        dd_copytree($extract.'/public',$publicRoot);
        $setupDir=$appRoot.'/storage/setup';if(!is_dir($setupDir)&&!mkdir($setupDir,0775,true)&&!is_dir($setupDir))throw new RuntimeException('Could not prepare setup storage.');
        if(file_put_contents($setupDir.'/license-bootstrap.json',json_encode(['license_key'=>$licenseKey,'domain'=>$installationDomain,'installation_class'=>$installationClass,'validated_at'=>gmdate('c')],JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES),LOCK_EX)===false)throw new RuntimeException('Could not prepare licensing setup data.');
        file_put_contents($setupDir.'/deployment-bootstrap.json',json_encode(['public_path'=>$publicRoot],JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES),LOCK_EX);
        $token=dd_prepare_setup($appRoot,__FILE__);header('Location: '.dd_setup_url($token));exit;
    }catch(Throwable $e){$error=$e->getMessage();}
    finally{if($tmp!=='')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 validates your license, downloads the protected Core release, verifies it, and installs through the local filesystem.</p>
<?php if($error):?><div class="error"><?=dd_e($error)?></div><?php endif?>
<?php if($manifest):?>
<div class="card"><h2>Authorized 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>Protected release</h2><p class="muted">Current Core release details and the one-use package URL are issued only after your license key and authorized domain are validated.</p></div><?php endif?>

<form method="post" enctype="multipart/form-data" class="card">
<h2>1. License & installation</h2>
<p class="muted">A valid license is required before a new DivisionDesk installation can proceed. Validation happens before Core is downloaded or copied.</p><div class="grid">
<label>DivisionDesk License Key<input name="license_key" value="<?=dd_e($_POST['license_key']??'')?>" required autocomplete="off"></label>
<label>Installation Domain<input name="installation_domain" value="<?=dd_e($_POST['installation_domain']??($_SERVER['HTTP_HOST']??''))?>" placeholder="example.org" required></label>
<label>Installation Class<select name="installation_class"><option value="production">Production</option><option value="staging">Staging</option><option value="development">Development</option><option value="demo">Demo</option><option value="review">Review</option></select></label>
</div>
<h2>2. Package source</h2>
<label class="method"><span><input type="radio" name="source" value="download" checked> Download and verify the protected current Core package after license validation</span></label>
<label class="method"><span><input type="radio" name="source" value="manual"> Manual ZIP upload fallback</span><input type="file" name="core_zip" accept=".zip,application/zip"></label>

<h2>3. Installation method</h2><div class="method"><strong>Local filesystem installation</strong><p class="muted">The directory containing this installer must be writable by PHP. FTP/FTPS/SFTP deployment has been permanently removed.</p></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><style>@keyframes ddspin{to{transform:rotate(360deg)}}</style><script>(()=>{
if(window.__divisionDeskFetchForms)return;window.__divisionDeskFetchForms=true;
const q=(s,r=document)=>r.querySelector(s),qa=(s,r=document)=>[...r.querySelectorAll(s)];
function toast(msg,type='ok'){let b=q('#dd-fetch-toast');if(!b){b=document.createElement('div');b.id='dd-fetch-toast';b.setAttribute('role','status');document.body.appendChild(b)}b.className='dd-fetch-toast '+type;b.textContent=msg;b.hidden=false;clearTimeout(b._t);b._t=setTimeout(()=>b.hidden=true,4200)}
function scripts(root){qa('script',root).forEach(o=>{const n=document.createElement('script');[...o.attributes].forEach(a=>n.setAttribute(a.name,a.value));n.textContent=o.textContent;o.replaceWith(n)})}
function swap(html,url){const d=new DOMParser().parseFromString(html,'text/html');document.title=d.title||document.title;let done=false;const a=q('.admin-shell'),b=q('.admin-shell',d);if(a&&b){a.replaceWith(b);scripts(b);done=true}if(!done){document.body.innerHTML=d.body.innerHTML;scripts(document.body)}try{const u=new URL(url,location.href);if(u.origin===location.origin)history.replaceState({},'',u.pathname+u.search+u.hash)}catch(e){}document.dispatchEvent(new CustomEvent('divisiondesk:content-updated'))}
async function download(r){const b=await r.blob(),cd=r.headers.get('Content-Disposition')||'',m=cd.match(/filename\*?=(?:UTF-8''|")?([^";]+)/i),name=m?decodeURIComponent(m[1].replace(/"/g,'')):'divisiondesk-download';const a=document.createElement('a');a.href=URL.createObjectURL(b);a.download=name;document.body.appendChild(a);a.click();a.remove();setTimeout(()=>URL.revokeObjectURL(a.href),3000)}
function formEndpoint(form){const raw=form?.getAttribute?.('action');return raw&&raw.trim()?new URL(raw,location.href).href:location.href}
async function submit(form,submitter){if(form.dataset.ddBusy==='1'||form.dataset.nativeSubmit==='1')return;const raw=form.getAttribute('onsubmit')||'',cm=raw.match(/confirm\((['"])(.*?)\1\)/);if((form.dataset.confirm||cm?.[2])&&!confirm(form.dataset.confirm||cm[2]))return;const fd=new FormData(form);if(submitter?.name)fd.set(submitter.name,submitter.value??'');form.dataset.ddBusy='1';const bs=qa('button,input[type=submit]',form),primary=submitter||bs[0];bs.forEach(x=>x.disabled=true);if(primary){primary.dataset.ddOldHtml=primary.innerHTML;primary.setAttribute('aria-busy','true');primary.innerHTML='<span style="display:inline-block;width:1em;height:1em;border:.16em solid currentColor;border-right-color:transparent;border-radius:50%;vertical-align:-.12em;animation:ddspin .7s linear infinite;margin-right:.45em"></span>Installing…'}try{const r=await fetch(formEndpoint(form),{method:'POST',body:fd,credentials:'same-origin',redirect:'follow',headers:{'X-Requested-With':'XMLHttpRequest','Accept':'application/json,text/html;q=0.9,*/*;q=0.8'}});if(/attachment/i.test(r.headers.get('Content-Disposition')||'')){await download(r);toast('Download ready.');return}const ct=(r.headers.get('Content-Type')||'').toLowerCase();if(ct.includes('application/json')){const d=await r.json();if(!r.ok||d.ok===false)throw new Error(d.error||d.message||'The action could not be completed.');if(d.redirect){const g=await fetch(d.redirect,{credentials:'same-origin',headers:{'X-Requested-With':'XMLHttpRequest'}});swap(await g.text(),g.url)}else if(d.html)swap(d.html,r.url);if(d.message)toast(d.message);return}const html=await r.text();if(!r.ok)throw new Error((new DOMParser().parseFromString(html,'text/html').body.textContent||'Request failed.').trim().slice(0,300));swap(html,r.url)}catch(e){toast(e.message||'Could not complete the action.','error')}finally{if(document.contains(form)){form.dataset.ddBusy='0';if(primary?.dataset.ddOldHtml){primary.innerHTML=primary.dataset.ddOldHtml;delete primary.dataset.ddOldHtml;primary.removeAttribute('aria-busy')}bs.forEach(x=>x.disabled=false)}}}
document.addEventListener('submit',e=>{const f=e.target.closest('form');if(!f||((f.method||'get').toLowerCase()!=='post')||f.dataset.nativeSubmit==='1')return;e.preventDefault();submit(f,e.submitter)},true);
window.DivisionDeskFetch={submit,toast,swap};
})();</script></body></html>
