Page MenuHomePhorge

No OneTemporary

diff --git a/src/applications/project/constants/PhabricatorProjectTransactionType.php b/src/applications/project/constants/PhabricatorProjectTransactionType.php
index 49ef0a9d81..77170e6f0f 100644
--- a/src/applications/project/constants/PhabricatorProjectTransactionType.php
+++ b/src/applications/project/constants/PhabricatorProjectTransactionType.php
@@ -1,26 +1,29 @@
<?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 PhabricatorProjectTransactionType
extends PhabricatorProjectConstants {
const TYPE_NAME = 'name';
const TYPE_MEMBERS = 'members';
const TYPE_STATUS = 'status';
+ const TYPE_CAN_VIEW = 'canview';
+ const TYPE_CAN_EDIT = 'canedit';
+ const TYPE_CAN_JOIN = 'canjoin';
}
diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php
index 5b404a152b..2b7736ab74 100644
--- a/src/applications/project/controller/PhabricatorProjectController.php
+++ b/src/applications/project/controller/PhabricatorProjectController.php
@@ -1,63 +1,82 @@
<?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.
* 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.
*/
abstract class PhabricatorProjectController extends PhabricatorController {
public function buildStandardPageResponse($view, array $data) {
$page = $this->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 02f9a1239f..5e3aad4861 100644
--- a/src/applications/project/controller/PhabricatorProjectMembersEditController.php
+++ b/src/applications/project/controller/PhabricatorProjectMembersEditController.php
@@ -1,172 +1,180 @@
<?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 PhabricatorProjectMembersEditController
extends PhabricatorProjectController {
public function willProcessRequest(array $data) {
$this->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('<br />');
$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 608cbbe4ab..241cb69518 100644
--- a/src/applications/project/controller/PhabricatorProjectProfileController.php
+++ b/src/applications/project/controller/PhabricatorProjectProfileController.php
@@ -1,291 +1,305 @@
<?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 PhabricatorProjectProfileController
extends PhabricatorProjectController {
private $id;
private $page;
public function willProcessRequest(array $data) {
$this->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 = '<div style="padding: 1em;">'.$content.'</div>';
$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", '<br />', $blurb);
$phids = array($project->getAuthorPHID());
$phids = array_unique($phids);
$handles = id(new PhabricatorObjectHandleData($phids))
->loadHandles();
$timestamp = phabricator_datetime($project->getDateCreated(), $viewer);
$about =
'<div class="phabricator-profile-info-group">
<h1 class="phabricator-profile-info-header">About</h1>
<div class="phabricator-profile-info-pane">
<table class="phabricator-profile-info-table">
<tr>
<th>Creator</th>
<td>'.$handles[$project->getAuthorPHID()]->renderLink().'</td>
</tr>
<tr>
<th>Created</th>
<td>'.$timestamp.'</td>
</tr>
<tr>
<th>PHID</th>
<td>'.phutil_escape_html($project->getPHID()).'</td>
</tr>
<tr>
<th>Blurb</th>
<td>'.$blurb.'</td>
</tr>
</table>
</div>
</div>';
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[] = '<li>'.$handle->renderLink().'</li>';
}
if ($affiliated) {
$affiliated = '<ul>'.implode("\n", $affiliated).'</ul>';
} else {
$affiliated = '<p><em>No one is affiliated with this project.</em></p>';
}
return
'<div class="phabricator-profile-info-group">'.
'<h1 class="phabricator-profile-info-header">People</h1>'.
'<div class="phabricator-profile-info-pane">'.
$affiliated.
'</div>'.
'</div>';
}
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
'<div class="phabricator-profile-info-group">'.
'<h1 class="phabricator-profile-info-header">Activity Feed</h1>'.
'<div class="phabricator-profile-info-pane">'.
$view->render().
'</div>'.
'</div>';
}
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 = '<em>No open tasks.</em>';
} 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 =
'<div class="phabricator-profile-info-group">
<h1 class="phabricator-profile-info-header">'.
"Open Tasks ({$open})".
'</h1>'.
'<div class="phabricator-profile-info-pane">'.
$task_views.
'<div class="phabricator-profile-info-pane-more-link">'.
$more_link.
'</div>'.
'</div>
</div>';
return $content;
}
}
diff --git a/src/applications/project/controller/PhabricatorProjectProfileEditController.php b/src/applications/project/controller/PhabricatorProjectProfileEditController.php
index f84ae0d2f3..b2839fa9b8 100644
--- a/src/applications/project/controller/PhabricatorProjectProfileEditController.php
+++ b/src/applications/project/controller/PhabricatorProjectProfileEditController.php
@@ -1,192 +1,244 @@
<?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 PhabricatorProjectProfileEditController
extends PhabricatorProjectController {
public function willProcessRequest(array $data) {
$this->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(
+ '<p class="aphront-form-instructions">NOTE: Policy settings are not '.
+ 'yet fully implemented. Some interfaces still ignore these settings, '.
+ 'particularly "Visible To".</p>')
+ ->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 b12bcc8fa7..41d1685f80 100644
--- a/src/applications/project/controller/PhabricatorProjectUpdateController.php
+++ b/src/applications/project/controller/PhabricatorProjectUpdateController.php
@@ -1,88 +1,94 @@
<?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 PhabricatorProjectUpdateController
extends PhabricatorProjectController {
private $id;
private $action;
public function willProcessRequest(array $data) {
$this->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(
'<p>Your tremendous contributions to this project will be sorely '.
'missed. Are you sure you want to leave?</p>');
$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 4c8646d9c6..3282520aa4 100644
--- a/src/applications/project/editor/PhabricatorProjectEditor.php
+++ b/src/applications/project/editor/PhabricatorProjectEditor.php
@@ -1,267 +1,394 @@
<?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 PhabricatorProjectEditor {
private $project;
private $user;
private $projectName;
private $addEdges = array();
private $remEdges = array();
public static function applyJoinProject(
PhabricatorProject $project,
PhabricatorUser $user) {
$members = $project->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;
+ }
+
}

File Metadata

Mime Type
text/x-diff
Expires
Jan 19 2025, 22:40 (6 w, 3 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1129495
Default Alt Text
(43 KB)

Event Timeline