diff --git a/src/applications/phid/query/PhabricatorHandleQuery.php b/src/applications/phid/query/PhabricatorHandleQuery.php index 296116312b..2635b84298 100644 --- a/src/applications/phid/query/PhabricatorHandleQuery.php +++ b/src/applications/phid/query/PhabricatorHandleQuery.php @@ -1,74 +1,88 @@ <?php final class PhabricatorHandleQuery extends PhabricatorCursorPagedPolicyAwareQuery { + private $objectCapabilities; private $phids = array(); public function withPHIDs(array $phids) { $this->phids = $phids; return $this; } + public function requireObjectCapabilities(array $capabilities) { + $this->objectCapabilities = $capabilities; + return $this; + } + + protected function getRequiredObjectCapabilities() { + if ($this->objectCapabilities) { + return $this->objectCapabilities; + } + return $this->getRequiredCapabilities(); + } + protected function loadPage() { $types = PhabricatorPHIDType::getAllTypes(); $phids = array_unique($this->phids); if (!$phids) { return array(); } $object_query = id(new PhabricatorObjectQuery()) ->withPHIDs($phids) + ->requireCapabilities($this->getRequiredObjectCapabilities()) ->setViewer($this->getViewer()); $objects = $object_query->execute(); $filtered = $object_query->getPolicyFilteredPHIDs(); $groups = array(); foreach ($phids as $phid) { $type = phid_get_type($phid); $groups[$type][] = $phid; } $results = array(); foreach ($groups as $type => $phid_group) { $handles = array(); foreach ($phid_group as $key => $phid) { if (isset($handles[$phid])) { unset($phid_group[$key]); // The input had a duplicate PHID; just skip it. continue; } $handles[$phid] = id(new PhabricatorObjectHandle()) ->setType($type) ->setPHID($phid); if (isset($objects[$phid])) { $handles[$phid]->setComplete(true); } else if (isset($filtered[$phid])) { $handles[$phid]->setPolicyFiltered(true); } } if (isset($types[$type])) { $type_objects = array_select_keys($objects, $phid_group); if ($type_objects) { $have_object_phids = array_keys($type_objects); $types[$type]->loadHandles( $this, array_select_keys($handles, $have_object_phids), $type_objects); } } $results += $handles; } return $results; } public function getQueryApplicationClass() { return null; } } diff --git a/src/applications/search/application/PhabricatorSearchApplication.php b/src/applications/search/application/PhabricatorSearchApplication.php index 51d5e9780c..f350438616 100644 --- a/src/applications/search/application/PhabricatorSearchApplication.php +++ b/src/applications/search/application/PhabricatorSearchApplication.php @@ -1,48 +1,48 @@ <?php final class PhabricatorSearchApplication extends PhabricatorApplication { public function getBaseURI() { return '/search/'; } public function getName() { return pht('Search'); } public function getShortDescription() { return pht('Full-Text Search'); } public function getFlavorText() { return pht('Find stuff in big piles.'); } public function getFontIcon() { return 'fa-search'; } public function isLaunchable() { return false; } public function getRoutes() { return array( '/search/' => array( '(?:query/(?P<queryKey>[^/]+)/)?' => 'PhabricatorSearchController', 'attach/(?P<phid>[^/]+)/(?P<type>\w+)/(?:(?P<action>\w+)/)?' => 'PhabricatorSearchAttachController', - 'select/(?P<type>\w+)/' + 'select/(?P<type>\w+)/(?:(?P<action>\w+)/)?' => 'PhabricatorSearchSelectController', 'index/(?P<phid>[^/]+)/' => 'PhabricatorSearchIndexController', 'hovercard/(?P<mode>retrieve|test)/' => 'PhabricatorSearchHovercardController', 'edit/(?P<queryKey>[^/]+)/' => 'PhabricatorSearchEditController', 'delete/(?P<queryKey>[^/]+)/(?P<engine>[^/]+)/' => 'PhabricatorSearchDeleteController', 'order/(?P<engine>[^/]+)/' => 'PhabricatorSearchOrderController', ), ); } } diff --git a/src/applications/search/controller/PhabricatorSearchAttachController.php b/src/applications/search/controller/PhabricatorSearchAttachController.php index e08fd7734c..b2e0a92b9d 100644 --- a/src/applications/search/controller/PhabricatorSearchAttachController.php +++ b/src/applications/search/controller/PhabricatorSearchAttachController.php @@ -1,335 +1,324 @@ <?php final class PhabricatorSearchAttachController extends PhabricatorSearchBaseController { - private $phid; - private $type; - private $action; - - const ACTION_ATTACH = 'attach'; - const ACTION_MERGE = 'merge'; - const ACTION_DEPENDENCIES = 'dependencies'; - const ACTION_BLOCKS = 'blocks'; - const ACTION_EDGE = 'edge'; - - public function willProcessRequest(array $data) { - $this->phid = $data['phid']; - $this->type = $data['type']; - $this->action = idx($data, 'action', self::ACTION_ATTACH); - } - - public function processRequest() { - - $request = $this->getRequest(); - $user = $request->getUser(); + public function handleRequest(AphrontRequest $request) { + $user = $request->getUser(); + $phid = $request->getURIData('phid'); + $attach_type = $request->getURIData('type'); + $action = $request->getURIData('action', self::ACTION_ATTACH); $handle = id(new PhabricatorHandleQuery()) ->setViewer($user) - ->withPHIDs(array($this->phid)) + ->withPHIDs(array($phid)) ->executeOne(); $object_type = $handle->getType(); - $attach_type = $this->type; $object = id(new PhabricatorObjectQuery()) ->setViewer($user) - ->withPHIDs(array($this->phid)) + ->withPHIDs(array($phid)) ->executeOne(); if (!$object) { return new Aphront404Response(); } $edge_type = null; - switch ($this->action) { + switch ($action) { case self::ACTION_EDGE: case self::ACTION_DEPENDENCIES: case self::ACTION_BLOCKS: case self::ACTION_ATTACH: $edge_type = $this->getEdgeType($object_type, $attach_type); break; } if ($request->isFormPost()) { $phids = explode(';', $request->getStr('phids')); $phids = array_filter($phids); $phids = array_values($phids); if ($edge_type) { if (!$object instanceof PhabricatorApplicationTransactionInterface) { throw new Exception( pht( 'Expected object ("%s") to implement interface "%s".', get_class($object), 'PhabricatorApplicationTransactionInterface')); } $old_phids = PhabricatorEdgeQuery::loadDestinationPHIDs( - $this->phid, + $phid, $edge_type); $add_phids = $phids; $rem_phids = array_diff($old_phids, $add_phids); $txn_editor = $object->getApplicationTransactionEditor() ->setActor($user) ->setContentSourceFromRequest($request) ->setContinueOnMissingFields(true) ->setContinueOnNoEffect(true); $txn_template = $object->getApplicationTransactionTemplate() ->setTransactionType(PhabricatorTransactions::TYPE_EDGE) ->setMetadataValue('edge:type', $edge_type) ->setNewValue(array( '+' => array_fuse($add_phids), '-' => array_fuse($rem_phids), )); try { $txn_editor->applyTransactions( $object->getApplicationTransactionObject(), array($txn_template)); } catch (PhabricatorEdgeCycleException $ex) { $this->raiseGraphCycleException($ex); } return id(new AphrontReloadResponse())->setURI($handle->getURI()); } else { return $this->performMerge($object, $handle, $phids); } } else { if ($edge_type) { $phids = PhabricatorEdgeQuery::loadDestinationPHIDs( - $this->phid, + $phid, $edge_type); } else { // This is a merge. $phids = array(); } } - $strings = $this->getStrings(); + $strings = $this->getStrings($attach_type, $action); $handles = $this->loadViewerHandles($phids); $obj_dialog = new PhabricatorObjectSelectorDialog(); $obj_dialog ->setUser($user) ->setHandles($handles) - ->setFilters($this->getFilters($strings)) + ->setFilters($this->getFilters($strings, $attach_type)) ->setSelectedFilter($strings['selected']) - ->setExcluded($this->phid) + ->setExcluded($phid) ->setCancelURI($handle->getURI()) - ->setSearchURI('/search/select/'.$attach_type.'/') + ->setSearchURI('/search/select/'.$attach_type.'/'.$action.'/') ->setTitle($strings['title']) ->setHeader($strings['header']) ->setButtonText($strings['button']) ->setInstructions($strings['instructions']); $dialog = $obj_dialog->buildDialog(); return id(new AphrontDialogResponse())->setDialog($dialog); } private function performMerge( ManiphestTask $task, PhabricatorObjectHandle $handle, array $phids) { $user = $this->getRequest()->getUser(); $response = id(new AphrontReloadResponse())->setURI($handle->getURI()); $phids = array_fill_keys($phids, true); unset($phids[$task->getPHID()]); // Prevent merging a task into itself. if (!$phids) { return $response; } $targets = id(new ManiphestTaskQuery()) ->setViewer($user) + ->requireCapabilities( + array( + PhabricatorPolicyCapability::CAN_VIEW, + PhabricatorPolicyCapability::CAN_EDIT, + )) ->withPHIDs(array_keys($phids)) ->needSubscriberPHIDs(true) ->needProjectPHIDs(true) ->execute(); if (empty($targets)) { return $response; } $editor = id(new ManiphestTransactionEditor()) ->setActor($user) ->setContentSourceFromRequest($this->getRequest()) ->setContinueOnNoEffect(true) ->setContinueOnMissingFields(true); $cc_vector = array(); // since we loaded this via a generic object query, go ahead and get the // attach the subscriber and project phids now $task->attachSubscriberPHIDs( PhabricatorSubscribersQuery::loadSubscribersForPHID($task->getPHID())); $task->attachProjectPHIDs( PhabricatorEdgeQuery::loadDestinationPHIDs($task->getPHID(), PhabricatorProjectObjectHasProjectEdgeType::EDGECONST)); $cc_vector[] = $task->getSubscriberPHIDs(); foreach ($targets as $target) { $cc_vector[] = $target->getSubscriberPHIDs(); $cc_vector[] = array( $target->getAuthorPHID(), $target->getOwnerPHID(), ); $merged_into_txn = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_MERGED_INTO) ->setNewValue($task->getPHID()); $editor->applyTransactions( $target, array($merged_into_txn)); } $all_ccs = array_mergev($cc_vector); $all_ccs = array_filter($all_ccs); $all_ccs = array_unique($all_ccs); $add_ccs = id(new ManiphestTransaction()) ->setTransactionType(PhabricatorTransactions::TYPE_SUBSCRIBERS) ->setNewValue(array('=' => $all_ccs)); $merged_from_txn = id(new ManiphestTransaction()) ->setTransactionType(ManiphestTransaction::TYPE_MERGED_FROM) ->setNewValue(mpull($targets, 'getPHID')); $editor->applyTransactions( $task, array($add_ccs, $merged_from_txn)); return $response; } - private function getStrings() { - switch ($this->type) { + private function getStrings($attach_type, $action) { + switch ($attach_type) { case DifferentialRevisionPHIDType::TYPECONST: $noun = 'Revisions'; $selected = 'created'; break; case ManiphestTaskPHIDType::TYPECONST: $noun = 'Tasks'; $selected = 'assigned'; break; case PhabricatorRepositoryCommitPHIDType::TYPECONST: $noun = 'Commits'; $selected = 'created'; break; case PholioMockPHIDType::TYPECONST: $noun = 'Mocks'; $selected = 'created'; break; } - switch ($this->action) { + switch ($action) { case self::ACTION_EDGE: case self::ACTION_ATTACH: $dialog_title = "Manage Attached {$noun}"; $header_text = "Currently Attached {$noun}"; $button_text = "Save {$noun}"; $instructions = null; break; case self::ACTION_MERGE: $dialog_title = 'Merge Duplicate Tasks'; $header_text = 'Tasks To Merge'; $button_text = "Merge {$noun}"; $instructions = 'These tasks will be merged into the current task and then closed. '. 'The current task will grow stronger.'; break; case self::ACTION_DEPENDENCIES: $dialog_title = 'Edit Dependencies'; $header_text = 'Current Dependencies'; $button_text = 'Save Dependencies'; $instructions = null; break; case self::ACTION_BLOCKS: $dialog_title = pht('Edit Blocking Tasks'); $header_text = pht('Current Blocking Tasks'); $button_text = pht('Save Blocking Tasks'); $instructions = null; break; } return array( 'target_plural_noun' => $noun, 'selected' => $selected, 'title' => $dialog_title, 'header' => $header_text, 'button' => $button_text, 'instructions' => $instructions, ); } - private function getFilters(array $strings) { - if ($this->type == PholioMockPHIDType::TYPECONST) { + private function getFilters(array $strings, $attach_type) { + if ($attach_type == PholioMockPHIDType::TYPECONST) { $filters = array( 'created' => 'Created By Me', 'all' => 'All '.$strings['target_plural_noun'], ); } else { $filters = array( 'assigned' => 'Assigned to Me', 'created' => 'Created By Me', 'open' => 'All Open '.$strings['target_plural_noun'], 'all' => 'All '.$strings['target_plural_noun'], ); } return $filters; } private function getEdgeType($src_type, $dst_type) { $t_cmit = PhabricatorRepositoryCommitPHIDType::TYPECONST; $t_task = ManiphestTaskPHIDType::TYPECONST; $t_drev = DifferentialRevisionPHIDType::TYPECONST; $t_mock = PholioMockPHIDType::TYPECONST; $map = array( $t_cmit => array( $t_task => DiffusionCommitHasTaskEdgeType::EDGECONST, ), $t_task => array( $t_cmit => ManiphestTaskHasCommitEdgeType::EDGECONST, $t_task => ManiphestTaskDependsOnTaskEdgeType::EDGECONST, $t_drev => ManiphestTaskHasRevisionEdgeType::EDGECONST, $t_mock => ManiphestTaskHasMockEdgeType::EDGECONST, ), $t_drev => array( $t_drev => DifferentialRevisionDependsOnRevisionEdgeType::EDGECONST, $t_task => DifferentialRevisionHasTaskEdgeType::EDGECONST, ), $t_mock => array( $t_task => PholioMockHasTaskEdgeType::EDGECONST, ), ); if (empty($map[$src_type][$dst_type])) { return null; } return $map[$src_type][$dst_type]; } private function raiseGraphCycleException(PhabricatorEdgeCycleException $ex) { $cycle = $ex->getCycle(); $handles = $this->loadViewerHandles($cycle); $names = array(); foreach ($cycle as $cycle_phid) { $names[] = $handles[$cycle_phid]->getFullName(); } $names = implode(" \xE2\x86\x92 ", $names); throw new Exception( "You can not create that dependency, because it would create a ". "circular dependency: {$names}."); } } diff --git a/src/applications/search/controller/PhabricatorSearchBaseController.php b/src/applications/search/controller/PhabricatorSearchBaseController.php index 446406dfea..86573b38bd 100644 --- a/src/applications/search/controller/PhabricatorSearchBaseController.php +++ b/src/applications/search/controller/PhabricatorSearchBaseController.php @@ -1,18 +1,24 @@ <?php abstract class PhabricatorSearchBaseController extends PhabricatorController { + const ACTION_ATTACH = 'attach'; + const ACTION_MERGE = 'merge'; + const ACTION_DEPENDENCIES = 'dependencies'; + const ACTION_BLOCKS = 'blocks'; + const ACTION_EDGE = 'edge'; + public function buildStandardPageResponse($view, array $data) { $page = $this->buildStandardPageView(); $page->setApplicationName('Search'); $page->setBaseURI('/search/'); $page->setTitle(idx($data, 'title')); $page->setGlyph("\xC2\xBF"); $page->appendChild($view); $response = new AphrontWebpageResponse(); return $response->setContent($page->render()); } } diff --git a/src/applications/search/controller/PhabricatorSearchSelectController.php b/src/applications/search/controller/PhabricatorSearchSelectController.php index 03d3ca4c69..f663cd03d7 100644 --- a/src/applications/search/controller/PhabricatorSearchSelectController.php +++ b/src/applications/search/controller/PhabricatorSearchSelectController.php @@ -1,79 +1,86 @@ <?php final class PhabricatorSearchSelectController extends PhabricatorSearchBaseController { - private $type; - - public function willProcessRequest(array $data) { - $this->type = $data['type']; - } - - public function processRequest() { - $request = $this->getRequest(); + public function handleRequest(AphrontRequest $request) { $user = $request->getUser(); + $type = $request->getURIData('type'); + $action = $request->getURIData('action'); $query = new PhabricatorSavedQuery(); $query_str = $request->getStr('query'); $query->setEngineClassName('PhabricatorSearchApplicationSearchEngine'); $query->setParameter('query', $query_str); - $query->setParameter('types', array($this->type)); + $query->setParameter('types', array($type)); $status_open = PhabricatorSearchRelationship::RELATIONSHIP_OPEN; switch ($request->getStr('filter')) { case 'assigned': $query->setParameter('ownerPHIDs', array($user->getPHID())); $query->setParameter('statuses', array($status_open)); break; case 'created'; $query->setParameter('authorPHIDs', array($user->getPHID())); // TODO - if / when we allow pholio mocks to be archived, etc // update this - if ($this->type != PholioMockPHIDType::TYPECONST) { + if ($type != PholioMockPHIDType::TYPECONST) { $query->setParameter('statuses', array($status_open)); } break; case 'open': $query->setParameter('statuses', array($status_open)); break; } $query->setParameter('excludePHIDs', array($request->getStr('exclude'))); + $capabilities = array(PhabricatorPolicyCapability::CAN_VIEW); + switch ($action) { + case self::ACTION_MERGE: + $capabilities[] = PhabricatorPolicyCapability::CAN_EDIT; + break; + default: + break; + } + $results = id(new PhabricatorSearchDocumentQuery()) ->setViewer($user) + ->requireObjectCapabilities($capabilities) ->withSavedQuery($query) ->setOffset(0) ->setLimit(100) ->execute(); $phids = array_fill_keys(mpull($results, 'getPHID'), true); - $phids += $this->queryObjectNames($query_str); + $phids += $this->queryObjectNames($query_str, $capabilities); $phids = array_keys($phids); $handles = $this->loadViewerHandles($phids); $data = array(); foreach ($handles as $handle) { $view = new PhabricatorHandleObjectSelectorDataView($handle); $data[] = $view->renderData(); } return id(new AphrontAjaxResponse())->setContent($data); } - private function queryObjectNames($query) { - $viewer = $this->getRequest()->getUser(); + private function queryObjectNames($query, $capabilities) { + $request = $this->getRequest(); + $viewer = $request->getUser(); $objects = id(new PhabricatorObjectQuery()) ->setViewer($viewer) - ->withTypes(array($this->type)) + ->requireCapabilities($capabilities) + ->withTypes(array($request->getURIData('type'))) ->withNames(array($query)) ->execute(); return mpull($objects, 'getPHID'); } } diff --git a/src/applications/search/query/PhabricatorSearchDocumentQuery.php b/src/applications/search/query/PhabricatorSearchDocumentQuery.php index b5591d8eca..903d40dd5a 100644 --- a/src/applications/search/query/PhabricatorSearchDocumentQuery.php +++ b/src/applications/search/query/PhabricatorSearchDocumentQuery.php @@ -1,84 +1,98 @@ <?php final class PhabricatorSearchDocumentQuery extends PhabricatorCursorPagedPolicyAwareQuery { private $savedQuery; + private $objectCapabilities; public function withSavedQuery(PhabricatorSavedQuery $query) { $this->savedQuery = $query; return $this; } + public function requireObjectCapabilities(array $capabilities) { + $this->objectCapabilities = $capabilities; + return $this; + } + + protected function getRequiredObjectCapabilities() { + if ($this->objectCapabilities) { + return $this->objectCapabilities; + } + return $this->getRequiredCapabilities(); + } + protected function loadPage() { $phids = $this->loadDocumentPHIDsWithoutPolicyChecks(); $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->getViewer()) + ->requireObjectCapabilities($this->getRequiredObjectCapabilities()) ->withPHIDs($phids) ->execute(); // Retain engine order. $handles = array_select_keys($handles, $phids); return $handles; } protected function willFilterPage(array $handles) { // NOTE: This is used by the object selector dialog to exclude the object // you're looking at, so that, e.g., a task can't be set as a dependency // of itself in the UI. // TODO: Remove this after object selection moves to ApplicationSearch. $exclude = array(); if ($this->savedQuery) { $exclude_phids = $this->savedQuery->getParameter('excludePHIDs', array()); $exclude = array_fuse($exclude_phids); } foreach ($handles as $key => $handle) { if (!$handle->isComplete()) { unset($handles[$key]); continue; } if ($handle->getPolicyFiltered()) { unset($handles[$key]); continue; } if (isset($exclude[$handle->getPHID()])) { unset($handles[$key]); continue; } } return $handles; } public function loadDocumentPHIDsWithoutPolicyChecks() { $query = id(clone($this->savedQuery)) ->setParameter('offset', $this->getOffset()) ->setParameter('limit', $this->getRawResultLimit()); $engine = PhabricatorSearchEngineSelector::newSelector()->newEngine(); return $engine->executeSearch($query); } public function getQueryApplicationClass() { return 'PhabricatorSearchApplication'; } protected function getResultCursor($result) { throw new Exception( pht( 'This query does not support cursor paging; it must be offset '. 'paged.')); } protected function nextPage(array $page) { $this->setOffset($this->getOffset() + count($page)); return $this; } }