Index: resources/sql/patches/policy-project.sql =================================================================== --- /dev/null +++ resources/sql/patches/policy-project.sql @@ -0,0 +1,8 @@ +ALTER TABLE `{$NAMESPACE}_project`.`project` + ADD `viewPolicy` varchar(64) COLLATE utf8_bin; + +ALTER TABLE `{$NAMESPACE}_project`.`project` + ADD `editPolicy` varchar(64) COLLATE utf8_bin; + +ALTER TABLE `{$NAMESPACE}_project`.`project` + ADD `joinPolicy` varchar(64) COLLATE utf8_bin; Index: src/applications/project/query/PhabricatorProjectQuery.php =================================================================== --- src/applications/project/query/PhabricatorProjectQuery.php +++ src/applications/project/query/PhabricatorProjectQuery.php @@ -68,27 +68,50 @@ $table = new PhabricatorProject(); $conn_r = $table->establishConnection('r'); + // NOTE: Because visibility checks for projects depend on whether or not + // the user is a project member, we always load their membership. If we're + // loading all members anyway we can piggyback on that; otherwise we + // do an explicit join. + + $select_clause = ''; + if (!$this->needMembers) { + $select_clause = ', vm.dst viewerIsMember'; + } + $data = queryfx_all( $conn_r, - 'SELECT p.* FROM %T p %Q %Q %Q %Q %Q', + 'SELECT p.* %Q FROM %T p %Q %Q %Q %Q %Q', + $select_clause, $table->getTableName(), $this->buildJoinClause($conn_r), $this->buildWhereClause($conn_r), $this->buildGroupClause($conn_r), - 'ORDER BY name', + $this->buildOrderClause($conn_r), $this->buildLimitClause($conn_r)); $projects = $table->loadAllFromArray($data); - if ($projects && $this->needMembers) { - $etype = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER; - $members = id(new PhabricatorEdgeQuery()) - ->withSourcePHIDs(mpull($projects, 'getPHID')) - ->withEdgeTypes(array($etype)) - ->execute(); - foreach ($projects as $project) { - $phid = $project->getPHID(); - $project->attachMemberPHIDs(array_keys($members[$phid][$etype])); + if ($projects) { + $viewer_phid = $this->getViewer()->getPHID(); + if ($this->needMembers) { + $etype = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER; + $members = id(new PhabricatorEdgeQuery()) + ->withSourcePHIDs(mpull($projects, 'getPHID')) + ->withEdgeTypes(array($etype)) + ->execute(); + foreach ($projects as $project) { + $phid = $project->getPHID(); + $project->attachMemberPHIDs(array_keys($members[$phid][$etype])); + $project->setIsUserMember( + $viewer_phid, + isset($members[$phid][$etype][$viewer_phid])); + } + } else { + foreach ($data as $row) { + $projects[$row['id']]->setIsUserMember( + $viewer_phid, + ($row['viewerIsMember'] !== null)); + } } } @@ -139,8 +162,7 @@ if ($this->memberPHIDs) { $where[] = qsprintf( $conn_r, - 'e.type = %s AND e.dst IN (%Ls)', - PhabricatorEdgeConfig::TYPE_PROJ_MEMBER, + 'e.dst IN (%Ls)', $this->memberPHIDs); } @@ -160,11 +182,21 @@ private function buildJoinClause($conn_r) { $joins = array(); + if (!$this->needMembers) { + $joins[] = qsprintf( + $conn_r, + 'LEFT JOIN %T vm ON vm.src = p.phid AND vm.type = %d AND vm.dst = %s', + PhabricatorEdgeConfig::TABLE_NAME_EDGE, + PhabricatorEdgeConfig::TYPE_PROJ_MEMBER, + $this->getViewer()->getPHID()); + } + if ($this->memberPHIDs) { $joins[] = qsprintf( $conn_r, - 'JOIN %T e ON e.src = p.phid', - PhabricatorEdgeConfig::TABLE_NAME_EDGE); + 'JOIN %T e ON e.src = p.phid AND e.type = %d', + PhabricatorEdgeConfig::TABLE_NAME_EDGE, + PhabricatorEdgeConfig::TYPE_PROJ_MEMBER); } return implode(' ', $joins); Index: src/applications/project/storage/PhabricatorProject.php =================================================================== --- src/applications/project/storage/PhabricatorProject.php +++ src/applications/project/storage/PhabricatorProject.php @@ -26,23 +26,69 @@ protected $subprojectPHIDs = array(); protected $phrictionSlug; + protected $viewPolicy; + protected $editPolicy; + protected $joinPolicy; + private $subprojectsNeedUpdate; private $memberPHIDs; + private $sparseMembers = array(); public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + PhabricatorPolicyCapability::CAN_JOIN, ); } public function getPolicy($capability) { - return PhabricatorPolicies::POLICY_USER; + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + return $this->getViewPolicy(); + case PhabricatorPolicyCapability::CAN_EDIT: + return $this->getEditPolicy(); + case PhabricatorPolicyCapability::CAN_JOIN: + return $this->getJoinPolicy(); + } } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { + + switch ($capability) { + case PhabricatorPolicyCapability::CAN_VIEW: + if ($this->isUserMember($viewer->getPHID())) { + // Project members can always view a project. + return true; + } + break; + case PhabricatorPolicyCapability::CAN_EDIT: + break; + case PhabricatorPolicyCapability::CAN_JOIN: + $can_edit = PhabricatorPolicyCapability::CAN_EDIT; + if (PhabricatorPolicyFilter::hasCapability($viewer, $this, $can_edit)) { + // Project editors can always join a project. + return true; + } + break; + } + return false; } + public function isUserMember($user_phid) { + if (!isset($this->sparseMembers[$user_phid])) { + throw new Exception( + "Call setIsUserMember() before isUserMember()!"); + } + return $this->sparseMembers[$user_phid]; + } + + public function setIsUserMember($user_phid, $is_member) { + $this->sparseMembers[$user_phid] = $is_member; + return $this; + } + public function getConfiguration() { return array( self::CONFIG_AUX_PHID => true, Index: src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php =================================================================== --- src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -960,6 +960,10 @@ 'type' => 'sql', 'name' => $this->getPatchPath('ponder.sql') ), + 'policy-project.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('policy-project.sql'), + ), ); }