← Back
mainapp/controllers/CommitController.php
<?php

class CommitController
{
  private array $cfg;
  public function __construct(array $cfg){ $this->cfg=$cfg; }

  public function showZip(int $repoId): void {
    $db = db();
    $uid = require_auth();

    if (!repo_can_write($db, $repoId, $uid)) { http_response_code(403); exit("403"); }
    $repo = repo_get($db, $repoId);

    $branch = safe_branch($_GET['branch'] ?? $repo['default_branch']);

    $st = $db->prepare("SELECT name FROM branches WHERE repo_id=? ORDER BY name ASC");
    $st->execute([$repoId]);
    $branches = $st->fetchAll();

    $title = $repo['owner_username'] . "/" . $repo['name'] . " / Commit ZIP";
    require __DIR__ . '/../views/repos/commit_zip.php';
  }

  public function commitZip(int $repoId): void {
    $db = db();
    $uid = require_auth();
    csrf_verify();

    if (!repo_can_write($db, $repoId, $uid)) { http_response_code(403); exit("403"); }
    $repo = repo_get($db, $repoId);

    $branch = safe_branch($_POST['branch'] ?? $repo['default_branch']);
    $message = trim($_POST['message'] ?? '');
    if ($message === '' || strlen($message) > 255) {
      flash_set('err', 'Commit message required (max 255).');
      redirect("/r/$repoId/commit-zip?branch=$branch");
    }

    if (!isset($_FILES['zip']) || $_FILES['zip']['error'] !== UPLOAD_ERR_OK) {
      flash_set('err', 'ZIP upload failed.');
      redirect("/r/$repoId/commit-zip?branch=$branch");
    }

    $maxBytes = 10 * 1024 * 1024;
    if ($_FILES['zip']['size'] > $maxBytes) {
      flash_set('err', 'ZIP too large (max 10MB).');
      redirect("/r/$repoId/commit-zip?branch=$branch");
    }

    $zipPath = $_FILES['zip']['tmp_name'];

    $blobs = new BlobStore($this->cfg['storage_base']);
    $engine = new RepoEngine($blobs);

    $parent = $engine->getBranchHead($db, $repoId, $branch);

    $changes = $this->zipToChanges($zipPath);
    if (!$changes) {
      flash_set('err', 'ZIP had no valid files.');
      redirect("/r/$repoId/commit-zip?branch=$branch");
    }

    $commitId = $engine->commit($db, $repoId, $branch, $parent, $uid, $message, $changes);

    flash_set('ok', "Committed ZIP (#$commitId).");
    redirect("/r/$repoId/browse?branch=$branch");
  }

  private function zipToChanges(string $zipPath): array {
    $z = new ZipArchive();
    if ($z->open($zipPath) !== true) throw new RuntimeException("Invalid ZIP");

    $changes = [];
    for ($i=0; $i<$z->numFiles; $i++) {
      $stat = $z->statIndex($i);
      if (!$stat) continue;

      $name = $stat['name'] ?? '';
      $name = str_replace('\\', '/', $name);

      if (str_ends_with($name, '/')) continue;

      $name = ltrim($name, '/');
      if ($name === '' || str_contains($name, '..') || str_contains($name, "\0")) continue;
      if (strlen($name) > 700) continue;

      $content = $z->getFromIndex($i);
      if ($content === false) continue;
      if (strlen($content) > 2 * 1024 * 1024) continue; // 2MB per file

      // Normalize path validation
      $name = safe_path($name);

      $changes[] = ['path' => $name, 'content' => $content, 'delete' => false];
    }

    $z->close();
    return $changes;
  }
}