| Author | Dave Jarvis <email> |
|---|---|
| Date | 2026-02-12 12:49:48 GMT-0800 |
| Commit | f9319e4ff763e04c0c501dd88e89b3b0a3f80316 |
| Parent | 5b075f6 |
| require_once __DIR__ . '/pages/RawPage.php'; | ||
| require_once __DIR__ . '/pages/TagsPage.php'; | ||
| +require_once __DIR__ . '/pages/ClonePage.php'; | ||
| class Router { | ||
| - private $repositories = []; | ||
| + private $repos = []; | ||
| private $git; | ||
| public function route(): Page { | ||
| $reqRepo = $_GET['repo'] ?? ''; | ||
| - $action = $_GET['action'] ?? 'file'; | ||
| - $hash = $this->sanitizePath( $_GET['hash'] ?? '' ); | ||
| + $action = $_GET['action'] ?? 'file'; | ||
| + $hash = $this->sanitize( $_GET['hash'] ?? '' ); | ||
| $currRepo = null; | ||
| - $decoded = urldecode( $reqRepo ); | ||
| + $subPath = ''; | ||
| + $decoded = urldecode( $reqRepo ); | ||
| foreach( $this->repos as $repo ) { | ||
| - if( $repo['safe_name'] === $reqRepo || | ||
| - $repo['name'] === $decoded ) { | ||
| + if( $repo['safe_name'] === $reqRepo || $repo['name'] === $decoded ) { | ||
| + $currRepo = $repo; | ||
| + break; | ||
| + } | ||
| + | ||
| + $prefix = $repo['safe_name'] . '/'; | ||
| + | ||
| + if( strpos( $reqRepo, $prefix ) === 0 ) { | ||
| $currRepo = $repo; | ||
| + $subPath = substr( $reqRepo, strlen( $prefix ) ); | ||
| + $action = 'clone'; | ||
| break; | ||
| } | ||
| } | ||
| if( $currRepo ) { | ||
| $this->git->setRepository( $currRepo['path'] ); | ||
| } | ||
| $routes = [ | ||
| - 'home' => fn() => new HomePage( $this->repos, $this->git ), | ||
| - 'file' => fn() => new FilePage( $this->repos, $currRepo, $this->git, $hash ), | ||
| - 'raw' => fn() => new RawPage( $this->git, $hash ), | ||
| - 'commit' => fn() => new DiffPage( $this->repos, $currRepo, $this->git, $hash ), | ||
| + 'home' => fn() => new HomePage( $this->repos, $this->git ), | ||
| + 'file' => fn() => new FilePage( $this->repos, $currRepo, $this->git, $hash ), | ||
| + 'raw' => fn() => new RawPage( $this->git, $hash ), | ||
| + 'commit' => fn() => new DiffPage( $this->repos, $currRepo, $this->git, $hash ), | ||
| 'commits' => fn() => new CommitsPage( $this->repos, $currRepo, $this->git, $hash ), | ||
| - 'tags' => fn() => new TagsPage( $this->repos, $currRepo, $this->git ), | ||
| + 'tags' => fn() => new TagsPage( $this->repos, $currRepo, $this->git ), | ||
| + 'clone' => fn() => new ClonePage( $this->git, $subPath ), | ||
| ]; | ||
| $action = !$currRepo ? 'home' : $action; | ||
| return ($routes[$action] ?? $routes['file'])(); | ||
| } | ||
| - private function sanitizePath( $path ) { | ||
| + private function sanitize( $path ) { | ||
| $path = str_replace( [ '..', '\\', "\0" ], [ '', '/', '' ], $path ); | ||
| -<?php | ||
| -require_once __DIR__ . '/File.php'; | ||
| -require_once __DIR__ . '/render/FileRenderer.php'; | ||
| -require_once __DIR__ . '/RepositoryList.php'; | ||
| - | ||
| -require_once __DIR__ . '/pages/BasePage.php'; | ||
| -require_once __DIR__ . '/pages/HomePage.php'; | ||
| -require_once __DIR__ . '/pages/CommitsPage.php'; | ||
| -require_once __DIR__ . '/pages/FilePage.php'; | ||
| -require_once __DIR__ . '/pages/RawPage.php'; | ||
| return isset( $parts[1] ) ? (int)$parts[1] : 0; | ||
| } | ||
| + | ||
| + public function streamRaw( string $subPath ): bool { | ||
| + if( strpos( $subPath, '..' ) !== false ) { | ||
| + return false; | ||
| + } | ||
| + | ||
| + $fullPath = "{$this->repoPath}/$subPath"; | ||
| + | ||
| + if( !file_exists( $fullPath ) ) { | ||
| + return false; | ||
| + } | ||
| + | ||
| + $realPath = realpath( $fullPath ); | ||
| + $repoReal = realpath( $this->repoPath ); | ||
| + | ||
| + if( !$realPath || strpos( $realPath, $repoReal ) !== 0 ) { | ||
| + return false; | ||
| + } | ||
| + | ||
| + readfile( $fullPath ); | ||
| + return true; | ||
| + } | ||
| + | ||
| + public function eachRef( callable $callback ): void { | ||
| + $head = $this->resolve( 'HEAD' ); | ||
| + | ||
| + if( $head !== '' ) { | ||
| + $callback( 'HEAD', $head ); | ||
| + } | ||
| + | ||
| + $this->refs->scanRefs( 'refs/heads', function( $name, $sha ) use ( $callback ) { | ||
| + $callback( "refs/heads/$name", $sha ); | ||
| + } ); | ||
| + | ||
| + $this->refs->scanRefs( 'refs/tags', function( $name, $sha ) use ( $callback ) { | ||
| + $callback( "refs/tags/$name", $sha ); | ||
| + } ); | ||
| + } | ||
| } | ||
| +<?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 === 'info/refs' ) { | ||
| + $this->renderInfoRefs(); | ||
| + return; | ||
| + } | ||
| + | ||
| + if( $this->subPath === 'HEAD' ) { | ||
| + $this->serve( 'HEAD', 'text/plain' ); | ||
| + return; | ||
| + } | ||
| + | ||
| + if( strpos( $this->subPath, 'objects/' ) === 0 ) { | ||
| + $this->serve( $this->subPath, 'application/x-git-object' ); | ||
| + return; | ||
| + } | ||
| + | ||
| + $this->serve( $this->subPath, 'text/plain' ); | ||
| + } | ||
| + | ||
| + private function renderInfoRefs(): void { | ||
| + header( 'Content-Type: text/plain' ); | ||
| + | ||
| + if( $this->git->streamRaw( 'info/refs' ) ) { | ||
| + exit; | ||
| + } | ||
| + | ||
| + $this->git->eachRef( function( $ref, $sha ) { | ||
| + echo "$sha\t$ref\n"; | ||
| + } ); | ||
| + | ||
| + exit; | ||
| + } | ||
| + | ||
| + private function serve( string $path, string $contentType ): void { | ||
| + header( 'Content-Type: ' . $contentType ); | ||
| + | ||
| + $success = $this->git->streamRaw( $path ); | ||
| + | ||
| + if( !$success ) { | ||
| + http_response_code( 404 ); | ||
| + echo "File not found: $path"; | ||
| + } | ||
| + | ||
| + exit; | ||
| + } | ||
| +} | ||
| Delta | 119 lines added, 22 lines removed, 97-line increase |
|---|