diff --git a/src/applications/project/constants/PhabricatorProjectTransactionType.php b/src/applications/project/constants/PhabricatorProjectTransactionType.php
index 49ef0a9d8..77170e6f0 100644
--- a/src/applications/project/constants/PhabricatorProjectTransactionType.php
+++ b/src/applications/project/constants/PhabricatorProjectTransactionType.php
@@ -1,26 +1,29 @@
buildStandardPageView();
$page->setApplicationName('Project');
$page->setBaseURI('/project/');
$page->setTitle(idx($data, 'title'));
$page->setGlyph("\xE2\x98\xA3");
$page->appendChild($view);
$response = new AphrontWebpageResponse();
return $response->setContent($page->render());
}
protected function buildLocalNavigation(PhabricatorProject $project) {
$id = $project->getID();
$nav_view = new AphrontSideNavFilterView();
$uri = new PhutilURI('/project/view/'.$id.'/');
$nav_view->setBaseURI($uri);
$external_arrow = "\xE2\x86\x97";
$tasks_uri = '/maniphest/view/all/?projects='.$project->getPHID();
$slug = PhabricatorSlug::normalize($project->getName());
$phriction_uri = '/w/projects/'.$slug;
$edit_uri = '/project/edit/'.$id.'/';
$members_uri = '/project/members/'.$id.'/';
$nav_view->addFilter('dashboard', 'Dashboard');
$nav_view->addSpacer();
$nav_view->addFilter('feed', 'Feed');
$nav_view->addFilter(null, 'Tasks '.$external_arrow, $tasks_uri);
$nav_view->addFilter(null, 'Wiki '.$external_arrow, $phriction_uri);
$nav_view->addFilter('people', 'People');
$nav_view->addFilter('about', 'About');
+
+ $user = $this->getRequest()->getUser();
+ $can_edit = PhabricatorPolicyCapability::CAN_EDIT;
+
$nav_view->addSpacer();
- $nav_view->addFilter('edit', "Edit Project\xE2\x80\xA6", $edit_uri);
- $nav_view->addFilter('members', "Edit Members\xE2\x80\xA6", $members_uri);
+ if (PhabricatorPolicyFilter::hasCapability($user, $project, $can_edit)) {
+ $nav_view->addFilter('edit', "Edit Project\xE2\x80\xA6", $edit_uri);
+ $nav_view->addFilter('members', "Edit Members\xE2\x80\xA6", $members_uri);
+ } else {
+ $nav_view->addFilter(
+ 'edit',
+ "Edit Project\xE2\x80\xA6",
+ $edit_uri,
+ $relative = false,
+ 'disabled');
+ $nav_view->addFilter(
+ 'members',
+ "Edit Members\xE2\x80\xA6",
+ $members_uri,
+ $relative = false,
+ 'disabled');
+ }
return $nav_view;
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectMembersEditController.php b/src/applications/project/controller/PhabricatorProjectMembersEditController.php
index 02f9a1239..5e3aad486 100644
--- a/src/applications/project/controller/PhabricatorProjectMembersEditController.php
+++ b/src/applications/project/controller/PhabricatorProjectMembersEditController.php
@@ -1,172 +1,180 @@
id = $data['id'];
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
- $project = id(new PhabricatorProject())->load($this->id);
+ $project = id(new PhabricatorProjectQuery())
+ ->setViewer($user)
+ ->withIDs(array($this->id))
+ ->requireCapabilities(
+ array(
+ PhabricatorPolicyCapability::CAN_VIEW,
+ PhabricatorPolicyCapability::CAN_EDIT,
+ ))
+ ->executeOne();
if (!$project) {
return new Aphront404Response();
}
$profile = $project->loadProfile();
if (empty($profile)) {
$profile = new PhabricatorProjectProfile();
}
$member_phids = $project->loadMemberPHIDs();
$errors = array();
if ($request->isFormPost()) {
$changed_something = false;
$member_map = array_fill_keys($member_phids, true);
$remove = $request->getStr('remove');
if ($remove) {
if (isset($member_map[$remove])) {
unset($member_map[$remove]);
$changed_something = true;
}
} else {
$new_members = $request->getArr('phids');
foreach ($new_members as $member) {
if (empty($member_map[$member])) {
$member_map[$member] = true;
$changed_something = true;
}
}
}
$xactions = array();
if ($changed_something) {
$xaction = new PhabricatorProjectTransaction();
$xaction->setTransactionType(
PhabricatorProjectTransactionType::TYPE_MEMBERS);
$xaction->setNewValue(array_keys($member_map));
$xactions[] = $xaction;
}
if ($xactions) {
$editor = new PhabricatorProjectEditor($project);
$editor->setUser($user);
$editor->applyTransactions($xactions);
}
return id(new AphrontRedirectResponse())
->setURI($request->getRequestURI());
}
$member_phids = array_reverse($member_phids);
$handles = id(new PhabricatorObjectHandleData($member_phids))
->loadHandles();
$state = array();
foreach ($handles as $handle) {
$state[] = array(
'phid' => $handle->getPHID(),
'name' => $handle->getFullName(),
);
}
$header_name = 'Edit Members';
$title = 'Edit Members';
$list = $this->renderMemberList($handles);
$form = new AphrontFormView();
$form
->setUser($user)
->appendChild(
id(new AphrontFormTokenizerControl())
->setName('phids')
->setLabel('Add Members')
->setDatasource('/typeahead/common/users/'))
->appendChild(
id(new AphrontFormSubmitControl())
->addCancelButton('/project/view/'.$project->getID().'/')
->setValue('Add Members'));
$faux_form = id(new AphrontFormLayoutView())
->setBackgroundShading(true)
->setPadded(true)
->appendChild(
id(new AphrontFormInsetView())
->setTitle('Current Members ('.count($handles).')')
->appendChild($list));
$panel = new AphrontPanelView();
$panel->setHeader($header_name);
$panel->setWidth(AphrontPanelView::WIDTH_FORM);
$panel->appendChild($form);
$panel->appendChild('
');
$panel->appendChild($faux_form);
$nav = $this->buildLocalNavigation($project);
$nav->selectFilter('members');
$nav->appendChild($panel);
return $this->buildStandardPageResponse(
$nav,
array(
'title' => $title,
));
}
private function renderMemberList(array $handles) {
$request = $this->getRequest();
$user = $request->getUser();
$list = id(new PhabricatorObjectListView())
->setHandles($handles);
foreach ($handles as $handle) {
$hidden_input = phutil_render_tag(
'input',
array(
'type' => 'hidden',
'name' => 'remove',
'value' => $handle->getPHID(),
),
'');
$button = javelin_render_tag(
'button',
array(
'class' => 'grey',
),
pht('Remove'));
$list->addButton(
$handle,
phabricator_render_form(
$user,
array(
'method' => 'POST',
'action' => $request->getRequestURI(),
),
$hidden_input.$button));
}
return $list;
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php
index 608cbbe4a..241cb6951 100644
--- a/src/applications/project/controller/PhabricatorProjectProfileController.php
+++ b/src/applications/project/controller/PhabricatorProjectProfileController.php
@@ -1,291 +1,305 @@
id = idx($data, 'id');
$this->page = idx($data, 'page');
}
public function processRequest() {
$request = $this->getRequest();
$user = $request->getUser();
- $project = id(new PhabricatorProject())->load($this->id);
+ $query = id(new PhabricatorProjectQuery())
+ ->setViewer($user)
+ ->withIDs(array($this->id));
+
+ if ($this->page == 'people') {
+ $query->needMembers(true);
+ }
+
+ $project = $query->executeOne();
if (!$project) {
return new Aphront404Response();
}
+
$profile = $project->loadProfile();
if (!$profile) {
$profile = new PhabricatorProjectProfile();
}
$picture = $profile->loadProfileImageURI();
- $members = $project->loadMemberPHIDs();
- $member_map = array_fill_keys($members, true);
$nav_view = $this->buildLocalNavigation($project);
$this->page = $nav_view->selectFilter($this->page, 'dashboard');
-
require_celerity_resource('phabricator-profile-css');
switch ($this->page) {
case 'dashboard':
$content = $this->renderTasksPage($project, $profile);
$query = new PhabricatorFeedQuery();
$query->setFilterPHIDs(
array(
$project->getPHID(),
));
$query->setLimit(50);
$query->setViewer($this->getRequest()->getUser());
$stories = $query->execute();
$content .= $this->renderStories($stories);
break;
case 'about':
$content = $this->renderAboutPage($project, $profile);
break;
case 'people':
$content = $this->renderPeoplePage($project, $profile);
break;
case 'feed':
$content = $this->renderFeedPage($project, $profile);
break;
default:
throw new Exception("Unimplemented filter '{$this->page}'.");
}
$content = '
Creator | '.$handles[$project->getAuthorPHID()]->renderLink().' |
---|---|
Created | '.$timestamp.' |
PHID | '.phutil_escape_html($project->getPHID()).' |
Blurb | '.$blurb.' |
No one is affiliated with this project.
'; } return 'NOTE: Policy settings are not '. + 'yet fully implemented. Some interfaces still ignore these settings, '. + 'particularly "Visible To".
') + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setUser($user) + ->setName('can_view') + ->setCaption('Members can always view a project.') + ->setPolicyObject($project) + ->setCapability(PhabricatorPolicyCapability::CAN_VIEW)) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setUser($user) + ->setName('can_edit') + ->setPolicyObject($project) + ->setCapability(PhabricatorPolicyCapability::CAN_EDIT)) + ->appendChild( + id(new AphrontFormPolicyControl()) + ->setUser($user) + ->setName('can_join') + ->setCaption( + 'Users who can edit a project can always join a project.') + ->setPolicyObject($project) + ->setCapability(PhabricatorPolicyCapability::CAN_JOIN)) ->appendChild( id(new AphrontFormMarkupControl()) ->setLabel('Profile Image') ->setValue( phutil_render_tag( 'img', array( 'src' => $img_src, )))) ->appendChild( id(new AphrontFormImageControl()) ->setLabel('Change Image') ->setName('image') ->setError($e_image) ->setCaption('Supported formats: '.implode(', ', $supported_formats))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton('/project/view/'.$project->getID().'/') ->setValue('Save')); $panel = new AphrontPanelView(); $panel->setHeader($header_name); $panel->setWidth(AphrontPanelView::WIDTH_FORM); $panel->appendChild($form); $nav = $this->buildLocalNavigation($project); $nav->selectFilter('edit'); $nav->appendChild( array( $error_view, $panel, )); return $this->buildStandardPageResponse( $nav, array( 'title' => $title, )); } } diff --git a/src/applications/project/controller/PhabricatorProjectUpdateController.php b/src/applications/project/controller/PhabricatorProjectUpdateController.php index b12bcc8fa..41d1685f8 100644 --- a/src/applications/project/controller/PhabricatorProjectUpdateController.php +++ b/src/applications/project/controller/PhabricatorProjectUpdateController.php @@ -1,88 +1,94 @@ id = $data['id']; $this->action = $data['action']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); - $project = id(new PhabricatorProjectQuery()) - ->setViewer($user) - ->needMembers(true) - ->withIDs(array($this->id)) - ->executeOne(); - if (!$project) { - return new Aphront404Response(); - } + $capabilities = array( + PhabricatorPolicyCapability::CAN_VIEW, + ); $process_action = false; switch ($this->action) { case 'join': + $capabilities[] = PhabricatorPolicyCapability::CAN_JOIN; $process_action = $request->isFormPost(); break; case 'leave': $process_action = $request->isDialogFormPost(); break; default: return new Aphront404Response(); } + $project = id(new PhabricatorProjectQuery()) + ->setViewer($user) + ->withIDs(array($this->id)) + ->needMembers(true) + ->requireCapabilities($capabilities) + ->executeOne(); + if (!$project) { + return new Aphront404Response(); + } + $project_uri = '/project/view/'.$project->getID().'/'; if ($process_action) { switch ($this->action) { case 'join': PhabricatorProjectEditor::applyJoinProject($project, $user); break; case 'leave': PhabricatorProjectEditor::applyLeaveProject($project, $user); break; } return id(new AphrontRedirectResponse())->setURI($project_uri); } $dialog = null; switch ($this->action) { case 'leave': $dialog = new AphrontDialogView(); $dialog->setUser($user); $dialog->setTitle('Really leave project?'); $dialog->appendChild( 'Your tremendous contributions to this project will be sorely '. 'missed. Are you sure you want to leave?
'); $dialog->addCancelButton($project_uri); $dialog->addSubmitButton('Leave Project'); break; default: return new Aphront404Response(); } return id(new AphrontDialogResponse())->setDialog($dialog); } } diff --git a/src/applications/project/editor/PhabricatorProjectEditor.php b/src/applications/project/editor/PhabricatorProjectEditor.php index 4c8646d9c..3282520aa 100644 --- a/src/applications/project/editor/PhabricatorProjectEditor.php +++ b/src/applications/project/editor/PhabricatorProjectEditor.php @@ -1,267 +1,394 @@ getMemberPHIDs(); $members[] = $user->getPHID(); self::applyOneTransaction( $project, $user, PhabricatorProjectTransactionType::TYPE_MEMBERS, $members); } public static function applyLeaveProject( PhabricatorProject $project, PhabricatorUser $user) { $members = array_fill_keys($project->getMemberPHIDs(), true); unset($members[$user->getPHID()]); $members = array_keys($members); self::applyOneTransaction( $project, $user, PhabricatorProjectTransactionType::TYPE_MEMBERS, $members); } private static function applyOneTransaction( PhabricatorProject $project, PhabricatorUser $user, $type, $new_value) { $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType($type); $xaction->setNewValue($new_value); $editor = new PhabricatorProjectEditor($project); $editor->setUser($user); $editor->applyTransactions(array($xaction)); } public function __construct(PhabricatorProject $project) { $this->project = $project; } public function setUser(PhabricatorUser $user) { $this->user = $user; return $this; } public function applyTransactions(array $transactions) { assert_instances_of($transactions, 'PhabricatorProjectTransaction'); if (!$this->user) { throw new Exception('Call setUser() before save()!'); } $user = $this->user; $project = $this->project; $is_new = !$project->getID(); if ($is_new) { $project->setAuthorPHID($user->getPHID()); } foreach ($transactions as $key => $xaction) { - $type = $xaction->getTransactionType(); - $this->setTransactionOldValue($project, $xaction); - if (!$this->transactionHasEffect($xaction)) { unset($transactions[$key]); continue; } + } - $this->applyTransactionEffect($project, $xaction); + if (!$is_new) { + // You must be able to view a project in order to edit it in any capacity. + PhabricatorPolicyFilter::requireCapability( + $user, + $project, + PhabricatorPolicyCapability::CAN_VIEW); + + $need_edit = false; + $need_join = false; + foreach ($transactions as $key => $xaction) { + if ($this->getTransactionRequiresEditCapability($xaction)) { + $need_edit = true; + } + if ($this->getTransactionRequiresJoinCapability($xaction)) { + $need_join = true; + } + } + + if ($need_edit) { + PhabricatorPolicyFilter::requireCapability( + $user, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + } + + if ($need_join) { + PhabricatorPolicyFilter::requireCapability( + $user, + $project, + PhabricatorPolicyCapability::CAN_JOIN); + } } if (!$transactions) { return $this; } + foreach ($transactions as $xaction) { + $this->applyTransactionEffect($project, $xaction); + } + try { $project->openTransaction(); $project->save(); $edge_type = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER; $editor = new PhabricatorEdgeEditor(); $editor->setUser($this->user); foreach ($this->remEdges as $phid) { $editor->removeEdge($project->getPHID(), $edge_type, $phid); } foreach ($this->addEdges as $phid) { $editor->addEdge($project->getPHID(), $edge_type, $phid); } $editor->save(); foreach ($transactions as $xaction) { $xaction->setAuthorPHID($user->getPHID()); $xaction->setProjectID($project->getID()); $xaction->save(); } $project->saveTransaction(); foreach ($transactions as $xaction) { $this->publishTransactionStory($project, $xaction); } } catch (AphrontQueryDuplicateKeyException $ex) { // We already validated the slug, but might race. Try again to see if // that's the issue. If it is, we'll throw a more specific exception. If // not, throw the original exception. $this->validateName($project); throw $ex; } // TODO: If we rename a project, we should move its Phriction page. Do // that once Phriction supports document moves. return $this; } private function validateName(PhabricatorProject $project) { $slug = $project->getPhrictionSlug(); $name = $project->getName(); if ($slug == '/') { throw new PhabricatorProjectNameCollisionException( "Project names must be unique and contain some letters or numbers."); } $id = $project->getID(); $collision = id(new PhabricatorProject())->loadOneWhere( '(name = %s OR phrictionSlug = %s) AND id %Q %nd', $name, $slug, $id ? '!=' : 'IS NOT', $id ? $id : null); if ($collision) { $other_name = $collision->getName(); $other_id = $collision->getID(); throw new PhabricatorProjectNameCollisionException( "Project names must be unique. The name '{$name}' is too similar to ". "the name of another project, '{$other_name}' (Project ID: ". "{$other_id}). Choose a unique name."); } } private function setTransactionOldValue( PhabricatorProject $project, PhabricatorProjectTransaction $xaction) { $type = $xaction->getTransactionType(); switch ($type) { case PhabricatorProjectTransactionType::TYPE_NAME: $xaction->setOldValue($project->getName()); break; case PhabricatorProjectTransactionType::TYPE_STATUS: $xaction->setOldValue($project->getStatus()); break; case PhabricatorProjectTransactionType::TYPE_MEMBERS: $member_phids = $project->loadMemberPHIDs(); $project->attachMemberPHIDs($member_phids); $old_value = array_values($member_phids); $xaction->setOldValue($old_value); $new_value = $xaction->getNewValue(); $new_value = array_filter($new_value); $new_value = array_unique($new_value); $new_value = array_values($new_value); $xaction->setNewValue($new_value); break; + case PhabricatorProjectTransactionType::TYPE_CAN_VIEW: + $xaction->setOldValue($project->getViewPolicy()); + break; + case PhabricatorProjectTransactionType::TYPE_CAN_EDIT: + $xaction->setOldValue($project->getEditPolicy()); + break; + case PhabricatorProjectTransactionType::TYPE_CAN_JOIN: + $xaction->setOldValue($project->getJoinPolicy()); + break; default: throw new Exception("Unknown transaction type '{$type}'!"); } return $this; } private function applyTransactionEffect( PhabricatorProject $project, PhabricatorProjectTransaction $xaction) { $type = $xaction->getTransactionType(); switch ($type) { case PhabricatorProjectTransactionType::TYPE_NAME: $project->setName($xaction->getNewValue()); $project->setPhrictionSlug($xaction->getNewValue()); $this->validateName($project); break; case PhabricatorProjectTransactionType::TYPE_STATUS: $project->setStatus($xaction->getNewValue()); break; case PhabricatorProjectTransactionType::TYPE_MEMBERS: $old = array_fill_keys($xaction->getOldValue(), true); $new = array_fill_keys($xaction->getNewValue(), true); $this->addEdges = array_keys(array_diff_key($new, $old)); $this->remEdges = array_keys(array_diff_key($old, $new)); break; + case PhabricatorProjectTransactionType::TYPE_CAN_VIEW: + $project->setViewPolicy($xaction->getNewValue()); + break; + case PhabricatorProjectTransactionType::TYPE_CAN_EDIT: + $project->setEditPolicy($xaction->getNewValue()); + + // You can't edit away your ability to edit the project. + PhabricatorPolicyFilter::mustRetainCapability( + $this->user, + $project, + PhabricatorPolicyCapability::CAN_EDIT); + break; + case PhabricatorProjectTransactionType::TYPE_CAN_JOIN: + $project->setJoinPolicy($xaction->getNewValue()); + break; default: throw new Exception("Unknown transaction type '{$type}'!"); } } private function publishTransactionStory( PhabricatorProject $project, PhabricatorProjectTransaction $xaction) { $related_phids = array( $project->getPHID(), $xaction->getAuthorPHID(), ); id(new PhabricatorFeedStoryPublisher()) ->setStoryType(PhabricatorFeedStoryTypeConstants::STORY_PROJECT) ->setStoryData( array( 'projectPHID' => $project->getPHID(), 'transactionID' => $xaction->getID(), 'type' => $xaction->getTransactionType(), 'old' => $xaction->getOldValue(), 'new' => $xaction->getNewValue(), )) ->setStoryTime(time()) ->setStoryAuthorPHID($xaction->getAuthorPHID()) ->setRelatedPHIDs($related_phids) ->publish(); } private function transactionHasEffect( PhabricatorProjectTransaction $xaction) { return ($xaction->getOldValue() !== $xaction->getNewValue()); } + + /** + * All transactions except joining or leaving a project require edit + * capability. + */ + private function getTransactionRequiresEditCapability( + PhabricatorProjectTransaction $xaction) { + return ($this->isJoinOrLeaveTransaction($xaction) === null); + } + + + /** + * Joining a project requires the join capability. Anyone leave a project. + */ + private function getTransactionRequiresJoinCapability( + PhabricatorProjectTransaction $xaction) { + $type = $this->isJoinOrLeaveTransaction($xaction); + return ($type == 'join'); + } + + + /** + * Returns 'join' if this transaction causes the acting user ONLY to join the + * project. + * + * Returns 'leave' if this transaction causes the acting user ONLY to leave + * the project. + * + * Returns null in all other cases. + */ + private function isJoinOrLeaveTransaction( + PhabricatorProjectTransaction $xaction) { + + $type = $xaction->getTransactionType(); + if ($type != PhabricatorProjectTransactionType::TYPE_MEMBERS) { + return null; + } + + switch ($type) { + case PhabricatorProjectTransactionType::TYPE_MEMBERS: + $old = $xaction->getOldValue(); + $new = $xaction->getNewValue(); + + $add = array_diff($new, $old); + $rem = array_diff($old, $new); + + if (count($add) > 1) { + return null; + } else if (count($add) == 1) { + if (reset($add) != $this->user->getPHID()) { + return null; + } else { + return 'join'; + } + } + + if (count($rem) > 1) { + return null; + } else if (count($rem) == 1) { + if (reset($rem) != $this->user->getPHID()) { + return null; + } else { + return 'leave'; + } + } + break; + } + + return true; + } + }