$method, CURLOPT_RETURNTRANSFER=>true, CURLOPT_FOLLOWLOCATION=>false, CURLOPT_TIMEOUT=>15, CURLOPT_PROXY=>'', CURLOPT_USERPWD=>$user.':'.$password, CURLOPT_HTTPHEADER=>$headers, CURLOPT_HEADERFUNCTION=>function($c,$line) use (&$seen) { $parts=explode(':',$line,2); if(count($parts)===2) $seen[strtolower(trim($parts[0]))]=trim($parts[1]); return strlen($line); }]); if($method==='HEAD') curl_setopt($curl, CURLOPT_NOBODY, true); if($body!==null) curl_setopt($curl,CURLOPT_POSTFIELDS,$body); $result=curl_exec($curl); $status=curl_getinfo($curl,CURLINFO_RESPONSE_CODE); if($result===false) throw new Exception('Loopback request failed'); curl_close($curl); return [$status,$seen,$result]; } function check($condition,$name) { if(!$condition) throw new Exception('FAIL '.$name); echo 'PASS '.$name."\n"; } if (($argv[1]??'')==='revoked') { [$status,,$body]=request('GET','/remote.php/dav/files/alice/contract.txt'); echo 'Disabled-account HTTP status '.$status.'; exact file bytes returned: '.($body==='abcdefgh'?'yes':'no')."\n"; $disabledStatus=$status; $disabledBody=$body; echo 'Disabled-account response: '.substr(trim(strip_tags($body)),0,300)."\n"; [$status,,$body]=request('GET','/ocs/v2.php/cloud/user?format=json','bob','Synthetic-Cloud-B-Only-2026!',['OCS-APIRequest: true','Accept: application/json']); check($status===200 && json_decode($body,true)['ocs']['data']['id']==='bob','other account survives source-account revocation'); check(in_array($disabledStatus,[401,403,503],true) && $disabledBody!=='abcdefgh' && str_contains(strtolower($disabledBody),'disabled'),'disabled account loses authenticated file access with explicit disabled-account error'); exit(0); } [$status,,$body]=request('GET','/status.php'); $version=json_decode($body,true); check($status===200 && $version['installed']===true && $version['maintenance']===false && str_starts_with($version['version'],'29.'),'pinned Nextcloud29 readiness'); echo 'Version '.$version['version']."\n"; [$status,,$body]=request('GET','/ocs/v2.php/cloud/user?format=json','alice','Synthetic-Cloud-A-Only-2026!',['OCS-APIRequest: true','Accept: application/json']); check($status===200 && json_decode($body,true)['ocs']['data']['id']==='alice','authenticated OCS identity'); [$status]=request('PUT','/remote.php/dav/files/alice/contract.txt','alice','Synthetic-Cloud-A-Only-2026!',['Content-Type: text/plain'],'abcdefgh'); check($status===201 || $status===204,'disposable account file creation'); $xml=''; [$status,,$body]=request('PROPFIND','/remote.php/dav/files/alice/','alice','Synthetic-Cloud-A-Only-2026!',['Depth: 1','Content-Type: application/xml; charset=utf-8'],$xml); $document=new DOMDocument(); check($status===207 && $document->loadXML($body,LIBXML_NONET),'Depth1 DAV XML'); $xpath=new DOMXPath($document); $xpath->registerNamespace('d','DAV:'); $xpath->registerNamespace('oc','http://owncloud.org/ns'); $rows=$xpath->query('//d:response[d:href="/remote.php/dav/files/alice/contract.txt"]'); check($rows->length===1,'account-relative unique href'); $row=$rows->item(0); $etag=$xpath->evaluate('string(d:propstat[d:status="HTTP/1.1 200 OK"]/d:prop/d:getetag)',$row); check($etag!=='' && str_contains($xpath->evaluate('string(d:propstat/d:prop/oc:permissions)',$row),'G'),'successful DAV etag/read permission'); [$status,$headers]=request('HEAD','/remote.php/dav/files/alice/contract.txt'); check($status===200 && ($headers['etag']??null)===$etag && ($headers['content-length']??null)==='8','HEAD matches DAV revision and exact length'); [$status,$headers,$body]=request('GET','/remote.php/dav/files/alice/contract.txt','alice','Synthetic-Cloud-A-Only-2026!',['If-Match: '.$etag,'Range: bytes=2-5','Accept-Encoding: identity']); check($status===206 && ($headers['content-range']??null)==='bytes 2-5/8' && $body==='cdef','conditional exact range'); [$status,,$body]=request('GET','/remote.php/dav/files/alice/contract.txt','alice','Synthetic-Cloud-A-Only-2026!',['If-Match: '.$etag]); check($status===200 && $body==='abcdefgh','authenticated exact full bytes'); [$status]=request('GET','/remote.php/dav/files/alice/contract.txt','bob','Synthetic-Cloud-B-Only-2026!'); check(in_array($status,[403,404],true),'other account cannot read private file'); [$status]=request('GET','/remote.php/dav/files/alice/contract.txt','alice','wrong-synthetic-password'); check($status===401,'invalid credentials refused'); [$status]=request('GET','/remote.php/dav/files/alice/contract.txt','alice','Synthetic-Cloud-A-Only-2026!',['If-Match: "changed-revision"']); check($status===412,'changed revision refused'); echo "No File Browser, host port, external network or real account was used.\n";