<?php require_once __DIR__ . '/Page.php'; class ClonePage implements Page { private $git; private $subPath; public function __construct( Git $git, string $subPath ) { $this->git = $git; $this->subPath = $subPath; } public function render() { if( $this->subPath === '' ) { $this->redirectBrowser(); } elseif( str_ends_with( $this->subPath, 'info/refs' ) ) { $this->renderInfoRefs(); } elseif( str_ends_with( $this->subPath, 'git-upload-pack' ) ) { $this->handleUploadPack(); } elseif( str_ends_with( $this->subPath, 'git-receive-pack' ) ) { http_response_code( 403 ); echo "Read-only repository."; } elseif( $this->subPath === 'HEAD' ) { $this->serve( 'HEAD', 'text/plain' ); } elseif( strpos( $this->subPath, 'objects/' ) === 0 ) { $this->serve( $this->subPath, 'application/x-git-object' ); } else { http_response_code( 404 ); echo "Not Found"; } exit; } private function redirectBrowser(): void { $url = str_replace( '.git', '', $_SERVER['REQUEST_URI'] ); header( "Location: $url" ); } private function renderInfoRefs(): void { $service = $_GET['service'] ?? ''; if( $service === 'git-upload-pack' ) { header( 'Content-Type: application/x-git-upload-pack-advertisement' ); header( 'Cache-Control: no-cache' ); $this->packetWrite( "# service=git-upload-pack\n" ); $this->packetFlush(); $refs = []; $this->git->eachRef( function( $ref, $sha ) use ( &$refs ) { $refs[] = ['ref' => $ref, 'sha' => $sha]; } ); $caps = "multi_ack_detailed thin-pack side-band side-band-64k " . "ofs-delta shallow no-progress include-tag"; if( empty( $refs ) ) { $this->packetWrite( "0000000000000000000000000000000000000000 capabilities^{}\0" . $caps . "\n" ); } else { $this->packetWrite( $refs[0]['sha'] . " " . $refs[0]['ref'] . "\0" . $caps . "\n" ); for( $i = 1; $i < count( $refs ); $i++ ) { $this->packetWrite( $refs[$i]['sha'] . " " . $refs[$i]['ref'] . "\n" ); } } $this->packetFlush(); } else { header( 'Content-Type: text/plain' ); if( !$this->git->streamRaw( 'info/refs' ) ) { $this->git->eachRef( function( $ref, $sha ) { echo "$sha\t$ref\n"; } ); } } } private function handleUploadPack(): void { header( 'Content-Type: application/x-git-upload-pack-result' ); header( 'Cache-Control: no-cache' ); $input = file_get_contents( 'php://input' ); $wants = []; $haves = []; $offset = 0; while( $offset < strlen( $input ) ) { $line = $this->readPacketLine( $input, $offset ); if( $line === null || $line === 'done' ) { break; } if( $line === '' ) { continue; } $trim = trim( $line ); if( strpos( $trim, 'want ' ) === 0 ) { $wants[] = explode( ' ', $trim )[1]; } elseif( strpos( $trim, 'have ' ) === 0 ) { $haves[] = explode( ' ', $trim )[1]; } } if( $wants ) { $this->packetWrite( "NAK\n" ); $objects = $this->git->collectObjects( $wants, $haves ); $pack = $this->git->generatePackfile( $objects ); $this->sendSidebandData( 1, $pack ); } $this->packetFlush(); } private function sendSidebandData( int $band, string $data ): void { $chunkSize = 65000; $len = strlen( $data ); for( $offset = 0; $offset < $len; $offset += $chunkSize ) { $chunk = substr( $data, $offset, $chunkSize ); $this->packetWrite( chr( $band ) . $chunk ); } } private function readPacketLine( string $input, int &$offset ): ?string { $line = null; if( $offset + 4 <= strlen( $input ) ) { $lenHex = substr( $input, $offset, 4 ); if( ctype_xdigit( $lenHex ) ) { $len = hexdec( $lenHex ); $offset += 4; $valid = $len >= 4 && $offset + ( $len - 4 ) <= strlen( $input ); $line = ($len === 0) ? '' : ($valid ? substr( $input, $offset, $len - 4 ) : null); $offset += ($len >= 4) ? ($len - 4) : 0; } } return $line; } private function serve( string $path, string $contentType ): void { header( 'Content-Type: ' . $contentType ); if( !$this->git->streamRaw( $path ) ) { http_response_code( 404 ); echo "Missing: $path"; } } private function packetWrite( string $data ): void { printf( "%04x%s", strlen( $data ) + 4, $data ); } private function packetFlush(): void { echo "0000"; } }