mirror of
https://we.phorge.it/source/phorge.git
synced 2025-03-04 08:29:18 +01:00
Summary: Mostly ripped from D7391, with some changes: - Serve repositories at `/diffusion/X/`, with no special `/git/` or `/serve/` URI component. - This requires a little bit of magic, but I got the magic working for Git, Mercurial and SVN, and it seems reasonable. - I think having one URI for everything will make it easier for users to understand. - One downside is that git will clone into `X` by default, but I think that's not a big deal, and we can work around that in the future easily enough. - Accept HTTP requests for Git, SVN and Mercurial repositories. - Auth logic is a little different in order to be more consistent with how other things work. - Instead of AphrontBasicAuthResponse, added "VCSResponse". Mercurial can print strings we send it on the CLI if we're careful, so support that. I did a fair amount of digging and didn't have any luck with git or svn. - Commands we don't know about are assumed to require "Push" capability by default. No actual VCS data going over the wire yet. Test Plan: Ran a bunch of stuff like this: $ hg clone http://local.aphront.com:8080/diffusion/P/ abort: HTTP Error 403: This repository is not available over HTTP. ...and got pretty reasonable-seeming errors in all cases. All this can do is produce errors for now. Reviewers: hach-que, btrahan Reviewed By: hach-que CC: aran Maniphest Tasks: T2230 Differential Revision: https://secure.phabricator.com/D7417
379 lines
10 KiB
PHP
379 lines
10 KiB
PHP
<?php
|
|
|
|
abstract class DiffusionController extends PhabricatorController {
|
|
|
|
protected $diffusionRequest;
|
|
|
|
public function willBeginExecution() {
|
|
$request = $this->getRequest();
|
|
$uri = $request->getRequestURI();
|
|
|
|
// Check if this is a VCS request, e.g. from "git clone", "hg clone", or
|
|
// "svn checkout". If it is, we jump off into repository serving code to
|
|
// process the request.
|
|
|
|
$regex = '@^/diffusion/(?P<callsign>[A-Z]+)(/|$)@';
|
|
$matches = null;
|
|
if (preg_match($regex, (string)$uri, $matches)) {
|
|
$vcs = null;
|
|
|
|
if ($request->getExists('__vcs__')) {
|
|
// This is magic to make it easier for us to debug stuff by telling
|
|
// users to run:
|
|
//
|
|
// curl http://example.phabricator.com/diffusion/X/?__vcs__=1
|
|
//
|
|
// ...to get a human-readable error.
|
|
$vcs = $request->getExists('__vcs__');
|
|
} else if ($request->getExists('service')) {
|
|
// Git also gives us a User-Agent like "git/1.8.2.3".
|
|
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
|
|
} else if ($request->getExists('cmd')) {
|
|
// Mercurial also sends an Accept header like
|
|
// "application/mercurial-0.1", and a User-Agent like
|
|
// "mercurial/proto-1.0".
|
|
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
|
|
} else {
|
|
// Subversion also sends an initial OPTIONS request (vs GET/POST), and
|
|
// has a User-Agent like "SVN/1.8.3 (x86_64-apple-darwin11.4.2)
|
|
// serf/1.3.2".
|
|
$dav = $request->getHTTPHeader('DAV');
|
|
$dav = new PhutilURI($dav);
|
|
if ($dav->getDomain() === 'subversion.tigris.org') {
|
|
$vcs = PhabricatorRepositoryType::REPOSITORY_TYPE_SVN;
|
|
}
|
|
}
|
|
|
|
if ($vcs) {
|
|
return $this->processVCSRequest($matches['callsign']);
|
|
}
|
|
}
|
|
|
|
parent::willBeginExecution();
|
|
}
|
|
|
|
private function processVCSRequest($callsign) {
|
|
|
|
// TODO: Authenticate user.
|
|
|
|
$viewer = new PhabricatorUser();
|
|
|
|
$allow_public = PhabricatorEnv::getEnvConfig('policy.allow-public');
|
|
if (!$allow_public) {
|
|
if (!$viewer->isLoggedIn()) {
|
|
return new PhabricatorVCSResponse(
|
|
403,
|
|
pht('You must log in to access repositories.'));
|
|
}
|
|
}
|
|
|
|
try {
|
|
$repository = id(new PhabricatorRepositoryQuery())
|
|
->setViewer($viewer)
|
|
->withCallsigns(array($callsign))
|
|
->executeOne();
|
|
if (!$repository) {
|
|
return new PhabricatorVCSResponse(
|
|
404,
|
|
pht('No such repository exists.'));
|
|
}
|
|
} catch (PhabricatorPolicyException $ex) {
|
|
if ($viewer->isLoggedIn()) {
|
|
return new PhabricatorVCSResponse(
|
|
403,
|
|
pht('You do not have permission to access this repository.'));
|
|
} else {
|
|
return new PhabricatorVCSResponse(
|
|
401,
|
|
pht('You must log in to access this repository.'));
|
|
}
|
|
}
|
|
|
|
$is_push = !$this->isReadOnlyRequest($repository);
|
|
|
|
switch ($repository->getServeOverHTTP()) {
|
|
case PhabricatorRepository::SERVE_READONLY:
|
|
if ($is_push) {
|
|
return new PhabricatorVCSResponse(
|
|
403,
|
|
pht('This repository is read-only over HTTP.'));
|
|
}
|
|
break;
|
|
case PhabricatorRepository::SERVE_READWRITE:
|
|
if ($is_push) {
|
|
$can_push = PhabricatorPolicyFilter::hasCapability(
|
|
$viewer,
|
|
$repository,
|
|
DiffusionCapabilityPush::CAPABILITY);
|
|
if (!$can_push) {
|
|
if ($viewer->isLoggedIn()) {
|
|
return new PhabricatorVCSResponse(
|
|
403,
|
|
pht('You do not have permission to push to this repository.'));
|
|
} else {
|
|
return new PhabricatorVCSResponse(
|
|
401,
|
|
pht('You must log in to push to this repository.'));
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
case PhabricatorRepository::SERVE_OFF:
|
|
default:
|
|
return new PhabricatorVCSResponse(
|
|
403,
|
|
pht('This repository is not available over HTTP.'));
|
|
}
|
|
|
|
return new PhabricatorVCSResponse(
|
|
999,
|
|
pht('TODO: Implement meaningful responses.'));
|
|
}
|
|
|
|
private function isReadOnlyRequest(
|
|
PhabricatorRepository $repository) {
|
|
$request = $this->getRequest();
|
|
|
|
// TODO: This implementation is safe by default, but very incomplete.
|
|
|
|
switch ($repository->getVersionControlSystem()) {
|
|
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
|
|
$service = $request->getStr('service');
|
|
// NOTE: Service names are the reverse of what you might expect, as they
|
|
// are from the point of view of the server. The main read service is
|
|
// "git-upload-pack", and the main write service is "git-receive-pack".
|
|
switch ($service) {
|
|
case 'git-upload-pack':
|
|
return true;
|
|
case 'git-receive-pack':
|
|
default:
|
|
return false;
|
|
}
|
|
break;
|
|
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
|
|
$cmd = $request->getStr('cmd');
|
|
switch ($cmd) {
|
|
case 'capabilities':
|
|
return true;
|
|
default:
|
|
return false;
|
|
}
|
|
break;
|
|
case PhabricatorRepositoryType::REPOSITORY_TYPE_SUBVERSION:
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public function willProcessRequest(array $data) {
|
|
if (isset($data['callsign'])) {
|
|
$drequest = DiffusionRequest::newFromAphrontRequestDictionary(
|
|
$data,
|
|
$this->getRequest());
|
|
$this->diffusionRequest = $drequest;
|
|
}
|
|
}
|
|
|
|
public function setDiffusionRequest(DiffusionRequest $request) {
|
|
$this->diffusionRequest = $request;
|
|
return $this;
|
|
}
|
|
|
|
protected function getDiffusionRequest() {
|
|
if (!$this->diffusionRequest) {
|
|
throw new Exception("No Diffusion request object!");
|
|
}
|
|
return $this->diffusionRequest;
|
|
}
|
|
|
|
public function buildCrumbs(array $spec = array()) {
|
|
$crumbs = $this->buildApplicationCrumbs();
|
|
$crumb_list = $this->buildCrumbList($spec);
|
|
foreach ($crumb_list as $crumb) {
|
|
$crumbs->addCrumb($crumb);
|
|
}
|
|
return $crumbs;
|
|
}
|
|
|
|
private function buildCrumbList(array $spec = array()) {
|
|
|
|
$spec = $spec + array(
|
|
'commit' => null,
|
|
'tags' => null,
|
|
'branches' => null,
|
|
'view' => null,
|
|
);
|
|
|
|
$crumb_list = array();
|
|
|
|
// On the home page, we don't have a DiffusionRequest.
|
|
if ($this->diffusionRequest) {
|
|
$drequest = $this->getDiffusionRequest();
|
|
$repository = $drequest->getRepository();
|
|
} else {
|
|
$drequest = null;
|
|
$repository = null;
|
|
}
|
|
|
|
if (!$repository) {
|
|
return $crumb_list;
|
|
}
|
|
|
|
$callsign = $repository->getCallsign();
|
|
$repository_name = 'r'.$callsign;
|
|
|
|
if (!$spec['commit'] && !$spec['tags'] && !$spec['branches']) {
|
|
$branch_name = $drequest->getBranch();
|
|
if ($branch_name) {
|
|
$repository_name .= ' ('.$branch_name.')';
|
|
}
|
|
}
|
|
|
|
$crumb = id(new PhabricatorCrumbView())
|
|
->setName($repository_name);
|
|
if (!$spec['view'] && !$spec['commit'] &&
|
|
!$spec['tags'] && !$spec['branches']) {
|
|
$crumb_list[] = $crumb;
|
|
return $crumb_list;
|
|
}
|
|
$crumb->setHref(
|
|
$drequest->generateURI(
|
|
array(
|
|
'action' => 'branch',
|
|
'path' => '/',
|
|
)));
|
|
$crumb_list[] = $crumb;
|
|
|
|
$raw_commit = $drequest->getRawCommit();
|
|
|
|
if ($spec['tags']) {
|
|
$crumb = new PhabricatorCrumbView();
|
|
if ($spec['commit']) {
|
|
$crumb->setName(
|
|
pht("Tags for %s", 'r'.$callsign.$raw_commit));
|
|
$crumb->setHref($drequest->generateURI(
|
|
array(
|
|
'action' => 'commit',
|
|
'commit' => $raw_commit,
|
|
)));
|
|
} else {
|
|
$crumb->setName(pht('Tags'));
|
|
}
|
|
$crumb_list[] = $crumb;
|
|
return $crumb_list;
|
|
}
|
|
|
|
if ($spec['branches']) {
|
|
$crumb = id(new PhabricatorCrumbView())
|
|
->setName(pht('Branches'));
|
|
$crumb_list[] = $crumb;
|
|
return $crumb_list;
|
|
}
|
|
|
|
if ($spec['commit']) {
|
|
$crumb = id(new PhabricatorCrumbView())
|
|
->setName("r{$callsign}{$raw_commit}")
|
|
->setHref("r{$callsign}{$raw_commit}");
|
|
$crumb_list[] = $crumb;
|
|
return $crumb_list;
|
|
}
|
|
|
|
$crumb = new PhabricatorCrumbView();
|
|
$view = $spec['view'];
|
|
|
|
switch ($view) {
|
|
case 'history':
|
|
$view_name = pht('History');
|
|
break;
|
|
case 'browse':
|
|
$view_name = pht('Browse');
|
|
break;
|
|
case 'lint':
|
|
$view_name = pht('Lint');
|
|
break;
|
|
case 'change':
|
|
$view_name = pht('Change');
|
|
break;
|
|
}
|
|
|
|
$crumb = id(new PhabricatorCrumbView())
|
|
->setName($view_name);
|
|
|
|
$crumb_list[] = $crumb;
|
|
return $crumb_list;
|
|
}
|
|
|
|
protected function callConduitWithDiffusionRequest(
|
|
$method,
|
|
array $params = array()) {
|
|
|
|
$user = $this->getRequest()->getUser();
|
|
$drequest = $this->getDiffusionRequest();
|
|
|
|
return DiffusionQuery::callConduitWithDiffusionRequest(
|
|
$user,
|
|
$drequest,
|
|
$method,
|
|
$params);
|
|
}
|
|
|
|
protected function getRepositoryControllerURI(
|
|
PhabricatorRepository $repository,
|
|
$path) {
|
|
return $this->getApplicationURI($repository->getCallsign().'/'.$path);
|
|
}
|
|
|
|
protected function renderPathLinks(DiffusionRequest $drequest, $action) {
|
|
$path = $drequest->getPath();
|
|
$path_parts = array_filter(explode('/', trim($path, '/')));
|
|
|
|
$divider = phutil_tag(
|
|
'span',
|
|
array(
|
|
'class' => 'phui-header-divider'),
|
|
'/');
|
|
|
|
$links = array();
|
|
if ($path_parts) {
|
|
$links[] = phutil_tag(
|
|
'a',
|
|
array(
|
|
'href' => $drequest->generateURI(
|
|
array(
|
|
'action' => $action,
|
|
'path' => '',
|
|
)),
|
|
),
|
|
'r'.$drequest->getRepository()->getCallsign());
|
|
$links[] = $divider;
|
|
$accum = '';
|
|
$last_key = last_key($path_parts);
|
|
foreach ($path_parts as $key => $part) {
|
|
$accum .= '/'.$part;
|
|
if ($key === $last_key) {
|
|
$links[] = $part;
|
|
} else {
|
|
$links[] = phutil_tag(
|
|
'a',
|
|
array(
|
|
'href' => $drequest->generateURI(
|
|
array(
|
|
'action' => $action,
|
|
'path' => $accum.'/',
|
|
)),
|
|
),
|
|
$part);
|
|
$links[] = $divider;
|
|
}
|
|
}
|
|
} else {
|
|
$links[] = 'r'.$drequest->getRepository()->getCallsign();
|
|
$links[] = $divider;
|
|
}
|
|
|
|
return $links;
|
|
}
|
|
|
|
}
|
|
|