1
0
Fork 0
mirror of https://we.phorge.it/source/phorge.git synced 2024-11-22 14:52:41 +01:00

Fix many encoding and architecture problems in Diffusion request and URI handling

Summary:
Diffusion request/uri handling is currently a big, hastily ported mess. In particular, it has:

  - Tons and tons of duplicated code.
  - Bugs with handling unusual branch and file names.
  - An excessively large (and yet insufficiently expressive) API on DiffusionRequest, including a nonsensical concrete base class.
  - Other tools were doing hacky things like passing ":" branch names.

This diff attempts to fix these issues.

  - Make the base class abstract (it was concrete ONLY for "/diffusion/").
  - Move all URI generation to DiffusionRequest. Make the core static. Add unit tests.
  - Delete the 300 copies of URI generation code throughout Diffusion.
  - Move all URI parsing to DiffusionRequest. Make the core static. Add unit tests.
  - Add an appropriate static initializer for other callers.
  - Convert all code calling `newFromAphrontRequestDictionary` outside of Diffusion to the new `newFromDictionary` API.
  - Refactor static initializers to be sensibly-sized.
  - Refactor derived DiffusionRequest classes to remove duplicated code.
  - Properly encode branch names (fixes branches with "/", see <https://github.com/facebook/phabricator/issues/100>).
  - Properly encode path names (fixes issues in D1742).
  - Properly escape delimiter characters ";" and "$" in path names so files like "$100" are not interpreted as "line 100".
  - Fix a couple warnings.
  - Fix a couple lint issues.
  - Fix a bug where we would not parse filenames with spaces in them correctly in the Git browse query.
  - Fix a bug where Git change queries would fail unnecessarily.
  - Provide or improve some documentation.

This thing is pretty gigantic but also kind of hard to split up. If it's unreasonably difficult to review, let me know and I can take a stab at it though.

This supplants D1742.

Test Plan:
  - Used home, repository, branch, browse, change, history, diff (ajax), lastmodified (ajax) views of Diffusion.
  - Used Owners typeaheads and search.
  - Used diffusion.getrecentcommitsbypath method.
  - Pushed a change to an absurdly-named file on an absurdly-named branch, everything worked properly.

{F9185}

Reviewers: nh, vrana, btrahan

Reviewed By: btrahan

CC: aran, epriestley

Differential Revision: https://secure.phabricator.com/D1921
This commit is contained in:
epriestley 2012-03-19 19:52:14 -07:00
parent fdc8bbff99
commit 30ae22bfcf
26 changed files with 671 additions and 331 deletions

View file

@ -340,6 +340,7 @@ phutil_register_library_map(array(
'DiffusionSvnRequest' => 'applications/diffusion/request/svn',
'DiffusionSymbolController' => 'applications/diffusion/controller/symbol',
'DiffusionSymbolQuery' => 'applications/diffusion/query/symbol',
'DiffusionURITestCase' => 'applications/diffusion/request/base/__tests__',
'DiffusionView' => 'applications/diffusion/view/base',
'DrydockAllocator' => 'applications/drydock/allocator/resource',
'DrydockAllocatorWorker' => 'applications/drydock/allocator/worker',
@ -1188,6 +1189,7 @@ phutil_register_library_map(array(
'DiffusionSvnLastModifiedQuery' => 'DiffusionLastModifiedQuery',
'DiffusionSvnRequest' => 'DiffusionRequest',
'DiffusionSymbolController' => 'DiffusionController',
'DiffusionURITestCase' => 'ArcanistPhutilTestCase',
'DiffusionView' => 'AphrontView',
'DrydockAllocatorWorker' => 'PhabricatorWorker',
'DrydockCommandInterface' => 'DrydockInterface',

View file

@ -247,30 +247,13 @@ class AphrontDefaultApplicationConfiguration
'' => 'DiffusionHomeController',
'(?P<callsign>[A-Z]+)/' => array(
'' => 'DiffusionRepositoryController',
'repository/'.
'(?P<path>[^/]+)/'
=> 'DiffusionRepositoryController',
'change/'.
'(?P<path>.*?)'.
'(?:[;](?P<commit>[a-z0-9]+))?'
=> 'DiffusionChangeController',
'history/'.
'(?P<path>.*?)'.
'(?:[;](?P<commit>[a-z0-9]+))?'
=> 'DiffusionHistoryController',
'browse/'.
'(?P<path>.*?)'.
'(?:[;](?P<commit>[a-z0-9]+))?'.
'(?:[$](?P<line>\d+(?:-\d+)?))?'
=> 'DiffusionBrowseController',
'diff/'.
'(?P<path>.*?)'.
'(?:[;](?P<commit>[a-z0-9]+))?'
=> 'DiffusionDiffController',
'lastmodified/'.
'(?P<path>.*?)'.
'(?:[;](?P<commit>[a-z0-9]+))?'
=> 'DiffusionLastModifiedController',
'repository/(?P<dblob>.*)' => 'DiffusionRepositoryController',
'change/(?P<dblob>.*)' => 'DiffusionChangeController',
'history/(?P<dblob>.*)' => 'DiffusionHistoryController',
'browse/(?P<dblob>.*)' => 'DiffusionBrowseController',
'lastmodified/(?P<dblob>.*)' => 'DiffusionLastModifiedController',
'diff/' => 'DiffusionDiffController',
),
'inline/(?P<phid>[^/]+)/' => 'DiffusionInlineCommentController',
'services/' => array(

View file

@ -45,13 +45,13 @@ final class ConduitAPI_diffusion_getrecentcommitsbypath_Method
}
protected function execute(ConduitAPIRequest $request) {
$results = array();
$drequest = DiffusionRequest::newFromDictionary(
array(
'callsign' => $request->getValue('callsign'),
'path' => $request->getValue('path'),
));
$history = DiffusionHistoryQuery::newFromDiffusionRequest(
DiffusionRequest::newFromAphrontRequestDictionary(
$request->getAllParameters()
)
)
$history = DiffusionHistoryQuery::newFromDiffusionRequest($drequest)
->setLimit(self::RESULT_LIMIT)
->needDirectChanges(true)
->needChildChanges(true)

View file

@ -21,8 +21,10 @@ abstract class DiffusionController extends PhabricatorController {
protected $diffusionRequest;
public function willProcessRequest(array $data) {
$this->diffusionRequest = DiffusionRequest::newFromAphrontRequestDictionary(
$data);
if (isset($data['callsign'])) {
$drequest = DiffusionRequest::newFromAphrontRequestDictionary($data);
$this->diffusionRequest = $drequest;
}
}
public function setDiffusionRequest(DiffusionRequest $request) {
@ -31,6 +33,9 @@ abstract class DiffusionController extends PhabricatorController {
}
protected function getDiffusionRequest() {
if (!$this->diffusionRequest) {
throw new Exception("No Diffusion request object!");
}
return $this->diffusionRequest;
}
@ -73,37 +78,34 @@ abstract class DiffusionController extends PhabricatorController {
}
$drequest = $this->getDiffusionRequest();
$repository = $drequest->getRepository();
$callsign = $repository->getCallsign();
$branch_uri = $drequest->getBranchURIComponent($drequest->getBranch());
$path_uri = $branch_uri.$drequest->getPath();
foreach ($navs as $action => $name) {
$href = $drequest->generateURI(
array(
'action' => $action,
));
$commit_uri = null;
$raw_commit = $drequest->getRawCommit();
if ($raw_commit) {
$commit_uri = ';'.$drequest->getCommitURIComponent($raw_commit);
}
foreach ($navs as $uri => $name) {
$nav->addNavItem(
phutil_render_tag(
'a',
array(
'href' => "/diffusion/{$callsign}/{$uri}/{$path_uri}{$commit_uri}",
'href' => $href,
'class' =>
($uri == $selected
($action == $selected
? 'aphront-side-nav-selected'
: null),
),
$name));
}
// TODO: URI encoding might need to be sorted out for this link.
$nav->addNavItem(
phutil_render_tag(
'a',
array(
'href' => '/owners/view/search/'.
'?repository='.phutil_escape_uri($callsign).
'?repository='.phutil_escape_uri($drequest->getCallsign()).
'&path='.phutil_escape_uri('/'.$drequest->getPath()),
),
'Search Owners'));
@ -158,11 +160,18 @@ abstract class DiffusionController extends PhabricatorController {
}
private function buildCrumbList(array $spec = array()) {
$drequest = $this->getDiffusionRequest();
$crumb_list = array();
$repository = $drequest->getRepository();
// 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) {
$crumb_list[] = phutil_render_tag(
'a',
@ -231,14 +240,9 @@ abstract class DiffusionController extends PhabricatorController {
return $crumb_list;
}
$branch_uri = $drequest->getBranchURIComponent($drequest->getBranch());
$view_root_uri = "/diffusion/{$callsign}/{$view}/{$branch_uri}";
$jump_href = $view_root_uri;
$view_tail_uri = null;
if ($raw_commit) {
$view_tail_uri = ';'.$drequest->getCommitURIComponent($raw_commit);
}
$uri_params = array(
'action' => $view,
);
if (!strlen($path)) {
$crumb_list[] = $view_name;
@ -247,7 +251,10 @@ abstract class DiffusionController extends PhabricatorController {
$crumb_list[] = phutil_render_tag(
'a',
array(
'href' => $view_root_uri.$view_tail_uri,
'href' => $drequest->generateURI(
array(
'path' => null,
) + $uri_params),
),
$view_name);
@ -263,7 +270,10 @@ abstract class DiffusionController extends PhabricatorController {
$path_sections[] = phutil_render_tag(
'a',
array(
'href' => $view_root_uri.$thus_far.$view_tail_uri,
'href' => $drequest->generateURI(
array(
'path' => $thus_far,
) + $uri_params),
),
phutil_escape_html($path_part));
}
@ -271,8 +281,6 @@ abstract class DiffusionController extends PhabricatorController {
$path_sections[] = phutil_escape_html($last);
$path_sections = '/'.implode('/', $path_sections);
$jump_href = $view_root_uri.$thus_far.$last;
$crumb_list[] = $path_sections;
}
@ -282,7 +290,10 @@ abstract class DiffusionController extends PhabricatorController {
$jump_link = phutil_render_tag(
'a',
array(
'href' => $jump_href,
'href' => $drequest->generateURI(
array(
'commit' => null,
) + $uri_params),
),
'Jump to HEAD');
$last_crumb .= " @ {$commit_link} ({$jump_link})";

View file

@ -20,6 +20,13 @@ final class DiffusionCommitController extends DiffusionController {
const CHANGES_LIMIT = 100;
public function willProcessRequest(array $data) {
// This controller doesn't use blob/path stuff, just pass the dictionary
// in directly instead of using the AphrontRequest parsing mechanism.
$drequest = DiffusionRequest::newFromDictionary($data);
$this->diffusionRequest = $drequest;
}
public function processRequest() {
$drequest = $this->getDiffusionRequest();
$request = $this->getRequest();
@ -192,11 +199,11 @@ final class DiffusionCommitController extends DiffusionController {
}
}
$branch = $drequest->getBranchURIComponent(
$drequest->getBranch());
$filename = $changeset->getFilename();
$reference = "{$branch}{$filename};".$drequest->getCommit();
$references[$key] = $reference;
$references[$key] = $drequest->generateURI(
array(
'action' => 'rendering-ref',
'path' => $changeset->getFilename(),
));
}
// TOOD: Some parts of the views still rely on properties of the

View file

@ -19,6 +19,7 @@ phutil_require_module('phabricator', 'applications/diffusion/data/pathchange');
phutil_require_module('phabricator', 'applications/diffusion/query/contains/base');
phutil_require_module('phabricator', 'applications/diffusion/query/pathchange/base');
phutil_require_module('phabricator', 'applications/diffusion/query/pathid/base');
phutil_require_module('phabricator', 'applications/diffusion/request/base');
phutil_require_module('phabricator', 'applications/diffusion/view/commentlist');
phutil_require_module('phabricator', 'applications/diffusion/view/commitchangetable');
phutil_require_module('phabricator', 'applications/draft/storage/draft');

View file

@ -19,15 +19,12 @@
final class DiffusionDiffController extends DiffusionController {
public function willProcessRequest(array $data) {
$request = $this->getRequest();
if ($request->getStr('ref')) {
$parts = explode(';', $request->getStr('ref'));
$data['path'] = idx($parts, 0);
$data['commit'] = idx($parts, 1);
}
$data = $data + array(
'dblob' => $this->getRequest()->getStr('ref'),
);
$drequest = DiffusionRequest::newFromAphrontRequestDictionary($data);
$this->diffusionRequest = DiffusionRequest::newFromAphrontRequestDictionary(
$data);
$this->diffusionRequest = $drequest;
}
public function processRequest() {

View file

@ -403,22 +403,27 @@ final class DiffusionBrowseFileController extends DiffusionController {
$targ = null;
}
// Create the row display.
$uri_path = $drequest->getUriPath();
$uri_rev = $drequest->getStableCommitName();
$uri_view = $view
? '?view='.$view
: null;
$href = $drequest->generateURI(
array(
'action' => 'browse',
'stable' => true,
));
$href = (string)$href;
$l = phutil_render_tag(
$query_params = null;
if ($view) {
$query_params = '?view='.$view;
}
$link = phutil_render_tag(
'a',
array(
'href' => $uri_path.';'.$uri_rev.'$'.$n.$uri_view,
'href' => $href.'$'.$n.$query_params,
),
$n);
$rows[] = $tr.$blame_info.
'<th class="diffusion-wide-link">'.$l.'</th>'.
'<th class="diffusion-wide-link">'.$link.'</th>'.
'<td>'.$targ.$line.'</td></tr>';
++$n;
}

View file

@ -44,10 +44,10 @@ final class DiffusionPathCompleteController extends DiffusionController {
}
}
$drequest = DiffusionRequest::newFromAphrontRequestDictionary(
$drequest = DiffusionRequest::newFromDictionary(
array(
'callsign' => $repository->getCallsign(),
'path' => ':/'.$query_dir,
'repository' => $repository,
'path' => $query_dir,
));
$browse_query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest);

View file

@ -36,10 +36,10 @@ final class DiffusionPathValidateController extends DiffusionController {
$path = $request->getStr('path');
$path = ltrim($path, '/');
$drequest = DiffusionRequest::newFromAphrontRequestDictionary(
$drequest = DiffusionRequest::newFromDictionary(
array(
'callsign' => $repository->getCallsign(),
'path' => ':/'.$path,
'repository' => $repository,
'path' => $path,
));
$browse_query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest);

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -22,16 +22,12 @@ final class DiffusionGitDiffQuery extends DiffusionDiffQuery {
$drequest = $this->getRequest();
$repository = $drequest->getRepository();
if (!$drequest->getRawCommit()) {
$effective_commit = $this->getEffectiveCommit();
if (!$effective_commit) {
return null;
}
// TODO: This side effect is kind of skethcy.
$drequest->setCommit($effective_commit);
} else {
$effective_commit = $drequest->getCommit();
$effective_commit = $this->getEffectiveCommit();
if (!$effective_commit) {
return null;
}
// TODO: This side effect is kind of skethcy.
$drequest->setCommit($effective_commit);
$options = array(
'-M',
@ -72,6 +68,10 @@ final class DiffusionGitDiffQuery extends DiffusionDiffQuery {
}
}
if (!$raw_diff) {
return null;
}
$parser = new ArcanistDiffParser();
$try_encoding = $repository->getDetail('encoding');
@ -86,10 +86,10 @@ final class DiffusionGitDiffQuery extends DiffusionDiffQuery {
$changesets = $diff->getChangesets();
$changeset = reset($changesets);
$this->renderingReference =
$drequest->getBranchURIComponent($drequest->getBranch()).
$drequest->getPath().';'.
$drequest->getCommit();
$this->renderingReference = $drequest->generateURI(
array(
'action' => 'rendering-ref',
));
return $changeset;
}

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -51,10 +51,10 @@ final class DiffusionMercurialDiffQuery extends DiffusionDiffQuery {
$changesets = $diff->getChangesets();
$changeset = reset($changesets);
$this->renderingReference =
$drequest->getBranchURIComponent($drequest->getBranch()).
$drequest->getPath().';'.
$drequest->getCommit();
$this->renderingReference = $drequest->generateURI(
array(
'action' => 'rendering-ref',
));
return $changeset;
}

View file

@ -17,10 +17,16 @@
*/
/**
* TODO: This might need to be concrete-extensible, but straighten out the
* class hierarchy here.
* Contains logic to parse Diffusion requests, which have a complicated URI
* structure.
*
*
* @task new Creating Requests
* @task uri Managing Diffusion URIs
*
* @group diffusion
*/
class DiffusionRequest {
abstract class DiffusionRequest {
protected $callsign;
protected $path;
@ -33,60 +39,152 @@ class DiffusionRequest {
protected $repositoryCommitData;
protected $stableCommitName;
abstract protected function getSupportsBranches();
abstract protected function didInitialize();
/* -( Creating Requests )-------------------------------------------------- */
/**
* Create a new synthetic request from a parameter dictionary. If you need
* a @{class:DiffusionRequest} object in order to issue a DiffusionQuery, you
* can use this method to build one.
*
* Parameters are:
*
* - `callsign` Repository callsign. Provide this or `repository`.
* - `repository` Repository object. Provide this or `callsign`.
* - `branch` Optional, branch name.
* - `path` Optional, file path.
* - `commit` Optional, commit identifier.
* - `line` Optional, line range.
*
* @param map See documentation.
* @return DiffusionRequest New request object.
* @task new
*/
final public static function newFromDictionary(array $data) {
if (isset($data['repository']) && isset($data['callsign'])) {
throw new Exception(
"Specify 'repository' or 'callsign', but not both.");
} else if (!isset($data['repository']) && !isset($data['callsign'])) {
throw new Exception(
"One of 'repository' and 'callsign' is required.");
}
if (isset($data['repository'])) {
$object = self::newFromRepository($data['repository']);
} else {
$object = self::newFromCallsign($data['callsign']);
}
$object->initializeFromDictionary($data);
return $object;
}
/**
* Create a new request from an Aphront request dictionary. This is an
* internal method that you generally should not call directly; instead,
* call @{method:newFromDictionary}.
*
* @param map Map of Aphront request data.
* @return DiffusionRequest New request object.
* @task new
*/
final public static function newFromAphrontRequestDictionary(array $data) {
$callsign = phutil_unescape_uri_path_component(idx($data, 'callsign'));
$object = self::newFromCallsign($callsign);
$use_branches = $object->getSupportsBranches();
$parsed = self::parseRequestBlob(idx($data, 'dblob'), $use_branches);
$object->initializeFromDictionary($parsed);
return $object;
}
/**
* Internal.
*
* @task new
*/
final private function __construct() {
// <private>
}
final public static function newFromAphrontRequestDictionary(array $data) {
$vcs = null;
$repository = null;
$callsign = idx($data, 'callsign');
if ($callsign) {
$repository = id(new PhabricatorRepository())->loadOneWhere(
'callsign = %s',
$callsign);
if (!$repository) {
throw new Exception("No such repository '{$callsign}'.");
}
$vcs = $repository->getVersionControlSystem();
/**
* Internal. Use @{method:newFromDictionary}, not this method.
*
* @param string Repository callsign.
* @return DiffusionRequest New request object.
* @task new
*/
final private static function newFromCallsign($callsign) {
$repository = id(new PhabricatorRepository())->loadOneWhere(
'callsign = %s',
$callsign);
if (!$repository) {
throw new Exception("No such repository '{$callsign}'.");
}
switch ($vcs) {
case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
$class = 'DiffusionGitRequest';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
$class = 'DiffusionSvnRequest';
break;
case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
$class = 'DiffusionMercurialRequest';
break;
default:
$class = 'DiffusionRequest';
break;
return self::newFromRepository($repository);
}
/**
* Internal. Use @{method:newFromDictionary}, not this method.
*
* @param PhabricatorRepository Repository object.
* @return DiffusionRequest New request object.
* @task new
*/
final private static function newFromRepository(
PhabricatorRepository $repository) {
$map = array(
PhabricatorRepositoryType::REPOSITORY_TYPE_GIT => 'DiffusionGitRequest',
PhabricatorRepositoryType::REPOSITORY_TYPE_SVN => 'DiffusionSvnRequest',
PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL =>
'DiffusionMercurialRequest',
);
$class = idx($map, $repository->getVersionControlSystem());
if (!$class) {
throw new Exception("Unknown version control system!");
}
$object = new $class();
$object->callsign = $callsign;
$object->repository = $repository;
$object->line = idx($data, 'line');
$object->commit = idx($data, 'commit');
$object->path = idx($data, 'path');
$object->initializeFromAphrontRequestDictionary($data);
$object->callsign = $repository->getCallsign();
return $object;
}
protected function initializeFromAphrontRequestDictionary(array $data) {
/**
* Internal. Use @{method:newFromDictionary}, not this method.
*
* @param map Map of parsed data.
* @return void
* @task new
*/
final private function initializeFromDictionary(array $data) {
$this->path = idx($data, 'path');
$this->commit = idx($data, 'commit');
$this->line = idx($data, 'line');
if ($this->getSupportsBranches()) {
$this->branch = idx($data, 'branch');
}
$this->didInitialize();
}
protected function parsePath($path) {
$this->path = $path;
}
public function getRepository() {
return $this->repository;
@ -100,10 +198,6 @@ class DiffusionRequest {
return $this->path;
}
public function getUriPath() {
return '/diffusion/'.$this->getCallsign().'/browse/'.$this->path;
}
public function getLine() {
return $this->line;
}
@ -166,12 +260,204 @@ class DiffusionRequest {
return $this;
}
public function getCommitURIComponent($commit) {
return $commit;
/* -( Managing Diffusion URIs )-------------------------------------------- */
/**
* Generate a Diffusion URI using this request to provide defaults. See
* @{method:generateDiffusionURI} for details. This method is the same, but
* preserves the request parameters if they are not overridden.
*
* @param map See @{method:generateDiffusionURI}.
* @return PhutilURI Generated URI.
* @task uri
*/
public function generateURI(array $params) {
if (empty($params['stable'])) {
$default_commit = $this->getRawCommit();
} else {
$default_commit = $this->getStableCommitName();
}
$params += array(
'callsign' => $this->getCallsign(),
'path' => $this->getPath(),
'branch' => $this->getBranch(),
'commit' => $default_commit,
);
return self::generateDiffusionURI($params);
}
public function getBranchURIComponent($branch) {
return $branch;
/**
* Generate a Diffusion URI from a parameter map. Applies the correct encoding
* and formatting to the URI. Parameters are:
*
* - `action` One of `history`, `browse`, `change`, `lastmodified`,
* `branch` or `revision-ref`. The action specified by the URI.
* - `callsign` Repository callsign.
* - `branch` Optional if action is not `branch`, branch name.
* - `path` Optional, path to file.
* - `commit` Optional, commit identifier.
* - `line` Optional, line range.
* - `params` Optional, query parameters.
*
* The function generates the specified URI and returns it.
*
* @param map See documentation.
* @return PhutilURI Generated URI.
* @task uri
*/
public static function generateDiffusionURI(array $params) {
$action = idx($params, 'action');
$callsign = idx($params, 'callsign');
$path = idx($params, 'path');
$branch = idx($params, 'branch');
$commit = idx($params, 'commit');
$line = idx($params, 'line');
if (strlen($callsign)) {
$callsign = phutil_escape_uri_path_component($callsign).'/';
}
if (strlen($branch)) {
$branch = phutil_escape_uri_path_component($branch).'/';
}
if (strlen($path)) {
$path = str_replace(array(';', '$'), array(';;', '$$'), $path);
$path = phutil_escape_uri($path);
}
$path = "{$branch}{$path}";
if (strlen($commit)) {
$commit = ';'.phutil_escape_uri($commit);
}
if (strlen($line)) {
$line = '$'.phutil_escape_uri($line);
}
$req_callsign = false;
$req_branch = false;
switch ($action) {
case 'history':
case 'browse':
case 'change':
case 'lastmodified':
$req_callsign = true;
break;
case 'branch':
$req_callsign = true;
$req_branch = true;
break;
}
if ($req_callsign && !strlen($callsign)) {
throw new Exception(
"Diffusion URI action '{$action}' requires callsign!");
}
if ($req_branch && !strlen($branch)) {
throw new Exception(
"Diffusion URI action '{$action}' requires brnach!");
}
switch ($action) {
case 'change':
case 'history':
case 'browse':
case 'lastmodified':
$uri = "/diffusion/{$callsign}{$action}/{$path}{$commit}{$line}";
break;
case 'branch':
$uri = "/diffusion/{$callsign}repository/{$path}";
break;
case 'rendering-ref':
// This isn't a real URI per se, it's passed as a query parameter to
// the ajax changeset stuff but then we parse it back out as though
// it came from a URI.
$uri = "{$path}{$commit}";
break;
default:
throw new Exception("Unknown Diffusion URI action '{$action}'!");
}
if ($action == 'rendering-ref') {
return $uri;
}
$uri = new PhutilURI($uri);
if (idx($params, 'params')) {
$uri->setQueryParams($params['params']);
}
return $uri;
}
/**
* Internal. Public only for unit tests.
*
* Parse the request URI into components.
*
* @param string URI blob.
* @param bool True if this VCS supports branches.
* @return map Parsed URI.
*
* @task uri
*/
public static function parseRequestBlob($blob, $supports_branches) {
$result = array(
'branch' => null,
'path' => null,
'commit' => null,
'line' => null,
);
$matches = null;
if ($supports_branches) {
// Consume the front part of the URI, up to the first "/". This is the
// path-component encoded branch name.
if (preg_match('@^([^/]+)/@', $blob, $matches)) {
$result['branch'] = phutil_unescape_uri_path_component($matches[1]);
$blob = substr($blob, strlen($matches[1]) + 1);
}
}
// Consume the back part of the URI, up to the first "$". Use a negative
// lookbehind to prevent matching '$$'. We double the '$' symbol when
// encoding so that files with names like "money/$100" will survive.
if (preg_match('@(?<![$])[$]([\d-]+)$@', $blob, $matches)) {
$result['line'] = $matches[1];
$blob = substr($blob, 0, -(strlen($matches[1]) + 1));
}
// Consume the commit name, stopping on ';;'.
if (preg_match('@(?<!;);([a-z0-9]+)$@', $blob, $matches)) {
$result['commit'] = $matches[1];
$blob = substr($blob, 0, -(strlen($matches[1]) + 1));
}
// Un-double our delimiter characters.
if (strlen($blob)) {
$result['path'] = str_replace(array(';;', '$$'), array(';', '$'), $blob);
}
$parts = explode('/', $result['path']);
foreach ($parts as $part) {
// Prevent any hyjinx since we're ultimately shipping this to the
// filesystem under a lot of workflows.
if ($part == '..') {
throw new Exception("Invalid path URI.");
}
}
return $result;
}
}

View file

@ -11,6 +11,8 @@ phutil_require_module('phabricator', 'applications/repository/storage/commit');
phutil_require_module('phabricator', 'applications/repository/storage/commitdata');
phutil_require_module('phabricator', 'applications/repository/storage/repository');
phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'parser/uri');
phutil_require_module('phutil', 'utils');

View file

@ -0,0 +1,137 @@
<?php
/*
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
final class DiffusionURITestCase extends ArcanistPhutilTestCase {
public function testBlobDecode() {
$map = array(
// This is a basic blob.
'branch/path.ext;abc$3' => array(
'branch' => 'branch',
'path' => 'path.ext',
'commit' => 'abc',
'line' => '3',
),
'branch/path.ext$3' => array(
'branch' => 'branch',
'path' => 'path.ext',
'line' => '3',
),
'branch/money;;/$$100' => array(
'branch' => 'branch',
'path' => 'money;/$100',
),
'a%252Fb/' => array(
'branch' => 'a/b',
),
);
foreach ($map as $input => $expect) {
// Simulate decode effect of the webserver.
$input = rawurldecode($input);
$expect = $expect + array(
'branch' => null,
'path' => null,
'commit' => null,
'line' => null,
);
$expect = array_select_keys(
$expect,
array('branch', 'path', 'commit', 'line'));
$actual = $this->parseBlob($input);
$this->assertEqual(
$expect,
$actual,
"Parsing '{$input}'");
}
}
public function testBlobDecodeFail() {
$this->tryTestCaseMap(
array(
'branch/path/../../../secrets/secrets.key' => false,
),
array($this, 'parseBlob'));
}
public function parseBlob($blob) {
return DiffusionRequest::parseRequestBlob(
$blob,
$supports_branches = true);
}
public function testURIGeneration() {
$map = array(
'/diffusion/A/browse/branch/path.ext;abc$1' => array(
'action' => 'browse',
'callsign' => 'A',
'branch' => 'branch',
'path' => 'path.ext',
'commit' => 'abc',
'line' => '1',
),
'/diffusion/A/browse/a%252Fb/path.ext' => array(
'action' => 'browse',
'callsign' => 'A',
'branch' => 'a/b',
'path' => 'path.ext',
),
'/diffusion/A/browse/%2B/%20%21' => array(
'action' => 'browse',
'callsign' => 'A',
'path' => '+/ !',
),
'/diffusion/A/browse/money/%24%24100$2' => array(
'action' => 'browse',
'callsign' => 'A',
'path' => 'money/$100',
'line' => '2',
),
'/diffusion/A/browse/path/to/file.ext?view=things' => array(
'action' => 'browse',
'callsign' => 'A',
'path' => 'path/to/file.ext',
'params' => array(
'view' => 'things',
),
),
'/diffusion/A/repository/master/' => array(
'action' => 'branch',
'callsign' => 'A',
'branch' => 'master',
),
'path/to/file.ext;abc' => array(
'action' => 'rendering-ref',
'path' => 'path/to/file.ext',
'commit' => 'abc',
),
);
foreach ($map as $expect => $input) {
$actual = DiffusionRequest::generateDiffusionURI($input);
$this->assertEqual(
$expect,
(string)$actual);
}
}
}

View file

@ -0,0 +1,16 @@
<?php
/**
* This file is automatically generated. Lint this module to rebuild it.
* @generated
*/
phutil_require_module('arcanist', 'unit/engine/phutil/testcase');
phutil_require_module('phabricator', 'applications/diffusion/request/base');
phutil_require_module('phutil', 'utils');
phutil_require_source('DiffusionURITestCase.php');

View file

@ -16,29 +16,16 @@
* limitations under the License.
*/
/**
* @group diffusion
*/
final class DiffusionGitRequest extends DiffusionRequest {
protected function initializeFromAphrontRequestDictionary(array $data) {
parent::initializeFromAphrontRequestDictionary($data);
$path = $this->path;
$parts = explode('/', $path);
$branch = array_shift($parts);
if ($branch != ':') {
$this->branch = $this->decodeBranchName($branch);
}
foreach ($parts as $key => $part) {
// Prevent any hyjinx since we're ultimately shipping this to the
// filesystem under a lot of git workflows.
if ($part == '..') {
unset($parts[$key]);
}
}
$this->path = implode('/', $parts);
protected function getSupportsBranches() {
return true;
}
protected function didInitialize() {
if ($this->repository) {
$repository = $this->repository;
@ -109,11 +96,6 @@ final class DiffusionGitRequest extends DiffusionRequest {
throw new Exception("Unable to determine branch!");
}
public function getUriPath() {
return '/diffusion/'.$this->getCallsign().'/browse/'.
$this->getBranchURIComponent($this->branch).$this->path;
}
public function getCommit() {
if ($this->commit) {
return $this->commit;
@ -126,26 +108,4 @@ final class DiffusionGitRequest extends DiffusionRequest {
return substr($this->stableCommitName, 0, 16);
}
public function getBranchURIComponent($branch) {
return $this->encodeBranchName($branch).'/';
}
private function decodeBranchName($branch) {
$branch = str_replace(':', '/', $branch);
// Backward compatibility for older-style URIs which had an explicit
// "origin" remote in the branch name. If a remote is specified, strip it
// away.
if (strpos($branch, '/') !== false) {
$parts = explode('/', $branch);
$branch = end($parts);
}
return $branch;
}
private function encodeBranchName($branch) {
return str_replace('/', ':', $branch);
}
}

View file

@ -16,28 +16,17 @@
* limitations under the License.
*/
// TODO: This has some minor code duplication vs the Git request that could be
// shared.
/**
* @group diffusion
*/
final class DiffusionMercurialRequest extends DiffusionRequest {
protected function initializeFromAphrontRequestDictionary(array $data) {
parent::initializeFromAphrontRequestDictionary($data);
protected function getSupportsBranches() {
return true;
}
$path = $this->path;
$parts = explode('/', $path);
$branch = array_shift($parts);
if ($branch != ':') {
$this->branch = $this->decodeBranchName($branch);
}
foreach ($parts as $key => $part) {
if ($part == '..') {
unset($parts[$key]);
}
}
$this->path = implode('/', $parts);
protected function didInitialize() {
return;
}
public function getBranch() {
@ -50,11 +39,6 @@ final class DiffusionMercurialRequest extends DiffusionRequest {
throw new Exception("Unable to determine branch!");
}
public function getUriPath() {
return '/diffusion/'.$this->getCallsign().'/browse/'.
$this->getBranchURIComponent($this->branch).$this->path;
}
public function getCommit() {
if ($this->commit) {
return $this->commit;
@ -66,16 +50,4 @@ final class DiffusionMercurialRequest extends DiffusionRequest {
return substr($this->stableCommitName, 0, 16);
}
public function getBranchURIComponent($branch) {
return $this->encodeBranchName($branch).'/';
}
private function decodeBranchName($branch) {
return str_replace(':', '/', $branch);
}
private function encodeBranchName($branch) {
return str_replace('/', ':', $branch);
}
}

View file

@ -16,22 +16,22 @@
* limitations under the License.
*/
/**
* @group diffusion
*/
final class DiffusionSvnRequest extends DiffusionRequest {
protected function initializeFromAphrontRequestDictionary(array $data) {
parent::initializeFromAphrontRequestDictionary($data);
protected function getSupportsBranches() {
return false;
}
protected function didInitialize() {
if ($this->path === null) {
$subpath = $this->repository->getDetail('svn-subpath');
if ($subpath) {
$this->path = $subpath;
}
}
if (!strncmp($this->path, ':', 1)) {
$this->path = substr($this->path, 1);
$this->path = ltrim($this->path, '/');
}
}
public function getStableCommitName() {

View file

@ -41,70 +41,43 @@ abstract class DiffusionView extends AphrontView {
return $text;
}
$drequest = $this->getDiffusionRequest();
if ($commit_identifier) {
$commit = ';'.$commit_identifier;
} else if ($drequest->getRawCommit()) {
$commit = ';'.$drequest->getCommitURIComponent($drequest->getRawCommit());
} else {
$commit = null;
}
$repository = $drequest->getRepository();
$callsign = $repository->getCallsign();
$branch = $drequest->getBranchURIComponent($drequest->getBranch());
$path = $branch.($path ? $path : $drequest->getPath());
$href = $this->getDiffusionRequest()->generateURI(
array(
'action' => 'change',
'path' => $path,
'commit' => $commit_identifier,
));
return phutil_render_tag(
'a',
array(
'href' => "/diffusion/{$callsign}/change/{$path}{$commit}",
'href' => $href,
),
$text);
}
final public function linkHistory($path) {
$drequest = $this->getDiffusionRequest();
if ($drequest->getRawCommit()) {
$commit = ';'.$drequest->getCommitURIComponent($drequest->getRawCommit());
} else {
$commit = null;
}
$repository = $drequest->getRepository();
$callsign = $repository->getCallsign();
$branch = $drequest->getBranchURIComponent($drequest->getBranch());
$path = $branch.$path;
$text = 'History';
$href = $this->getDiffusionRequest()->generateURI(
array(
'action' => 'history',
'path' => $path,
));
return phutil_render_tag(
'a',
array(
'href' => "/diffusion/{$callsign}/history/{$path}{$commit}",
'href' => $href,
),
$text);
'History');
}
final public function linkBrowse($path, array $details = array()) {
$drequest = $this->getDiffusionRequest();
$raw_commit = idx($details, 'commit', $drequest->getRawCommit());
if ($raw_commit) {
$commit = ';'.$drequest->getCommitURIComponent($raw_commit);
} else {
$commit = null;
}
$repository = $drequest->getRepository();
$callsign = $repository->getCallsign();
$branch = $drequest->getBranchURIComponent($drequest->getBranch());
$path = $branch.$path;
$href = $this->getDiffusionRequest()->generateURI(
array(
'action' => 'browse',
'path' => $path,
));
if (isset($details['text'])) {
$text = phutil_escape_html($details['text']);
@ -115,7 +88,7 @@ abstract class DiffusionView extends AphrontView {
return phutil_render_tag(
'a',
array(
'href' => "/diffusion/{$callsign}/browse/{$path}{$commit}",
'href' => $href,
),
$text);
}

View file

@ -11,7 +11,6 @@ phutil_require_module('phabricator', 'applications/repository/constants/reposito
phutil_require_module('phabricator', 'view/base');
phutil_require_module('phutil', 'markup');
phutil_require_module('phutil', 'utils');
phutil_require_source('DiffusionView.php');

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -29,18 +29,18 @@ final class DiffusionBranchTableView extends DiffusionView {
$drequest = $this->getDiffusionRequest();
$current_branch = $drequest->getBranch();
$callsign = $drequest->getRepository()->getCallsign();
$rows = array();
$rowc = array();
foreach ($this->branches as $branch) {
$branch_uri = $drequest->getBranchURIComponent($branch->getName());
$rows[] = array(
phutil_render_tag(
'a',
array(
'href' => "/diffusion/{$callsign}/repository/{$branch_uri}",
'href' => $drequest->generateURI(
array(
'action' => 'branch',
'branch' => $branch->getName(),
)),
),
phutil_escape_html($branch->getName())),
self::linkCommit(

View file

@ -1,7 +1,7 @@
<?php
/*
* Copyright 2011 Facebook, Inc.
* Copyright 2012 Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -120,13 +120,13 @@ final class DiffusionBrowseTableView extends DiffusionView {
'author' => celerity_generate_unique_node_id(),
'details' => celerity_generate_unique_node_id(),
);
$uri =
'/diffusion/'.$repository->getCallsign().'/lastmodified/'.
$request->getBranchURIComponent($request->getBranch()).
$base_path.$path->getPath();
if ($request->getRawCommit()) {
$uri .= ';'.$request->getRawCommit();
}
$uri = (string)$request->generateURI(
array(
'action' => 'lastmodified',
'path' => $base_path.$path->getPath(),
));
$need_pull[$uri] = $dict;
foreach ($dict as $k => $uniq) {
$dict[$k] = '<span id="'.$uniq.'"></span>';

View file

@ -22,7 +22,12 @@ final class PhabricatorOwnerPathQuery {
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
$drequest = self::buildDiffusionRequest($repository, $commit);
$drequest = DiffusionRequest::newFromAphrontRequestDictionary(
array(
'repository' => $repository,
'commit' => $commit->getCommitIdentifier(),
));
$path_query = DiffusionPathChangeQuery::newFromDiffusionRequest(
$drequest);
$paths = $path_query->loadChanges();
@ -38,15 +43,4 @@ final class PhabricatorOwnerPathQuery {
return $result;
}
private static function buildDiffusionRequest(
PhabricatorRepository $repository,
PhabricatorRepositoryCommit $commit) {
return DiffusionRequest::newFromAphrontRequestDictionary(
array(
'callsign' => $repository->getCallsign(),
'commit' => $commit->getCommitIdentifier(),
));
}
}

View file

@ -188,10 +188,10 @@ final class PhabricatorOwnersPackage extends PhabricatorOwnersDAO {
foreach ($paths as $path => $ignored) {
$path = ltrim($path, '/');
// build query to validate path
$drequest = DiffusionRequest::newFromAphrontRequestDictionary(
$drequest = DiffusionRequest::newFromDictionary(
array(
'callsign' => $repository->getCallsign(),
'path' => ':/'.$path,
'repository' => $repository,
'path' => $path,
));
$query = DiffusionBrowseQuery::newFromDiffusionRequest($drequest);
$query->needValidityOnly(true);

View file

@ -45,18 +45,13 @@ final class PhabricatorRepositorySymbol extends PhabricatorRepositoryDAO {
}
public function getURI() {
$repo = $this->getRepository();
$file = $this->getPath();
$line = $this->getLineNumber();
$drequest = DiffusionRequest::newFromAphrontRequestDictionary(
return DiffusionRequest::generateDiffusionURI(
array(
'callsign' => $repo->getCallsign(),
'action' => 'browse',
'callsign' => $this->getRepository()->getCallsign(),
'path' => $this->getPath(),
'line' => $this->getLineNumber(),
));
$branch = $drequest->getBranchURIComponent($drequest->getBranch());
$file = $branch.ltrim($file, '/');
return '/diffusion/'.$repo->getCallsign().'/browse/'.$file.'$'.$line;
}
public function getPath() {