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 = '
'.$content.'
'; $nav_view->appendChild($content); $header = new PhabricatorProfileHeaderView(); $header->setName($project->getName()); $header->setDescription( phutil_utf8_shorten($profile->getBlurb(), 1024)); $header->setProfilePicture($picture); $action = null; - if (empty($member_map[$user->getPHID()])) { + if (!$project->isUserMember($user->getPHID())) { + $can_join = PhabricatorPolicyCapability::CAN_JOIN; + + if (PhabricatorPolicyFilter::hasCapability($user, $project, $can_join)) { + $class = 'green'; + } else { + $class = 'grey disabled'; + } + $action = phabricator_render_form( $user, array( 'action' => '/project/update/'.$project->getID().'/join/', 'method' => 'post', ), phutil_render_tag( 'button', array( - 'class' => 'green', + 'class' => $class, ), 'Join Project')); } else { $action = javelin_render_tag( 'a', array( 'href' => '/project/update/'.$project->getID().'/leave/', 'sigil' => 'workflow', 'class' => 'grey button', ), 'Leave Project...'); } $header->addAction($action); $header->appendChild($nav_view); return $this->buildStandardPageResponse( $header, array( 'title' => $project->getName().' Project', )); } private function renderAboutPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $viewer = $this->getRequest()->getUser(); $blurb = $profile->getBlurb(); $blurb = phutil_escape_html($blurb); $blurb = str_replace("\n", '
', $blurb); $phids = array($project->getAuthorPHID()); $phids = array_unique($phids); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); $timestamp = phabricator_datetime($project->getDateCreated(), $viewer); $about = '

About

Creator '.$handles[$project->getAuthorPHID()]->renderLink().'
Created '.$timestamp.'
PHID '.phutil_escape_html($project->getPHID()).'
Blurb '.$blurb.'
'; return $about; } private function renderPeoplePage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { - $member_phids = $project->loadMemberPHIDs(); + $member_phids = $project->getMemberPHIDs(); $handles = id(new PhabricatorObjectHandleData($member_phids)) ->loadHandles(); $affiliated = array(); foreach ($handles as $phids => $handle) { $affiliated[] = '
  • '.$handle->renderLink().'
  • '; } if ($affiliated) { $affiliated = ''; } else { $affiliated = '

    No one is affiliated with this project.

    '; } return '
    '. '

    People

    '. '
    '. $affiliated. '
    '. '
    '; } private function renderFeedPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $query = new PhabricatorFeedQuery(); $query->setFilterPHIDs(array($project->getPHID())); $query->setViewer($this->getRequest()->getUser()); $query->setLimit(100); $stories = $query->execute(); if (!$stories) { return 'There are no stories about this project.'; } return $this->renderStories($stories); } private function renderStories(array $stories) { assert_instances_of($stories, 'PhabricatorFeedStory'); $builder = new PhabricatorFeedBuilder($stories); $builder->setUser($this->getRequest()->getUser()); $view = $builder->buildView(); return '
    '. '

    Activity Feed

    '. '
    '. $view->render(). '
    '. '
    '; } private function renderTasksPage( PhabricatorProject $project, PhabricatorProjectProfile $profile) { $query = id(new ManiphestTaskQuery()) ->withProjects(array($project->getPHID())) ->withStatus(ManiphestTaskQuery::STATUS_OPEN) ->setOrderBy(ManiphestTaskQuery::ORDER_PRIORITY) ->setLimit(10) ->setCalculateRows(true); $tasks = $query->execute(); $count = $query->getRowCount(); $phids = mpull($tasks, 'getOwnerPHID'); $phids = array_filter($phids); $handles = id(new PhabricatorObjectHandleData($phids)) ->loadHandles(); $task_views = array(); foreach ($tasks as $task) { $view = id(new ManiphestTaskSummaryView()) ->setTask($task) ->setHandles($handles) ->setUser($this->getRequest()->getUser()); $task_views[] = $view->render(); } if (empty($tasks)) { $task_views = 'No open tasks.'; } else { $task_views = implode('', $task_views); } $open = number_format($count); $more_link = phutil_render_tag( 'a', array( 'href' => '/maniphest/view/all/?projects='.$project->getPHID(), ), "View All Open Tasks \xC2\xBB"); $content = '

    '. "Open Tasks ({$open})". '

    '. '
    '. $task_views. ''. '
    '; return $content; } } diff --git a/src/applications/project/controller/PhabricatorProjectProfileEditController.php b/src/applications/project/controller/PhabricatorProjectProfileEditController.php index f84ae0d2f..b2839fa9b 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileEditController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileEditController.php @@ -1,192 +1,244 @@ 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(); } $img_src = $profile->loadProfileImageURI(); $options = PhabricatorProjectStatus::getStatusMap(); $supported_formats = PhabricatorFile::getTransformableImageFormats(); $e_name = true; $e_image = null; $errors = array(); if ($request->isFormPost()) { try { $xactions = array(); $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_NAME); $xaction->setNewValue($request->getStr('name')); $xactions[] = $xaction; $xaction = new PhabricatorProjectTransaction(); $xaction->setTransactionType( PhabricatorProjectTransactionType::TYPE_STATUS); $xaction->setNewValue($request->getStr('status')); $xactions[] = $xaction; + $xaction = new PhabricatorProjectTransaction(); + $xaction->setTransactionType( + PhabricatorProjectTransactionType::TYPE_CAN_VIEW); + $xaction->setNewValue($request->getStr('can_view')); + $xactions[] = $xaction; + + $xaction = new PhabricatorProjectTransaction(); + $xaction->setTransactionType( + PhabricatorProjectTransactionType::TYPE_CAN_EDIT); + $xaction->setNewValue($request->getStr('can_edit')); + $xactions[] = $xaction; + + $xaction = new PhabricatorProjectTransaction(); + $xaction->setTransactionType( + PhabricatorProjectTransactionType::TYPE_CAN_JOIN); + $xaction->setNewValue($request->getStr('can_join')); + $xactions[] = $xaction; + $editor = new PhabricatorProjectEditor($project); $editor->setUser($user); $editor->applyTransactions($xactions); } catch (PhabricatorProjectNameCollisionException $ex) { $e_name = 'Not Unique'; $errors[] = $ex->getMessage(); } $profile->setBlurb($request->getStr('blurb')); if (!strlen($project->getName())) { $e_name = 'Required'; $errors[] = 'Project name is required.'; } else { $e_name = null; } $default_image = $request->getExists('default_image'); if ($default_image) { $profile->setProfileImagePHID(null); } else if (!empty($_FILES['image'])) { $err = idx($_FILES['image'], 'error'); if ($err != UPLOAD_ERR_NO_FILE) { $file = PhabricatorFile::newFromPHPUpload( $_FILES['image'], array( 'authorPHID' => $user->getPHID(), )); $okay = $file->isTransformableImage(); if ($okay) { $xformer = new PhabricatorImageTransformer(); $xformed = $xformer->executeThumbTransform( $file, $x = 50, $y = 50); $profile->setProfileImagePHID($xformed->getPHID()); } else { $e_image = 'Not Supported'; $errors[] = 'This server only supports these image formats: '. implode(', ', $supported_formats).'.'; } } } if (!$errors) { $project->save(); $profile->setProjectPHID($project->getPHID()); $profile->save(); return id(new AphrontRedirectResponse()) ->setURI('/project/view/'.$project->getID().'/'); } } $error_view = null; if ($errors) { $error_view = new AphrontErrorView(); $error_view->setTitle('Form Errors'); $error_view->setErrors($errors); } $header_name = 'Edit Project'; $title = 'Edit Project'; $action = '/project/edit/'.$project->getID().'/'; $form = new AphrontFormView(); $form ->setID('project-edit-form') ->setUser($user) ->setAction($action) ->setEncType('multipart/form-data') ->appendChild( id(new AphrontFormTextControl()) ->setLabel('Name') ->setName('name') ->setValue($project->getName()) ->setError($e_name)) ->appendChild( id(new AphrontFormSelectControl()) ->setLabel('Project Status') ->setName('status') ->setOptions($options) ->setValue($project->getStatus())) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel('Blurb') ->setName('blurb') ->setValue($profile->getBlurb())) + ->appendChild( + '

    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; + } + }