diff --git a/src/applications/project/controller/PhabricatorProjectController.php b/src/applications/project/controller/PhabricatorProjectController.php index ac406412ad..2fcb276576 100644 --- a/src/applications/project/controller/PhabricatorProjectController.php +++ b/src/applications/project/controller/PhabricatorProjectController.php @@ -1,91 +1,157 @@ project = $project; return $this; } protected function getProject() { return $this->project; } + protected function loadProject() { + $viewer = $this->getViewer(); + $request = $this->getRequest(); + + $id = $request->getURIData('id'); + $slug = $request->getURIData('slug'); + + if ($slug) { + $normal_slug = PhabricatorSlug::normalizeProjectSlug($slug); + $is_abnormal = ($slug !== $normal_slug); + $normal_uri = "/tag/{$normal_slug}/"; + } else { + $is_abnormal = false; + } + + $query = id(new PhabricatorProjectQuery()) + ->setViewer($viewer) + ->needMembers(true) + ->needWatchers(true) + ->needImages(true) + ->needSlugs(true); + + if ($slug) { + $query->withSlugs(array($slug)); + } else { + $query->withIDs(array($id)); + } + + $policy_exception = null; + try { + $project = $query->executeOne(); + } catch (PhabricatorPolicyException $ex) { + $policy_exception = $ex; + $project = null; + } + + if (!$project) { + // This project legitimately does not exist, so just 404 the user. + if (!$policy_exception) { + return new Aphront404Response(); + } + + // Here, the project exists but the user can't see it. If they are + // using a non-canonical slug to view the project, redirect to the + // canonical slug. If they're already using the canonical slug, rethrow + // the exception to give them the policy error. + if ($is_abnormal) { + return id(new AphrontRedirectResponse())->setURI($normal_uri); + } else { + throw $policy_exception; + } + } + + // The user can view the project, but is using a noncanonical slug. + // Redirect to the canonical slug. + $primary_slug = $project->getPrimarySlug(); + if ($slug && ($slug !== $primary_slug)) { + $primary_uri = "/tag/{$primary_slug}/"; + return id(new AphrontRedirectResponse())->setURI($primary_uri); + } + + $this->setProject($project); + + return null; + } + public function buildApplicationMenu() { return $this->buildSideNavView(true)->getMenu(); } public function buildSideNavView($for_app = false) { $project = $this->getProject(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); $viewer = $this->getViewer(); $id = null; if ($for_app) { if ($project) { $id = $project->getID(); $nav->addFilter("profile/{$id}/", pht('Profile')); $nav->addFilter("board/{$id}/", pht('Workboard')); $nav->addFilter("members/{$id}/", pht('Members')); $nav->addFilter("feed/{$id}/", pht('Feed')); $nav->addFilter("details/{$id}/", pht('Edit Details')); } $nav->addFilter('create', pht('Create Project')); } if (!$id) { id(new PhabricatorProjectSearchEngine()) ->setViewer($viewer) ->addNavigationItems($nav->getMenu()); } $nav->selectFilter(null); return $nav; } public function buildIconNavView(PhabricatorProject $project) { $this->setProject($project); $viewer = $this->getViewer(); $id = $project->getID(); $picture = $project->getProfileImageURI(); $name = $project->getName(); $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array($project->getPHID())) ->execute(); if ($columns) { $board_icon = 'fa-columns'; } else { $board_icon = 'fa-columns grey'; } $nav = new AphrontSideNavFilterView(); $nav->setIconNav(true); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); $nav->addIcon("profile/{$id}/", $name, null, $picture); $class = 'PhabricatorManiphestApplication'; if (PhabricatorApplication::isClassInstalledForViewer($class, $viewer)) { $phid = $project->getPHID(); $nav->addIcon("board/{$id}/", pht('Workboard'), $board_icon); $query_uri = urisprintf( '/maniphest/?statuses=open()&projects=%s#R', $phid); $nav->addIcon(null, pht('Open Tasks'), 'fa-anchor', null, $query_uri); } $nav->addIcon("feed/{$id}/", pht('Feed'), 'fa-newspaper-o'); $nav->addIcon("members/{$id}/", pht('Members'), 'fa-group'); $nav->addIcon("details/{$id}/", pht('Edit Details'), 'fa-pencil'); return $nav; } } diff --git a/src/applications/project/controller/PhabricatorProjectFeedController.php b/src/applications/project/controller/PhabricatorProjectFeedController.php index efd82d31d0..5cd74094fc 100644 --- a/src/applications/project/controller/PhabricatorProjectFeedController.php +++ b/src/applications/project/controller/PhabricatorProjectFeedController.php @@ -1,89 +1,60 @@ getUser(); + $viewer = $request->getUser(); - $query = id(new PhabricatorProjectQuery()) - ->setViewer($user) - ->needMembers(true) - ->needWatchers(true) - ->needImages(true) - ->needSlugs(true); - $id = $request->getURIData('id'); - $slug = $request->getURIData('slug'); - if ($slug) { - $query->withSlugs(array($slug)); - } else { - $query->withIDs(array($id)); - } - $project = $query->executeOne(); - if (!$project) { - return new Aphront404Response(); - } - if ($slug && $slug != $project->getPrimarySlug()) { - return id(new AphrontRedirectResponse()) - ->setURI('/tag/'.$project->getPrimarySlug().'/'); + $response = $this->loadProject(); + if ($response) { + return $response; } - $query = new PhabricatorFeedQuery(); - $query->setFilterPHIDs( - array( - $project->getPHID(), - )); - $query->setLimit(50); - $query->setViewer($request->getUser()); - $stories = $query->execute(); + $project = $this->getProject(); + $id = $project->getID(); + + $stories = id(new PhabricatorFeedQuery()) + ->setViewer($viewer) + ->setFilterPHIDs( + array( + $project->getPHID(), + )) + ->setLimit(50) + ->execute(); + $feed = $this->renderStories($stories); $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Project Activity')) ->appendChild($feed); $nav = $this->buildIconNavView($project); $nav->selectFilter("feed/{$id}/"); $nav->appendChild($box); return $this->buildApplicationPage( $nav, array( 'title' => $project->getName(), )); } - private function renderFeedPage(PhabricatorProject $project) { - - $query = new PhabricatorFeedQuery(); - $query->setFilterPHIDs(array($project->getPHID())); - $query->setViewer($this->getRequest()->getUser()); - $query->setLimit(100); - $stories = $query->execute(); - - if (!$stories) { - return pht('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()); $builder->setShowHovercards(true); $view = $builder->buildView(); return phutil_tag_div( 'profile-feed', $view->render()); } - } diff --git a/src/applications/project/controller/PhabricatorProjectMembersEditController.php b/src/applications/project/controller/PhabricatorProjectMembersEditController.php index c7a9144188..f007650e4a 100644 --- a/src/applications/project/controller/PhabricatorProjectMembersEditController.php +++ b/src/applications/project/controller/PhabricatorProjectMembersEditController.php @@ -1,154 +1,150 @@ getViewer(); $id = $request->getURIData('id'); $project = id(new PhabricatorProjectQuery()) ->setViewer($viewer) ->withIDs(array($id)) ->needMembers(true) ->needImages(true) - ->requireCapabilities( - array( - PhabricatorPolicyCapability::CAN_VIEW, - )) ->executeOne(); if (!$project) { return new Aphront404Response(); } $member_phids = $project->getMemberPHIDs(); if ($request->isFormPost()) { $member_spec = array(); $remove = $request->getStr('remove'); if ($remove) { $member_spec['-'] = array_fuse(array($remove)); } $add_members = $request->getArr('phids'); if ($add_members) { $member_spec['+'] = array_fuse($add_members); } $type_member = PhabricatorProjectProjectHasMemberEdgeType::EDGECONST; $xactions = array(); $xactions[] = id(new PhabricatorProjectTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $type_member) ->setNewValue($member_spec); $editor = id(new PhabricatorProjectTransactionEditor($project)) ->setActor($viewer) ->setContentSourceFromRequest($request) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true) ->applyTransactions($project, $xactions); return id(new AphrontRedirectResponse()) ->setURI($request->getRequestURI()); } $member_phids = array_reverse($member_phids); $handles = $this->loadViewerHandles($member_phids); $state = array(); foreach ($handles as $handle) { $state[] = array( 'phid' => $handle->getPHID(), 'name' => $handle->getFullName(), ); } $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $form_box = null; $title = pht('Add Members'); if ($can_edit) { $header_name = pht('Edit Members'); $view_uri = $this->getApplicationURI('profile/'.$project->getID().'/'); $form = new AphrontFormView(); $form ->setUser($viewer) ->appendControl( id(new AphrontFormTokenizerControl()) ->setName('phids') ->setLabel(pht('Add Members')) ->setDatasource(new PhabricatorPeopleDatasource())) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($view_uri) ->setValue(pht('Add Members'))); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setForm($form); } $member_list = $this->renderMemberList($project, $handles); $nav = $this->buildIconNavView($project); $nav->selectFilter("members/{$id}/"); $nav->appendChild($form_box); $nav->appendChild($member_list); return $this->buildApplicationPage( $nav, array( 'title' => $title, )); } private function renderMemberList( PhabricatorProject $project, array $handles) { $request = $this->getRequest(); $viewer = $request->getUser(); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $list = id(new PHUIObjectItemListView()) ->setNoDataString(pht('This project does not have any members.')); foreach ($handles as $handle) { $remove_uri = $this->getApplicationURI( '/members/'.$project->getID().'/remove/?phid='.$handle->getPHID()); $item = id(new PHUIObjectItemView()) ->setHeader($handle->getFullName()) ->setHref($handle->getURI()) ->setImageURI($handle->getImageURI()); if ($can_edit) { $item->addAction( id(new PHUIListItemView()) ->setIcon('fa-times') ->setName(pht('Remove')) ->setHref($remove_uri) ->setWorkflow(true)); } $list->addItem($item); } $box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Members')) ->setObjectList($list); return $box; } } diff --git a/src/applications/project/controller/PhabricatorProjectProfileController.php b/src/applications/project/controller/PhabricatorProjectProfileController.php index 89880bdd1a..cc246b5a2d 100644 --- a/src/applications/project/controller/PhabricatorProjectProfileController.php +++ b/src/applications/project/controller/PhabricatorProjectProfileController.php @@ -1,223 +1,206 @@ getUser(); - - $query = id(new PhabricatorProjectQuery()) - ->setViewer($user) - ->needMembers(true) - ->needWatchers(true) - ->needImages(true) - ->needSlugs(true); - $id = $request->getURIData('id'); - $slug = $request->getURIData('slug'); - if ($slug) { - $query->withSlugs(array($slug)); - } else { - $query->withIDs(array($id)); - } - $project = $query->executeOne(); - if (!$project) { - return new Aphront404Response(); - } - if ($slug && $slug != $project->getPrimarySlug()) { - return id(new AphrontRedirectResponse()) - ->setURI('/tag/'.$project->getPrimarySlug().'/'); + $viewer = $request->getUser(); + + $response = $this->loadProject(); + if ($response) { + return $response; } + $project = $this->getProject(); + $id = $project->getID(); $picture = $project->getProfileImageURI(); $header = id(new PHUIHeaderView()) ->setHeader($project->getName()) - ->setUser($user) + ->setUser($viewer) ->setPolicyObject($project) ->setImage($picture); if ($project->getStatus() == PhabricatorProjectStatus::STATUS_ACTIVE) { $header->setStatus('fa-check', 'bluegrey', pht('Active')); } else { $header->setStatus('fa-ban', 'red', pht('Archived')); } $actions = $this->buildActionListView($project); $properties = $this->buildPropertyListView($project, $actions); $object_box = id(new PHUIObjectBoxView()) ->setHeader($header) ->addPropertyList($properties); $timeline = $this->buildTransactionTimeline( $project, new PhabricatorProjectTransactionQuery()); $timeline->setShouldTerminate(true); $nav = $this->buildIconNavView($project); $nav->selectFilter("profile/{$id}/"); - $nav->appendChild($object_box); - $nav->appendChild($timeline); - - return $this->buildApplicationPage( - $nav, - array( - 'title' => $project->getName(), - 'pageObjects' => array($project->getPHID()), - )); + + return $this->newPage() + ->setNavigation($nav) + ->setTitle($project->getName()) + ->setPageObjectPHIDs(array($project->getPHID())) + ->appendChild($object_box) + ->appendChild($timeline); } private function buildActionListView(PhabricatorProject $project) { $request = $this->getRequest(); $viewer = $request->getUser(); $id = $project->getID(); $view = id(new PhabricatorActionListView()) ->setUser($viewer) ->setObject($project); $can_edit = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_EDIT); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Details')) ->setIcon('fa-pencil') ->setHref($this->getApplicationURI("details/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Edit Picture')) ->setIcon('fa-picture-o') ->setHref($this->getApplicationURI("picture/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(!$can_edit)); if ($project->isArchived()) { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Activate Project')) ->setIcon('fa-check') ->setHref($this->getApplicationURI("archive/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(true)); } else { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Archive Project')) ->setIcon('fa-ban') ->setHref($this->getApplicationURI("archive/{$id}/")) ->setDisabled(!$can_edit) ->setWorkflow(true)); } $action = null; if (!$project->isUserMember($viewer->getPHID())) { $can_join = PhabricatorPolicyFilter::hasCapability( $viewer, $project, PhabricatorPolicyCapability::CAN_JOIN); $action = id(new PhabricatorActionView()) ->setUser($viewer) ->setRenderAsForm(true) ->setHref('/project/update/'.$project->getID().'/join/') ->setIcon('fa-plus') ->setDisabled(!$can_join) ->setName(pht('Join Project')); $view->addAction($action); } else { $action = id(new PhabricatorActionView()) ->setWorkflow(true) ->setHref('/project/update/'.$project->getID().'/leave/') ->setIcon('fa-times') ->setName(pht('Leave Project...')); $view->addAction($action); if (!$project->isUserWatcher($viewer->getPHID())) { $action = id(new PhabricatorActionView()) ->setWorkflow(true) ->setHref('/project/watch/'.$project->getID().'/') ->setIcon('fa-eye') ->setName(pht('Watch Project')); $view->addAction($action); } else { $action = id(new PhabricatorActionView()) ->setWorkflow(true) ->setHref('/project/unwatch/'.$project->getID().'/') ->setIcon('fa-eye-slash') ->setName(pht('Unwatch Project')); $view->addAction($action); } } return $view; } private function buildPropertyListView( PhabricatorProject $project, PhabricatorActionListView $actions) { $request = $this->getRequest(); $viewer = $request->getUser(); $view = id(new PHUIPropertyListView()) ->setUser($viewer) ->setObject($project) ->setActionList($actions); $hashtags = array(); foreach ($project->getSlugs() as $slug) { $hashtags[] = id(new PHUITagView()) ->setType(PHUITagView::TYPE_OBJECT) ->setName('#'.$slug->getSlug()); } $view->addProperty(pht('Hashtags'), phutil_implode_html(' ', $hashtags)); $view->addProperty( pht('Members'), $project->getMemberPHIDs() ? $viewer ->renderHandleList($project->getMemberPHIDs()) ->setAsInline(true) : phutil_tag('em', array(), pht('None'))); $view->addProperty( pht('Watchers'), $project->getWatcherPHIDs() ? $viewer ->renderHandleList($project->getWatcherPHIDs()) ->setAsInline(true) : phutil_tag('em', array(), pht('None'))); $descriptions = PhabricatorPolicyQuery::renderPolicyDescriptions( $viewer, $project); $view->addProperty( pht('Looks Like'), $viewer->renderHandle($project->getPHID())->setAsTag(true)); $view->addProperty( pht('Joinable By'), $descriptions[PhabricatorPolicyCapability::CAN_JOIN]); $field_list = PhabricatorCustomField::getObjectFields( $project, PhabricatorCustomField::ROLE_VIEW); $field_list->appendFieldsToPropertyList($project, $viewer, $view); return $view; } } diff --git a/src/applications/project/controller/PhabricatorProjectViewController.php b/src/applications/project/controller/PhabricatorProjectViewController.php index bfc9827ab5..087971878b 100644 --- a/src/applications/project/controller/PhabricatorProjectViewController.php +++ b/src/applications/project/controller/PhabricatorProjectViewController.php @@ -1,90 +1,43 @@ getRequest(); $viewer = $request->getViewer(); - $query = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->needMembers(true) - ->needWatchers(true) - ->needImages(true) - ->needSlugs(true); - $id = $request->getURIData('id'); - $slug = $request->getURIData('slug'); - if ($slug) { - $query->withSlugs(array($slug)); - } else { - $query->withIDs(array($id)); - } - $project = $query->executeOne(); - if (!$project) { - - // If this request corresponds to a project but just doesn't have the - // slug quite right, redirect to the proper URI. - $uri = $this->getNormalizedURI($slug); - if ($uri !== null) { - return id(new AphrontRedirectResponse())->setURI($uri); - } - - return new Aphront404Response(); + $response = $this->loadProject(); + if ($response) { + return $response; } + $project = $this->getProject(); $columns = id(new PhabricatorProjectColumnQuery()) ->setViewer($viewer) ->withProjectPHIDs(array($project->getPHID())) ->execute(); if ($columns) { $controller = 'board'; } else { $controller = 'profile'; } switch ($controller) { case 'board': $controller_object = new PhabricatorProjectBoardViewController(); break; case 'profile': default: $controller_object = new PhabricatorProjectProfileController(); break; } return $this->delegateToController($controller_object); } - private function getNormalizedURI($slug) { - if (!strlen($slug)) { - return null; - } - - $normal = PhabricatorSlug::normalizeProjectSlug($slug); - if ($normal === $slug) { - return null; - } - - $viewer = $this->getViewer(); - - // Do execute() instead of executeOne() here so we canonicalize before - // raising a policy exception. This is a little more polished than letting - // the user hit the error on any variant of the slug. - - $projects = id(new PhabricatorProjectQuery()) - ->setViewer($viewer) - ->withSlugs(array($normal)) - ->execute(); - if (!$projects) { - return null; - } - - return "/tag/{$normal}/"; - } - }