<?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(): void { $path = $this->subPath; if( $path === '' ) { $this->redirectBrowser(); } elseif( \str_ends_with( $path, 'info/refs' ) ) { $this->renderInfoRefs(); } elseif( \str_ends_with( $path, 'git-upload-pack' ) ) { $this->handleUploadPack(); } elseif( \str_ends_with( $path, 'git-receive-pack' ) ) { \http_response_code( 403 ); echo "Read-only repository."; } elseif( $path === 'HEAD' ) { $this->serve( 'HEAD', 'text/plain' ); } elseif( \strpos( $path, 'objects/' ) === 0 ) { $this->serve( $path, 'application/x-git-object' ); } else { \http_response_code( 404 ); echo "Not Found"; } exit; } private function redirectBrowser(): void { \header( "Location: " . \str_replace( '.git', '', $_SERVER['REQUEST_URI'] ) ); } private function renderInfoRefs(): void { if( ( $_GET['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 { \set_time_limit( 0 ); \header( 'Content-Type: application/x-git-upload-pack-result' ); \header( 'Cache-Control: no-cache' ); $wants = []; $haves = []; $handle = \fopen( 'php://input', 'rb' ); if( $handle ) { if( isset( $_SERVER['HTTP_CONTENT_ENCODING'] ) && $_SERVER['HTTP_CONTENT_ENCODING'] === 'gzip' ) { \stream_filter_append( $handle, 'zlib.inflate', \STREAM_FILTER_READ, [ 'window' => 31 ] ); } while( !\feof( $handle ) ) { $lenHex = \fread( $handle, 4 ); $len = \strlen( $lenHex ) === 4 ? \hexdec( $lenHex ) : 0; if( $len === 0 ) { break; } if( $len > 4 ) { $trim = \trim( \fread( $handle, $len - 4 ) ); if( \strpos( $trim, 'want ' ) === 0 ) { $wants[] = \explode( ' ', $trim )[1]; } elseif( \strpos( $trim, 'have ' ) === 0 ) { $haves[] = \explode( ' ', $trim )[1]; } elseif( $trim === 'done' ) { break; } } } \fclose( $handle ); } if( !empty( $wants ) ) { $this->packetWrite( "NAK\n" ); $objects = $this->git->collectObjects( $wants, $haves ); $lastHb = \time(); foreach( $this->git->generatePackfile( $objects ) as $chunk ) { if( $chunk !== '' ) { $this->sendSidebandData( 1, $chunk ); } $now = \time(); if( $now - $lastHb >= 5 ) { $this->sendSidebandData( 2, "\r" ); $lastHb = $now; } } } $this->packetFlush(); } private function sendSidebandData( int $band, string $data ): void { $chunkSize = 65515; $len = \strlen( $data ); for( $offset = 0; $offset < $len; $offset += $chunkSize ) { $this->packetWrite( \chr( $band ) . \substr( $data, $offset, $chunkSize ) ); } } 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"; } }