diff --git a/src/applications/audit/application/PhabricatorAuditApplication.php b/src/applications/audit/application/PhabricatorAuditApplication.php index 7d8ad94aa7..ee5cbe7781 100644 --- a/src/applications/audit/application/PhabricatorAuditApplication.php +++ b/src/applications/audit/application/PhabricatorAuditApplication.php @@ -1,37 +1,35 @@ <?php final class PhabricatorAuditApplication extends PhabricatorApplication { public function getBaseURI() { return '/diffusion/commit/'; } public function getIcon() { return 'fa-check-circle-o'; } public function getName() { return pht('Audit'); } public function getShortDescription() { return pht('Browse and Audit Commits'); } public function canUninstall() { - // Audit was once a separate application, but has largely merged with - // Diffusion. - return false; + return true; } public function isPinnedByDefault(PhabricatorUser $viewer) { return parent::isClassInstalledForViewer( 'PhabricatorDiffusionApplication', $viewer); } public function getApplicationOrder() { return 0.130; } } diff --git a/src/applications/audit/conduit/AuditConduitAPIMethod.php b/src/applications/audit/conduit/AuditConduitAPIMethod.php index a5a7957b1f..4dc9eb103e 100644 --- a/src/applications/audit/conduit/AuditConduitAPIMethod.php +++ b/src/applications/audit/conduit/AuditConduitAPIMethod.php @@ -1,10 +1,10 @@ <?php abstract class AuditConduitAPIMethod extends ConduitAPIMethod { final public function getApplication() { return PhabricatorApplication::getByClass( - 'PhabricatorDiffusionApplication'); + 'PhabricatorAuditApplication'); } } diff --git a/src/applications/audit/query/PhabricatorCommitSearchEngine.php b/src/applications/audit/query/PhabricatorCommitSearchEngine.php index a6a2ee071e..dfc038c197 100644 --- a/src/applications/audit/query/PhabricatorCommitSearchEngine.php +++ b/src/applications/audit/query/PhabricatorCommitSearchEngine.php @@ -1,280 +1,289 @@ <?php final class PhabricatorCommitSearchEngine extends PhabricatorApplicationSearchEngine { public function getResultTypeDescription() { return pht('Diffusion Commits'); } public function getApplicationClassName() { return PhabricatorDiffusionApplication::class; } public function newQuery() { return id(new DiffusionCommitQuery()) ->needAuditRequests(true) ->needCommitData(true) ->needIdentities(true) ->needDrafts(true); } protected function newResultBuckets() { return DiffusionCommitResultBucket::getAllResultBuckets(); } protected function buildQueryFromParameters(array $map) { $query = $this->newQuery(); if ($map['responsiblePHIDs']) { $query->withResponsiblePHIDs($map['responsiblePHIDs']); } if ($map['auditorPHIDs']) { $query->withAuditorPHIDs($map['auditorPHIDs']); } if ($map['authorPHIDs']) { $query->withAuthorPHIDs($map['authorPHIDs']); } if ($map['statuses']) { $query->withStatuses($map['statuses']); } if ($map['repositoryPHIDs']) { $query->withRepositoryPHIDs($map['repositoryPHIDs']); } if ($map['packagePHIDs']) { $query->withPackagePHIDs($map['packagePHIDs']); } if ($map['unreachable'] !== null) { $query->withUnreachable($map['unreachable']); } if ($map['permanent'] !== null) { $query->withPermanent($map['permanent']); } if ($map['ancestorsOf']) { $query->withAncestorsOf($map['ancestorsOf']); } if ($map['identifiers']) { $query->withIdentifiers($map['identifiers']); } return $query; } protected function buildCustomSearchFields() { + $show_audit_fields = (id(new PhabricatorAuditApplication())->isInstalled()); $show_packages = PhabricatorApplication::isClassInstalled( 'PhabricatorPackagesApplication'); return array( id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Responsible Users')) ->setKey('responsiblePHIDs') ->setConduitKey('responsible') ->setAliases(array('responsible', 'responsibles', 'responsiblePHID')) ->setDatasource(new DifferentialResponsibleDatasource()) ->setDescription( pht( 'Find commits where given users, projects, or packages are '. 'responsible for the next steps in the audit workflow.')), id(new PhabricatorUsersSearchField()) ->setLabel(pht('Authors')) ->setKey('authorPHIDs') ->setConduitKey('authors') ->setAliases(array('author', 'authors', 'authorPHID')) ->setDescription(pht('Find commits authored by particular users.')), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Auditors')) ->setKey('auditorPHIDs') ->setConduitKey('auditors') ->setAliases(array('auditor', 'auditors', 'auditorPHID')) ->setDatasource(new DiffusionAuditorFunctionDatasource()) + ->setIsHidden(!$show_audit_fields) ->setDescription( pht( 'Find commits where given users, projects, or packages are '. 'auditors.')), id(new PhabricatorSearchCheckboxesField()) ->setLabel(pht('Audit Status')) ->setKey('statuses') ->setAliases(array('status')) ->setOptions(DiffusionCommitAuditStatus::newOptions()) ->setDeprecatedOptions( DiffusionCommitAuditStatus::newDeprecatedOptions()) + ->setIsHidden(!$show_audit_fields) ->setDescription(pht('Find commits with given audit statuses.')), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Repositories')) ->setKey('repositoryPHIDs') ->setConduitKey('repositories') ->setAliases(array('repository', 'repositories', 'repositoryPHID')) ->setDatasource(new DiffusionRepositoryFunctionDatasource()) ->setDescription(pht('Find commits in particular repositories.')), id(new PhabricatorSearchDatasourceField()) ->setLabel(pht('Packages')) ->setKey('packagePHIDs') ->setConduitKey('packages') ->setAliases(array('package', 'packages', 'packagePHID')) ->setDatasource(new PhabricatorOwnersPackageDatasource()) ->setIsHidden(!$show_packages) ->setDescription( pht('Find commits which affect given packages.')), id(new PhabricatorSearchThreeStateField()) ->setLabel(pht('Unreachable')) ->setKey('unreachable') ->setOptions( pht('(Show All)'), pht('Show Only Unreachable Commits'), pht('Hide Unreachable Commits')) ->setDescription( pht( 'Find or exclude unreachable commits which are not ancestors of '. 'any branch, tag, or ref.')), id(new PhabricatorSearchThreeStateField()) ->setLabel(pht('Permanent')) ->setKey('permanent') ->setOptions( pht('(Show All)'), pht('Show Only Permanent Commits'), pht('Hide Permanent Commits')) ->setDescription( pht( 'Find or exclude permanent commits which are ancestors of '. 'any permanent branch, tag, or ref.')), id(new PhabricatorSearchStringListField()) ->setLabel(pht('Ancestors Of')) ->setKey('ancestorsOf') ->setDescription( pht( 'Find commits which are ancestors of a particular ref, '. 'like "master".')), id(new PhabricatorSearchStringListField()) ->setLabel(pht('Identifiers')) ->setKey('identifiers') ->setDescription( pht( 'Find commits with particular identifiers (usually, hashes). '. 'Supports full or partial identifiers (like "abcd12340987..." or '. '"abcd1234") and qualified or unqualified identifiers (like '. '"rXabcd1234" or "abcd1234").')), ); } protected function getURI($path) { return '/diffusion/commit/'.$path; } protected function getBuiltinQueryNames() { $names = array(); if ($this->requireViewer()->isLoggedIn()) { - $names['active'] = pht('Active Audits'); + if (id(new PhabricatorAuditApplication())->isInstalled()) { + $names['active'] = pht('Active Audits'); + } $names['authored'] = pht('Authored'); - $names['audited'] = pht('Audited'); + if (id(new PhabricatorAuditApplication())->isInstalled()) { + $names['audited'] = pht('Audited'); + } } $names['all'] = pht('All Commits'); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); $viewer = $this->requireViewer(); $viewer_phid = $viewer->getPHID(); switch ($query_key) { case 'all': return $query; case 'active': $bucket_key = DiffusionCommitRequiredActionResultBucket::BUCKETKEY; $open = DiffusionCommitAuditStatus::getOpenStatusConstants(); $query ->setParameter('responsiblePHIDs', array($viewer_phid)) ->setParameter('statuses', $open) ->setParameter('bucket', $bucket_key) ->setParameter('unreachable', false); return $query; case 'authored': $query ->setParameter('authorPHIDs', array($viewer_phid)); return $query; case 'audited': $query ->setParameter('auditorPHIDs', array($viewer_phid)); return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } protected function renderResultList( array $commits, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($commits, 'PhabricatorRepositoryCommit'); $viewer = $this->requireViewer(); $bucket = $this->getResultBucket($query); + // hide "Auditors" on /diffusion/commit/query/all/ if Audit not installed + $show_auditors = id(new PhabricatorAuditApplication())->isInstalled(); $template = id(new DiffusionCommitGraphView()) ->setViewer($viewer) - ->setShowAuditors(true); + ->setShowAuditors($show_auditors); $views = array(); if ($bucket) { $bucket->setViewer($viewer); try { $groups = $bucket->newResultGroups($query, $commits); foreach ($groups as $group) { // Don't show groups in Dashboard Panels if ($group->getObjects() || !$this->isPanelContext()) { $item_list = id(clone $template) ->setCommits($group->getObjects()) ->newObjectItemListView(); $views[] = $item_list ->setHeader($group->getName()) ->setNoDataString($group->getNoDataString()); } } } catch (Exception $ex) { $this->addError($ex->getMessage()); } } if (!$views) { $item_list = id(clone $template) ->setCommits($commits) ->newObjectItemListView(); $views[] = $item_list ->setNoDataString(pht('No commits found.')); } return id(new PhabricatorApplicationSearchResultView()) ->setContent($views); } protected function getNewUserBody() { $view = id(new PHUIBigInfoView()) ->setIcon('fa-check-circle-o') ->setTitle(pht('Welcome to Audit')) ->setDescription( pht('Post-commit code review and auditing. Audits you are assigned '. 'to will appear here.')); return $view; } } diff --git a/src/applications/diffusion/editor/DiffusionCommitEditEngine.php b/src/applications/diffusion/editor/DiffusionCommitEditEngine.php index 02213cceee..c807d4e8c4 100644 --- a/src/applications/diffusion/editor/DiffusionCommitEditEngine.php +++ b/src/applications/diffusion/editor/DiffusionCommitEditEngine.php @@ -1,169 +1,172 @@ <?php final class DiffusionCommitEditEngine extends PhabricatorEditEngine { const ENGINECONST = 'diffusion.commit'; const ACTIONGROUP_AUDIT = 'audit'; const ACTIONGROUP_COMMIT = 'commit'; public function isEngineConfigurable() { return false; } public function getEngineName() { return pht('Commits'); } public function getSummaryHeader() { return pht('Edit Commits'); } public function getSummaryText() { return pht('Edit commits.'); } public function getEngineApplicationClass() { return PhabricatorDiffusionApplication::class; } protected function newEditableObject() { // NOTE: We must return a valid object here so that things like Conduit // documentation generation work. You can't actually create commits via // EditEngine. This is enforced with a "No One" creation policy. $repository = new PhabricatorRepository(); $data = new PhabricatorRepositoryCommitData(); return id(new PhabricatorRepositoryCommit()) ->attachRepository($repository) ->attachCommitData($data) ->attachAudits(array()); } protected function newObjectQuery() { $viewer = $this->getViewer(); return id(new DiffusionCommitQuery()) ->needCommitData(true) ->needAuditRequests(true) ->needAuditAuthority(array($viewer)) ->needIdentities(true); } protected function getEditorURI() { return $this->getApplication()->getApplicationURI('commit/edit/'); } protected function newCommentActionGroups() { return array( id(new PhabricatorEditEngineCommentActionGroup()) ->setKey(self::ACTIONGROUP_AUDIT) ->setLabel(pht('Audit Actions')), id(new PhabricatorEditEngineCommentActionGroup()) ->setKey(self::ACTIONGROUP_COMMIT) ->setLabel(pht('Commit Actions')), ); } protected function getObjectCreateTitleText($object) { return pht('Create Commit'); } protected function getObjectCreateShortText() { return pht('Create Commit'); } protected function getObjectEditTitleText($object) { return pht('Edit Commit: %s', $object->getDisplayName()); } protected function getObjectEditShortText($object) { return $object->getDisplayName(); } protected function getObjectName() { return pht('Commit'); } protected function getObjectViewURI($object) { return $object->getURI(); } protected function getCreateNewObjectPolicy() { return PhabricatorPolicies::POLICY_NOONE; } protected function buildCustomEditFields($object) { $viewer = $this->getViewer(); $data = $object->getCommitData(); $fields = array(); - - $fields[] = id(new PhabricatorDatasourceEditField()) - ->setKey('auditors') - ->setLabel(pht('Auditors')) - ->setDatasource(new DiffusionAuditorDatasource()) - ->setUseEdgeTransactions(true) - ->setTransactionType( - DiffusionCommitAuditorsTransaction::TRANSACTIONTYPE) - ->setCommentActionLabel(pht('Change Auditors')) - ->setDescription(pht('Auditors for this commit.')) - ->setConduitDescription(pht('Change the auditors for this commit.')) - ->setConduitTypeDescription(pht('New auditors.')) - ->setValue($object->getAuditorPHIDsForEdit()); + // remove "Change Auditors" from "Add Action" dropdown etc + // if Audit is not installed + if (id(new PhabricatorAuditApplication())->isInstalled()) { + $fields[] = id(new PhabricatorDatasourceEditField()) + ->setKey('auditors') + ->setLabel(pht('Auditors')) + ->setDatasource(new DiffusionAuditorDatasource()) + ->setUseEdgeTransactions(true) + ->setTransactionType( + DiffusionCommitAuditorsTransaction::TRANSACTIONTYPE) + ->setCommentActionLabel(pht('Change Auditors')) + ->setDescription(pht('Auditors for this commit.')) + ->setConduitDescription(pht('Change the auditors for this commit.')) + ->setConduitTypeDescription(pht('New auditors.')) + ->setValue($object->getAuditorPHIDsForEdit()); + } $actions = DiffusionCommitActionTransaction::loadAllActions(); $actions = msortv($actions, 'getCommitActionOrderVector'); foreach ($actions as $key => $action) { $fields[] = $action->newEditField($object, $viewer); } return $fields; } protected function newAutomaticCommentTransactions($object) { $viewer = $this->getViewer(); $editor = $object->getApplicationTransactionEditor() ->setActor($viewer); $xactions = $editor->newAutomaticInlineTransactions( $object, PhabricatorAuditActionConstants::INLINE, new DiffusionDiffInlineCommentQuery()); return $xactions; } protected function newCommentPreviewContent($object, array $xactions) { $viewer = $this->getViewer(); $type_inline = PhabricatorAuditActionConstants::INLINE; $inlines = array(); foreach ($xactions as $xaction) { if ($xaction->getTransactionType() === $type_inline) { $inlines[] = $xaction->getComment(); } } $content = array(); if ($inlines) { $inline_preview = id(new PHUIDiffInlineCommentPreviewListView()) ->setViewer($viewer) ->setInlineComments($inlines); $content[] = phutil_tag( 'div', array( 'id' => 'inline-comment-preview', ), $inline_preview); } return $content; } } diff --git a/src/applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php b/src/applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php index f895847d4a..f3ae92248c 100644 --- a/src/applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php +++ b/src/applications/diffusion/herald/DiffusionAuditorsAddAuditorsHeraldAction.php @@ -1,37 +1,42 @@ <?php final class DiffusionAuditorsAddAuditorsHeraldAction extends DiffusionAuditorsHeraldAction { const ACTIONCONST = 'diffusion.auditors.add'; public function getHeraldActionName() { return pht('Add auditors'); } + // hide "Add auditors" Herald action if Audit not installed public function supportsRuleType($rule_type) { - return ($rule_type != HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); + if (id(new PhabricatorAuditApplication())->isInstalled()) { + return ($rule_type != HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); + } else { + return false; + } } public function applyEffect($object, HeraldEffect $effect) { $rule = $effect->getRule(); return $this->applyAuditors($effect->getTarget(), $rule); } public function getHeraldActionStandardType() { return self::STANDARD_PHID_LIST; } protected function getDatasource() { return new DiffusionAuditorDatasource(); } public function renderActionDescription($value) { return pht('Add auditors: %s.', $this->renderHandleList($value)); } public function getPHIDsAffectedByAction(HeraldActionRecord $record) { return $record->getTarget(); } } diff --git a/src/applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php b/src/applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php index d27876d40e..b1ab98db77 100644 --- a/src/applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php +++ b/src/applications/diffusion/herald/DiffusionAuditorsAddSelfHeraldAction.php @@ -1,30 +1,35 @@ <?php final class DiffusionAuditorsAddSelfHeraldAction extends DiffusionAuditorsHeraldAction { const ACTIONCONST = 'diffusion.auditors.self.add'; public function getHeraldActionName() { return pht('Add me as an auditor'); } + // hide "Add me as an auditor" Herald action if Audit not installed public function supportsRuleType($rule_type) { - return ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); + if (id(new PhabricatorAuditApplication())->isInstalled()) { + return ($rule_type == HeraldRuleTypeConfig::RULE_TYPE_PERSONAL); + } else { + return false; + } } public function applyEffect($object, HeraldEffect $effect) { $rule = $effect->getRule(); $phid = $rule->getAuthorPHID(); return $this->applyAuditors(array($phid), $rule); } public function getHeraldActionStandardType() { return self::STANDARD_NONE; } public function renderActionDescription($value) { return pht('Add rule author as auditor.'); } } diff --git a/src/applications/diffusion/herald/DiffusionCommitAuditorsHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitAuditorsHeraldField.php index 5f0da133f8..7afd0f6705 100644 --- a/src/applications/diffusion/herald/DiffusionCommitAuditorsHeraldField.php +++ b/src/applications/diffusion/herald/DiffusionCommitAuditorsHeraldField.php @@ -1,43 +1,52 @@ <?php final class DiffusionCommitAuditorsHeraldField extends DiffusionCommitHeraldField { const FIELDCONST = 'diffusion.commit.auditors'; + // hide "Auditors" Herald condition if Audit not installed + public function supportsObject($object) { + if (id(new PhabricatorAuditApplication())->isInstalled()) { + return ($object instanceof PhabricatorRepositoryCommit); + } else { + return false; + } + } + public function getHeraldFieldName() { return pht('Auditors'); } public function getHeraldFieldValue($object) { $viewer = PhabricatorUser::getOmnipotentUser(); $commit = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withPHIDs(array($object->getPHID())) ->needAuditRequests(true) ->executeOne(); $audits = $commit->getAudits(); $phids = array(); foreach ($audits as $audit) { if ($audit->isResigned()) { continue; } $phids[] = $audit->getAuditorPHID(); } return $phids; } protected function getHeraldFieldStandardType() { return self::STANDARD_PHID_LIST; } protected function getDatasource() { return new DiffusionAuditorDatasource(); } } diff --git a/src/applications/diffusion/herald/DiffusionCommitPackageAuditHeraldField.php b/src/applications/diffusion/herald/DiffusionCommitPackageAuditHeraldField.php index 587c0b58d3..9fdd785fcb 100644 --- a/src/applications/diffusion/herald/DiffusionCommitPackageAuditHeraldField.php +++ b/src/applications/diffusion/herald/DiffusionCommitPackageAuditHeraldField.php @@ -1,33 +1,43 @@ <?php final class DiffusionCommitPackageAuditHeraldField extends DiffusionCommitHeraldField { const FIELDCONST = 'diffusion.commit.package.audit'; + // hide "Affected packages that need audit" Herald condition + // if Audit not installed + public function supportsObject($object) { + if (id(new PhabricatorAuditApplication())->isInstalled()) { + return ($object instanceof PhabricatorRepositoryCommit); + } else { + return false; + } + } + public function getHeraldFieldName() { return pht('Affected packages that need audit'); } public function getFieldGroupKey() { return HeraldRelatedFieldGroup::FIELDGROUPKEY; } public function getHeraldFieldValue($object) { $packages = $this->getAdapter()->loadAuditNeededPackages(); if (!$packages) { return array(); } return mpull($packages, 'getPHID'); } protected function getHeraldFieldStandardType() { return self::STANDARD_PHID_LIST; } protected function getDatasource() { return new PhabricatorOwnersPackageDatasource(); } } diff --git a/src/applications/diffusion/view/DiffusionCommitGraphView.php b/src/applications/diffusion/view/DiffusionCommitGraphView.php index 30c252712c..7e70a4e655 100644 --- a/src/applications/diffusion/view/DiffusionCommitGraphView.php +++ b/src/applications/diffusion/view/DiffusionCommitGraphView.php @@ -1,642 +1,645 @@ <?php final class DiffusionCommitGraphView extends DiffusionView { private $history; private $commits; private $isHead; private $isTail; private $parents; private $filterParents; private $commitMap; private $buildableMap; private $revisionMap; private $showAuditors; public function setHistory(array $history) { assert_instances_of($history, 'DiffusionPathChange'); $this->history = $history; return $this; } public function getHistory() { return $this->history; } public function setCommits(array $commits) { assert_instances_of($commits, 'PhabricatorRepositoryCommit'); $this->commits = $commits; return $this; } public function getCommits() { return $this->commits; } public function setShowAuditors($show_auditors) { $this->showAuditors = $show_auditors; return $this; } public function getShowAuditors() { return $this->showAuditors; } public function setParents(array $parents) { $this->parents = $parents; return $this; } public function getParents() { return $this->parents; } public function setIsHead($is_head) { $this->isHead = $is_head; return $this; } public function getIsHead() { return $this->isHead; } public function setIsTail($is_tail) { $this->isTail = $is_tail; return $this; } public function getIsTail() { return $this->isTail; } public function setFilterParents($filter_parents) { $this->filterParents = $filter_parents; return $this; } public function getFilterParents() { return $this->filterParents; } private function getRepository() { $drequest = $this->getDiffusionRequest(); if (!$drequest) { return null; } return $drequest->getRepository(); } public function newObjectItemListView() { $list_view = id(new PHUIObjectItemListView()); $item_views = $this->newObjectItemViews(); foreach ($item_views as $item_view) { $list_view->addItem($item_view); } return $list_view; } private function newObjectItemViews() { $viewer = $this->getViewer(); require_celerity_resource('diffusion-css'); $show_builds = $this->shouldShowBuilds(); $show_revisions = $this->shouldShowRevisions(); $show_auditors = $this->shouldShowAuditors(); $phids = array(); if ($show_revisions) { $revision_map = $this->getRevisionMap(); foreach ($revision_map as $revisions) { foreach ($revisions as $revision) { $phids[] = $revision->getPHID(); } } } $commits = $this->getCommitMap(); foreach ($commits as $commit) { $author_phid = $commit->getAuthorDisplayPHID(); if ($author_phid !== null) { $phids[] = $author_phid; } } if ($show_auditors) { foreach ($commits as $commit) { $audits = $commit->getAudits(); foreach ($audits as $auditor) { $phids[] = $auditor->getAuditorPHID(); } } } $handles = $viewer->loadHandles($phids); $views = array(); $items = $this->newHistoryItems(); foreach ($items as $hash => $item) { $content = array(); $commit = $item['commit']; $commit_description = $this->getCommitDescription($commit); $commit_link = $this->getCommitURI($hash); $short_hash = $this->getCommitObjectName($hash); $is_disabled = $this->getCommitIsDisabled($commit); $item_view = id(new PHUIObjectItemView()) ->setViewer($viewer) ->setHeader($commit_description) ->setObjectName($short_hash) ->setHref($commit_link) ->setDisabled($is_disabled); $this->addBrowseAction($item_view, $hash); if ($show_builds) { $this->addBuildAction($item_view, $hash); } - $this->addAuditAction($item_view, $hash); + // hide Audit entry on /diffusion/commit/query/all if Audit not installed + if (id(new PhabricatorAuditApplication())->isInstalled()) { + $this->addAuditAction($item_view, $hash); + } if ($show_auditors) { $auditor_list = $item_view->newMapView(); if ($commit) { $auditors = $this->newAuditorList($commit, $handles); $auditor_list->newItem() ->setName(pht('Auditors')) ->setValue($auditors); } } $property_list = $item_view->newMapView(); if ($commit) { $author_view = $this->getCommitAuthorView($commit); if ($author_view) { $property_list->newItem() ->setName(pht('Author')) ->setValue($author_view); } } if ($show_revisions) { if ($commit) { $revisions = $this->getRevisions($commit); if ($revisions) { $list_view = $handles->newSublist(mpull($revisions, 'getPHID')) ->newListView(); $property_list->newItem() ->setName(pht('Revisions')) ->setValue($list_view); } } } $views[$hash] = $item_view; } return $views; } private function newObjectItemRows() { $viewer = $this->getViewer(); $items = $this->newHistoryItems(); $views = $this->newObjectItemViews(); $last_date = null; $rows = array(); foreach ($items as $hash => $item) { $item_epoch = $item['epoch']; $item_date = phabricator_date($item_epoch, $viewer); if ($item_date !== $last_date) { $last_date = $item_date; $header = $item_date; } else { $header = null; } $item_view = $views[$hash]; $list_view = id(new PHUIObjectItemListView()) ->setViewer($viewer) ->setFlush(true) ->addItem($item_view); if ($header !== null) { $list_view->setHeader($header); } $rows[] = $list_view; } return $rows; } public function render() { $rows = $this->newObjectItemRows(); $graph = $this->newGraphView(); foreach ($rows as $idx => $row) { $cells = array(); if ($graph) { $cells[] = phutil_tag( 'td', array( 'class' => 'diffusion-commit-graph-path-cell', ), $graph[$idx]); } $cells[] = phutil_tag( 'td', array( 'class' => 'diffusion-commit-graph-content-cell', ), $row); $rows[$idx] = phutil_tag('tr', array(), $cells); } $table = phutil_tag( 'table', array( 'class' => 'diffusion-commit-graph-table', ), $rows); return $table; } private function newGraphView() { if (!$this->getParents()) { return null; } $parents = $this->getParents(); // If we're filtering parents, remove relationships which point to // commits that are not part of the visible graph. Otherwise, we get // a big tree of nonsense when viewing release branches like "stable" // versus "master". if ($this->getFilterParents()) { foreach ($parents as $key => $nodes) { foreach ($nodes as $nkey => $node) { if (empty($parents[$node])) { unset($parents[$key][$nkey]); } } } } return id(new PHUIDiffGraphView()) ->setIsHead($this->getIsHead()) ->setIsTail($this->getIsTail()) ->renderGraph($parents); } private function shouldShowBuilds() { $viewer = $this->getViewer(); $show_builds = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorHarbormasterApplication', $this->getUser()); return $show_builds; } private function shouldShowRevisions() { $viewer = $this->getViewer(); $show_revisions = PhabricatorApplication::isClassInstalledForViewer( 'PhabricatorDifferentialApplication', $viewer); return $show_revisions; } private function shouldShowAuditors() { return $this->getShowAuditors(); } private function newHistoryItems() { $items = array(); $history = $this->getHistory(); if ($history !== null) { foreach ($history as $history_item) { $commit_hash = $history_item->getCommitIdentifier(); $items[$commit_hash] = array( 'epoch' => $history_item->getEpoch(), 'hash' => $commit_hash, 'commit' => $this->getCommit($commit_hash), ); } } else { $commits = $this->getCommitMap(); foreach ($commits as $commit) { $commit_hash = $commit->getCommitIdentifier(); $items[$commit_hash] = array( 'epoch' => $commit->getEpoch(), 'hash' => $commit_hash, 'commit' => $commit, ); } } return $items; } private function getCommitDescription($commit) { if (!$commit) { return phutil_tag('em', array(), pht("Discovering\xE2\x80\xA6")); } // We can show details once the message and change have been imported. $partial_import = PhabricatorRepositoryCommit::IMPORTED_MESSAGE | PhabricatorRepositoryCommit::IMPORTED_CHANGE; if (!$commit->isPartiallyImported($partial_import)) { return phutil_tag('em', array(), pht("Importing\xE2\x80\xA6")); } return $commit->getCommitData()->getSummary(); } private function getCommitURI($hash) { $repository = $this->getRepository(); if ($repository) { return $repository->getCommitURI($hash); } $commit = $this->getCommit($hash); if ($commit) { return $commit->getURI(); } return null; } private function getCommitObjectName($hash) { $repository = $this->getRepository(); if ($repository) { return $repository->formatCommitName( $hash, $is_local = true); } $commit = $this->getCommit($hash); if ($commit) { return $commit->getDisplayName(); } return null; } private function getCommitIsDisabled($commit) { if (!$commit) { return true; } if ($commit->isUnreachable()) { return true; } return false; } private function getCommitAuthorView($commit) { if (!$commit) { return null; } $viewer = $this->getViewer(); $author_phid = $commit->getAuthorDisplayPHID(); if ($author_phid) { return $viewer->loadHandles(array($author_phid)) ->newListView(); } return $commit->newCommitAuthorView($viewer); } private function getCommit($hash) { $commit_map = $this->getCommitMap(); return idx($commit_map, $hash); } private function getCommitMap() { if ($this->commitMap === null) { $commit_list = $this->newCommitList(); $this->commitMap = mpull($commit_list, null, 'getCommitIdentifier'); } return $this->commitMap; } private function addBrowseAction(PHUIObjectItemView $item, $hash) { $repository = $this->getRepository(); if (!$repository) { return; } $drequest = $this->getDiffusionRequest(); $path = $drequest->getPath(); $uri = $drequest->generateURI( array( 'action' => 'browse', 'path' => $path, 'commit' => $hash, )); $menu_item = $item->newMenuItem() ->setName(pht('Browse Repository')) ->setURI($uri); $menu_item->newIcon() ->setIcon('fa-folder-open-o') ->setColor('bluegrey'); } private function addBuildAction(PHUIObjectItemView $item, $hash) { $is_disabled = true; $buildable = null; $commit = $this->getCommit($hash); if ($commit) { $buildable = $this->getBuildable($commit); } if ($buildable) { $icon = $buildable->getStatusIcon(); $color = $buildable->getStatusColor(); $name = $buildable->getStatusDisplayName(); $uri = $buildable->getURI(); } else { $icon = 'fa-times'; $color = 'grey'; $name = pht('No Builds'); $uri = null; } $menu_item = $item->newMenuItem() ->setName($name) ->setURI($uri) ->setDisabled(($uri === null)); $menu_item->newIcon() ->setIcon($icon) ->setColor($color); } private function addAuditAction(PHUIObjectItemView $item_view, $hash) { $commit = $this->getCommit($hash); if ($commit) { $status = $commit->getAuditStatusObject(); $text = $status->getName(); $icon = $status->getIcon(); $is_disabled = $status->isNoAudit(); if ($is_disabled) { $uri = null; $color = 'grey'; } else { $color = $status->getColor(); $uri = $commit->getURI(); } } else { $text = pht('No Audit'); $color = 'grey'; $icon = 'fa-times'; $uri = null; $is_disabled = true; } $menu_item = $item_view->newMenuItem() ->setName($text) ->setURI($uri) ->setBackgroundColor($color) ->setDisabled($is_disabled); $menu_item->newIcon() ->setIcon($icon) ->setColor($color); } private function getBuildable(PhabricatorRepositoryCommit $commit) { $buildable_map = $this->getBuildableMap(); return idx($buildable_map, $commit->getPHID()); } private function getBuildableMap() { if ($this->buildableMap === null) { $commits = $this->getCommitMap(); $buildables = $this->loadBuildables($commits); $this->buildableMap = $buildables; } return $this->buildableMap; } private function getRevisions(PhabricatorRepositoryCommit $commit) { $revision_map = $this->getRevisionMap(); return idx($revision_map, $commit->getPHID(), array()); } private function getRevisionMap() { if ($this->revisionMap === null) { $this->revisionMap = $this->newRevisionMap(); } return $this->revisionMap; } private function newRevisionMap() { $viewer = $this->getViewer(); $commits = $this->getCommitMap(); return DiffusionCommitRevisionQuery::loadRevisionMapForCommits( $viewer, $commits); } private function newCommitList() { $commits = $this->getCommits(); if ($commits !== null) { return $commits; } $repository = $this->getRepository(); if (!$repository) { return array(); } $history = $this->getHistory(); if ($history === null) { return array(); } $identifiers = array(); foreach ($history as $item) { $identifiers[] = $item->getCommitIdentifier(); } if (!$identifiers) { return array(); } $viewer = $this->getViewer(); $commits = id(new DiffusionCommitQuery()) ->setViewer($viewer) ->withRepository($repository) ->withIdentifiers($identifiers) ->needCommitData(true) ->needIdentities(true) ->execute(); return $commits; } private function newAuditorList( PhabricatorRepositoryCommit $commit, $handles) { $auditors = $commit->getAudits(); if (!$auditors) { return phutil_tag('em', array(), pht('None')); } $auditor_phids = mpull($auditors, 'getAuditorPHID'); $auditor_list = $handles->newSublist($auditor_phids) ->newListView(); return $auditor_list; } } diff --git a/src/applications/diffusion/xaction/DiffusionCommitActionTransaction.php b/src/applications/diffusion/xaction/DiffusionCommitActionTransaction.php index 1d351ffa5d..d381602766 100644 --- a/src/applications/diffusion/xaction/DiffusionCommitActionTransaction.php +++ b/src/applications/diffusion/xaction/DiffusionCommitActionTransaction.php @@ -1,124 +1,127 @@ <?php abstract class DiffusionCommitActionTransaction extends DiffusionCommitTransactionType { final public function getCommitActionKey() { return $this->getPhobjectClassConstant('ACTIONKEY', 32); } public function isActionAvailable($object, PhabricatorUser $viewer) { + if (!id(new PhabricatorAuditApplication())->isInstalled()) { + return false; + } try { $this->validateAction($object, $viewer); return true; } catch (Exception $ex) { return false; } } abstract protected function validateAction($object, PhabricatorUser $viewer); abstract protected function getCommitActionLabel(); public function getCommandKeyword() { return null; } public function getCommandAliases() { return array(); } public function getCommandSummary() { return null; } protected function getCommitActionOrder() { return 1000; } public function getCommitActionOrderVector() { return id(new PhutilSortVector()) ->addInt($this->getCommitActionOrder()); } protected function getCommitActionGroupKey() { return DiffusionCommitEditEngine::ACTIONGROUP_COMMIT; } protected function getCommitActionDescription() { return null; } public static function loadAllActions() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getCommitActionKey') ->execute(); } protected function isViewerCommitAuthor( PhabricatorRepositoryCommit $commit, PhabricatorUser $viewer) { if (!$viewer->getPHID()) { return false; } return ($viewer->getPHID() === $commit->getEffectiveAuthorPHID()); } public function newEditField( PhabricatorRepositoryCommit $commit, PhabricatorUser $viewer) { // Actions in the "audit" group, like "Accept Commit", do not require // that the actor be able to edit the commit. $group_audit = DiffusionCommitEditEngine::ACTIONGROUP_AUDIT; $is_audit = ($this->getCommitActionGroupKey() == $group_audit); $field = id(new PhabricatorApplyEditField()) ->setKey($this->getCommitActionKey()) ->setTransactionType($this->getTransactionTypeConstant()) ->setCanApplyWithoutEditCapability($is_audit) ->setValue(true); if ($this->isActionAvailable($commit, $viewer)) { $label = $this->getCommitActionLabel(); if ($label !== null) { $field->setCommentActionLabel($label); $description = $this->getCommitActionDescription(); $field->setActionDescription($description); $group_key = $this->getCommitActionGroupKey(); $field->setCommentActionGroupKey($group_key); $field->setActionConflictKey('commit.action'); } } return $field; } public function validateTransactions($object, array $xactions) { $errors = array(); $actor = $this->getActor(); $action_exception = null; try { $this->validateAction($object, $actor); } catch (Exception $ex) { $action_exception = $ex; } foreach ($xactions as $xaction) { if ($action_exception) { $errors[] = $this->newInvalidError( $action_exception->getMessage(), $xaction); } } return $errors; } } diff --git a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php index 076a9a1e52..cad397b0ac 100644 --- a/src/applications/search/engine/PhabricatorApplicationSearchEngine.php +++ b/src/applications/search/engine/PhabricatorApplicationSearchEngine.php @@ -1,1627 +1,1629 @@ <?php /** * Represents an abstract search engine for an application. It supports * creating and storing saved queries. * * @task construct Constructing Engines * @task app Applications * @task builtin Builtin Queries * @task uri Query URIs * @task dates Date Filters * @task order Result Ordering * @task read Reading Utilities * @task exec Paging and Executing Queries * @task render Rendering Results */ abstract class PhabricatorApplicationSearchEngine extends Phobject { private $application; private $viewer; private $errors = array(); private $request; private $context; private $controller; private $namedQueries; private $navigationItems = array(); const CONTEXT_LIST = 'list'; const CONTEXT_PANEL = 'panel'; const BUCKET_NONE = 'none'; public function setController(PhabricatorController $controller) { $this->controller = $controller; return $this; } public function getController() { return $this->controller; } public function buildResponse() { $controller = $this->getController(); $request = $controller->getRequest(); $search = id(new PhabricatorApplicationSearchController()) ->setQueryKey($request->getURIData('queryKey')) ->setSearchEngine($this); return $controller->delegateToController($search); } public function newResultObject() { // We may be able to get this automatically if newQuery() is implemented. $query = $this->newQuery(); if ($query) { $object = $query->newResultObject(); if ($object) { return $object; } } return null; } public function newQuery() { return null; } public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } protected function requireViewer() { if (!$this->viewer) { throw new PhutilInvalidStateException('setViewer'); } return $this->viewer; } public function setContext($context) { $this->context = $context; return $this; } public function isPanelContext() { return ($this->context == self::CONTEXT_PANEL); } public function setNavigationItems(array $navigation_items) { assert_instances_of($navigation_items, 'PHUIListItemView'); $this->navigationItems = $navigation_items; return $this; } public function getNavigationItems() { return $this->navigationItems; } public function canUseInPanelContext() { return true; } public function saveQuery(PhabricatorSavedQuery $query) { if ($query->getID()) { throw new Exception( pht( 'Query (with ID "%s") has already been saved. Queries are '. 'immutable once saved.', $query->getID())); } $query->setEngineClassName(get_class($this)); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); try { $query->save(); } catch (AphrontDuplicateKeyQueryException $ex) { // Ignore, this is just a repeated search. } unset($unguarded); } /** * Create a saved query object from the request. * * @param AphrontRequest The search request. * @return PhabricatorSavedQuery */ public function buildSavedQueryFromRequest(AphrontRequest $request) { $fields = $this->buildSearchFields(); $viewer = $this->requireViewer(); $saved = new PhabricatorSavedQuery(); foreach ($fields as $field) { $field->setViewer($viewer); $value = $field->readValueFromRequest($request); $saved->setParameter($field->getKey(), $value); } return $saved; } /** * Executes the saved query. * * @param PhabricatorSavedQuery The saved query to operate on. * @return PhabricatorQuery The result of the query. */ public function buildQueryFromSavedQuery(PhabricatorSavedQuery $original) { $saved = clone $original; $this->willUseSavedQuery($saved); $fields = $this->buildSearchFields(); $viewer = $this->requireViewer(); $map = array(); foreach ($fields as $field) { $field->setViewer($viewer); $field->readValueFromSavedQuery($saved); $value = $field->getValueForQuery($field->getValue()); $map[$field->getKey()] = $value; } $original->attachParameterMap($map); $query = $this->buildQueryFromParameters($map); $object = $this->newResultObject(); if (!$object) { return $query; } $extensions = $this->getEngineExtensions(); foreach ($extensions as $extension) { $extension->applyConstraintsToQuery($object, $query, $saved, $map); } $order = $saved->getParameter('order'); $builtin = $query->getBuiltinOrderAliasMap(); if (phutil_nonempty_string($order) && isset($builtin[$order])) { $query->setOrder($order); } else { // If the order is invalid or not available, we choose the first // builtin order. This isn't always the default order for the query, // but is the first value in the "Order" dropdown, and makes the query // behavior more consistent with the UI. In queries where the two // orders differ, this order is the preferred order for humans. $query->setOrder(head_key($builtin)); } return $query; } /** * Hook for subclasses to adjust saved queries prior to use. * * If an application changes how queries are saved, it can implement this * hook to keep old queries working the way users expect, by reading, * adjusting, and overwriting parameters. * * @param PhabricatorSavedQuery Saved query which will be executed. * @return void */ protected function willUseSavedQuery(PhabricatorSavedQuery $saved) { return; } protected function buildQueryFromParameters(array $parameters) { throw new PhutilMethodNotImplementedException(); } /** * Builds the search form using the request. * * @param AphrontFormView Form to populate. * @param PhabricatorSavedQuery The query from which to build the form. * @return void */ public function buildSearchForm( AphrontFormView $form, PhabricatorSavedQuery $saved) { $saved = clone $saved; $this->willUseSavedQuery($saved); $fields = $this->buildSearchFields(); $fields = $this->adjustFieldsForDisplay($fields); $viewer = $this->requireViewer(); foreach ($fields as $field) { $field->setViewer($viewer); $field->readValueFromSavedQuery($saved); } foreach ($fields as $field) { foreach ($field->getErrors() as $error) { $this->addError(last($error)); } } foreach ($fields as $field) { $field->appendToForm($form); } } protected function buildSearchFields() { $fields = array(); foreach ($this->buildCustomSearchFields() as $field) { $fields[] = $field; } $object = $this->newResultObject(); if ($object) { $extensions = $this->getEngineExtensions(); foreach ($extensions as $extension) { $extension_fields = $extension->getSearchFields($object); foreach ($extension_fields as $extension_field) { $fields[] = $extension_field; } } } $query = $this->newQuery(); if ($query && $this->shouldShowOrderField()) { $orders = $query->getBuiltinOrders(); $orders = ipull($orders, 'name'); $fields[] = id(new PhabricatorSearchOrderField()) ->setLabel(pht('Order By')) ->setKey('order') ->setOrderAliases($query->getBuiltinOrderAliasMap()) ->setOptions($orders); } - $buckets = $this->newResultBuckets(); - if ($query && $buckets) { - $bucket_options = array( - self::BUCKET_NONE => pht('No Bucketing'), - ) + mpull($buckets, 'getResultBucketName'); - - $fields[] = id(new PhabricatorSearchSelectField()) - ->setLabel(pht('Bucket')) - ->setKey('bucket') - ->setOptions($bucket_options); + if (id(new PhabricatorAuditApplication())->isInstalled()) { + $buckets = $this->newResultBuckets(); + if ($query && $buckets) { + $bucket_options = array( + self::BUCKET_NONE => pht('No Bucketing'), + ) + mpull($buckets, 'getResultBucketName'); + + $fields[] = id(new PhabricatorSearchSelectField()) + ->setLabel(pht('Bucket')) + ->setKey('bucket') + ->setOptions($bucket_options); + } } $field_map = array(); foreach ($fields as $field) { $key = $field->getKey(); if (isset($field_map[$key])) { throw new Exception( pht( 'Two fields in this SearchEngine use the same key ("%s"), but '. 'each field must use a unique key.', $key)); } $field_map[$key] = $field; } return $field_map; } protected function shouldShowOrderField() { return true; } private function adjustFieldsForDisplay(array $field_map) { $order = $this->getDefaultFieldOrder(); $head_keys = array(); $tail_keys = array(); $seen_tail = false; foreach ($order as $order_key) { if ($order_key === '...') { $seen_tail = true; continue; } if (!$seen_tail) { $head_keys[] = $order_key; } else { $tail_keys[] = $order_key; } } $head = array_select_keys($field_map, $head_keys); $body = array_diff_key($field_map, array_fuse($tail_keys)); $tail = array_select_keys($field_map, $tail_keys); $result = $head + $body + $tail; // Force the fulltext "query" field to the top unconditionally. $result = array_select_keys($result, array('query')) + $result; foreach ($this->getHiddenFields() as $hidden_key) { unset($result[$hidden_key]); } return $result; } protected function buildCustomSearchFields() { throw new PhutilMethodNotImplementedException(); } /** * Define the default display order for fields by returning a list of * field keys. * * You can use the special key `...` to mean "all unspecified fields go * here". This lets you easily put important fields at the top of the form, * standard fields in the middle of the form, and less important fields at * the bottom. * * For example, you might return a list like this: * * return array( * 'authorPHIDs', * 'reviewerPHIDs', * '...', * 'createdAfter', * 'createdBefore', * ); * * Any unspecified fields (including custom fields and fields added * automatically by infrastructure) will be put in the middle. * * @return list<string> Default ordering for field keys. */ protected function getDefaultFieldOrder() { return array(); } /** * Return a list of field keys which should be hidden from the viewer. * * @return list<string> Fields to hide. */ protected function getHiddenFields() { return array(); } public function getErrors() { return $this->errors; } public function addError($error) { $this->errors[] = $error; return $this; } /** * Return an application URI corresponding to the results page of a query. * Normally, this is something like `/application/query/QUERYKEY/`. * * @param string The query key to build a URI for. * @return string URI where the query can be executed. * @task uri */ public function getQueryResultsPageURI($query_key) { return $this->getURI('query/'.$query_key.'/'); } /** * Return an application URI for query management. This is used when, e.g., * a query deletion operation is cancelled. * * @return string URI where queries can be managed. * @task uri */ public function getQueryManagementURI() { return $this->getURI('query/edit/'); } public function getQueryBaseURI() { return $this->getURI(''); } public function getExportURI($query_key) { return $this->getURI('query/'.$query_key.'/export/'); } public function getCustomizeURI($query_key, $object_phid, $context_phid) { $params = array( 'search.objectPHID' => $object_phid, 'search.contextPHID' => $context_phid, ); $uri = $this->getURI('query/'.$query_key.'/customize/'); $uri = new PhutilURI($uri, $params); return phutil_string_cast($uri); } /** * Return the URI to a path within the application. Used to construct default * URIs for management and results. * * @return string URI to path. * @task uri */ abstract protected function getURI($path); /** * Return a human readable description of the type of objects this query * searches for. * * For example, "Tasks" or "Commits". * * @return string Human-readable description of what this engine is used to * find. */ abstract public function getResultTypeDescription(); public function newSavedQuery() { return id(new PhabricatorSavedQuery()) ->setEngineClassName(get_class($this)); } public function addNavigationItems(PHUIListView $menu) { $viewer = $this->requireViewer(); $menu->newLabel(pht('Queries')); $named_queries = $this->loadEnabledNamedQueries(); foreach ($named_queries as $query) { $key = $query->getQueryKey(); $uri = $this->getQueryResultsPageURI($key); $menu->newLink($query->getQueryName(), $uri, 'query/'.$key); } if ($viewer->isLoggedIn()) { $manage_uri = $this->getQueryManagementURI(); $menu->newLink(pht('Edit Queries...'), $manage_uri, 'query/edit'); } $menu->newLabel(pht('Search')); $advanced_uri = $this->getQueryResultsPageURI('advanced'); $menu->newLink(pht('Advanced Search'), $advanced_uri, 'query/advanced'); foreach ($this->navigationItems as $extra_item) { $menu->addMenuItem($extra_item); } return $this; } public function loadAllNamedQueries() { $viewer = $this->requireViewer(); $builtin = $this->getBuiltinQueries(); if ($this->namedQueries === null) { $named_queries = id(new PhabricatorNamedQueryQuery()) ->setViewer($viewer) ->withEngineClassNames(array(get_class($this))) ->withUserPHIDs( array( $viewer->getPHID(), PhabricatorNamedQuery::SCOPE_GLOBAL, )) ->execute(); $named_queries = mpull($named_queries, null, 'getQueryKey'); $builtin = mpull($builtin, null, 'getQueryKey'); foreach ($named_queries as $key => $named_query) { if ($named_query->getIsBuiltin()) { if (isset($builtin[$key])) { $named_queries[$key]->setQueryName($builtin[$key]->getQueryName()); unset($builtin[$key]); } else { unset($named_queries[$key]); } } unset($builtin[$key]); } $named_queries = msortv($named_queries, 'getNamedQuerySortVector'); $this->namedQueries = $named_queries; } return $this->namedQueries + $builtin; } public function loadEnabledNamedQueries() { $named_queries = $this->loadAllNamedQueries(); foreach ($named_queries as $key => $named_query) { if ($named_query->getIsBuiltin() && $named_query->getIsDisabled()) { unset($named_queries[$key]); } } return $named_queries; } public function getDefaultQueryKey() { $viewer = $this->requireViewer(); $configs = id(new PhabricatorNamedQueryConfigQuery()) ->setViewer($viewer) ->withEngineClassNames(array(get_class($this))) ->withScopePHIDs( array( $viewer->getPHID(), PhabricatorNamedQueryConfig::SCOPE_GLOBAL, )) ->execute(); $configs = msortv($configs, 'getStrengthSortVector'); $key_pinned = PhabricatorNamedQueryConfig::PROPERTY_PINNED; $map = $this->loadEnabledNamedQueries(); foreach ($configs as $config) { $pinned = $config->getConfigProperty($key_pinned); if (!isset($map[$pinned])) { continue; } return $pinned; } return head_key($map); } protected function setQueryProjects( PhabricatorCursorPagedPolicyAwareQuery $query, PhabricatorSavedQuery $saved) { $datasource = id(new PhabricatorProjectLogicalDatasource()) ->setViewer($this->requireViewer()); $projects = $saved->getParameter('projects', array()); $constraints = $datasource->evaluateTokens($projects); if ($constraints) { $query->withEdgeLogicConstraints( PhabricatorProjectObjectHasProjectEdgeType::EDGECONST, $constraints); } return $this; } /* -( Applications )------------------------------------------------------- */ protected function getApplicationURI($path = '') { return $this->getApplication()->getApplicationURI($path); } protected function getApplication() { if (!$this->application) { $class = $this->getApplicationClassName(); $this->application = id(new PhabricatorApplicationQuery()) ->setViewer($this->requireViewer()) ->withClasses(array($class)) ->withInstalled(true) ->executeOne(); if (!$this->application) { throw new Exception( pht( 'Application "%s" is not installed!', $class)); } } return $this->application; } abstract public function getApplicationClassName(); /* -( Constructing Engines )----------------------------------------------- */ /** * Load all available application search engines. * * @return list<PhabricatorApplicationSearchEngine> All available engines. * @task construct */ public static function getAllEngines() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->execute(); } /** * Get an engine by class name, if it exists. * * @return PhabricatorApplicationSearchEngine|null Engine, or null if it does * not exist. * @task construct */ public static function getEngineByClassName($class_name) { return idx(self::getAllEngines(), $class_name); } /* -( Builtin Queries )---------------------------------------------------- */ /** * @task builtin */ public function getBuiltinQueries() { $names = $this->getBuiltinQueryNames(); $queries = array(); $sequence = 0; foreach ($names as $key => $name) { $queries[$key] = id(new PhabricatorNamedQuery()) ->setUserPHID(PhabricatorNamedQuery::SCOPE_GLOBAL) ->setEngineClassName(get_class($this)) ->setQueryName($name) ->setQueryKey($key) ->setSequence((1 << 24) + $sequence++) ->setIsBuiltin(true); } return $queries; } /** * @task builtin */ public function getBuiltinQuery($query_key) { if (!$this->isBuiltinQuery($query_key)) { throw new Exception(pht("'%s' is not a builtin!", $query_key)); } return idx($this->getBuiltinQueries(), $query_key); } /** * @task builtin */ protected function getBuiltinQueryNames() { return array(); } /** * @task builtin */ public function isBuiltinQuery($query_key) { $builtins = $this->getBuiltinQueries(); return isset($builtins[$query_key]); } /** * @task builtin */ public function buildSavedQueryFromBuiltin($query_key) { throw new Exception(pht("Builtin '%s' is not supported!", $query_key)); } /* -( Reading Utilities )--------------------------------------------------- */ /** * Read a list of user PHIDs from a request in a flexible way. This method * supports either of these forms: * * users[]=alincoln&users[]=htaft * users=alincoln,htaft * * Additionally, users can be specified either by PHID or by name. * * The main goal of this flexibility is to allow external programs to generate * links to pages (like "alincoln's open revisions") without needing to make * API calls. * * @param AphrontRequest Request to read user PHIDs from. * @param string Key to read in the request. * @param list<const> Other permitted PHID types. * @return list<phid> List of user PHIDs and selector functions. * @task read */ protected function readUsersFromRequest( AphrontRequest $request, $key, array $allow_types = array()) { $list = $this->readListFromRequest($request, $key); $phids = array(); $names = array(); $allow_types = array_fuse($allow_types); $user_type = PhabricatorPeopleUserPHIDType::TYPECONST; foreach ($list as $item) { $type = phid_get_type($item); if ($type == $user_type) { $phids[] = $item; } else if (isset($allow_types[$type])) { $phids[] = $item; } else { if (PhabricatorTypeaheadDatasource::isFunctionToken($item)) { // If this is a function, pass it through unchanged; we'll evaluate // it later. $phids[] = $item; } else { $names[] = $item; } } } if ($names) { $users = id(new PhabricatorPeopleQuery()) ->setViewer($this->requireViewer()) ->withUsernames($names) ->execute(); foreach ($users as $user) { $phids[] = $user->getPHID(); } $phids = array_unique($phids); } return $phids; } /** * Read a list of subscribers from a request in a flexible way. * * @param AphrontRequest Request to read PHIDs from. * @param string Key to read in the request. * @return list<phid> List of object PHIDs. * @task read */ protected function readSubscribersFromRequest( AphrontRequest $request, $key) { return $this->readUsersFromRequest( $request, $key, array( PhabricatorProjectProjectPHIDType::TYPECONST, )); } /** * Read a list of generic PHIDs from a request in a flexible way. Like * @{method:readUsersFromRequest}, this method supports either array or * comma-delimited forms. Objects can be specified either by PHID or by * object name. * * @param AphrontRequest Request to read PHIDs from. * @param string Key to read in the request. * @param list<const> Optional, list of permitted PHID types. * @return list<phid> List of object PHIDs. * * @task read */ protected function readPHIDsFromRequest( AphrontRequest $request, $key, array $allow_types = array()) { $list = $this->readListFromRequest($request, $key); $objects = id(new PhabricatorObjectQuery()) ->setViewer($this->requireViewer()) ->withNames($list) ->execute(); $list = mpull($objects, 'getPHID'); if (!$list) { return array(); } // If only certain PHID types are allowed, filter out all the others. if ($allow_types) { $allow_types = array_fuse($allow_types); foreach ($list as $key => $phid) { if (empty($allow_types[phid_get_type($phid)])) { unset($list[$key]); } } } return $list; } /** * Read a list of items from the request, in either array format or string * format: * * list[]=item1&list[]=item2 * list=item1,item2 * * This provides flexibility when constructing URIs, especially from external * sources. * * @param AphrontRequest Request to read strings from. * @param string Key to read in the request. * @return list<string> List of values. */ protected function readListFromRequest( AphrontRequest $request, $key) { $list = $request->getArr($key, null); if ($list === null) { $list = $request->getStrList($key); } if (!$list) { return array(); } return $list; } protected function readBoolFromRequest( AphrontRequest $request, $key) { if (!phutil_nonempty_string($request->getStr($key))) { return null; } return $request->getBool($key); } protected function getBoolFromQuery(PhabricatorSavedQuery $query, $key) { $value = $query->getParameter($key); if ($value === null) { return $value; } return $value ? 'true' : 'false'; } /* -( Dates )-------------------------------------------------------------- */ /** * @task dates */ protected function parseDateTime($date_time) { if (!strlen($date_time)) { return null; } return PhabricatorTime::parseLocalTime($date_time, $this->requireViewer()); } /** * @task dates */ protected function buildDateRange( AphrontFormView $form, PhabricatorSavedQuery $saved_query, $start_key, $start_name, $end_key, $end_name) { $start_str = $saved_query->getParameter($start_key); $start = null; if (strlen($start_str)) { $start = $this->parseDateTime($start_str); if (!$start) { $this->addError( pht( '"%s" date can not be parsed.', $start_name)); } } $end_str = $saved_query->getParameter($end_key); $end = null; if (strlen($end_str)) { $end = $this->parseDateTime($end_str); if (!$end) { $this->addError( pht( '"%s" date can not be parsed.', $end_name)); } } if ($start && $end && ($start >= $end)) { $this->addError( pht( '"%s" must be a date before "%s".', $start_name, $end_name)); } $form ->appendChild( id(new PHUIFormFreeformDateControl()) ->setName($start_key) ->setLabel($start_name) ->setValue($start_str)) ->appendChild( id(new AphrontFormTextControl()) ->setName($end_key) ->setLabel($end_name) ->setValue($end_str)); } /* -( Paging and Executing Queries )--------------------------------------- */ protected function newResultBuckets() { return array(); } public function getResultBucket(PhabricatorSavedQuery $saved) { $key = $saved->getParameter('bucket'); if ($key == self::BUCKET_NONE) { return null; } $buckets = $this->newResultBuckets(); return idx($buckets, $key); } public function getPageSize(PhabricatorSavedQuery $saved) { $bucket = $this->getResultBucket($saved); $limit = (int)$saved->getParameter('limit'); if ($limit > 0) { if ($bucket) { $bucket->setPageSize($limit); } return $limit; } if ($bucket) { return $bucket->getPageSize(); } return 100; } public function shouldUseOffsetPaging() { return false; } public function newPagerForSavedQuery(PhabricatorSavedQuery $saved) { if ($this->shouldUseOffsetPaging()) { $pager = new PHUIPagerView(); } else { $pager = new AphrontCursorPagerView(); } $page_size = $this->getPageSize($saved); if (is_finite($page_size)) { $pager->setPageSize($page_size); } else { // Consider an INF pagesize to mean a large finite pagesize. // TODO: It would be nice to handle this more gracefully, but math // with INF seems to vary across PHP versions, systems, and runtimes. $pager->setPageSize(0xFFFF); } return $pager; } public function executeQuery( PhabricatorPolicyAwareQuery $query, AphrontView $pager) { $query->setViewer($this->requireViewer()); if ($this->shouldUseOffsetPaging()) { $objects = $query->executeWithOffsetPager($pager); } else { $objects = $query->executeWithCursorPager($pager); } $this->didExecuteQuery($query); return $objects; } protected function didExecuteQuery(PhabricatorPolicyAwareQuery $query) { return; } /* -( Rendering )---------------------------------------------------------- */ public function setRequest(AphrontRequest $request) { $this->request = $request; return $this; } public function getRequest() { return $this->request; } public function renderResults( array $objects, PhabricatorSavedQuery $query) { $phids = $this->getRequiredHandlePHIDsForResultList($objects, $query); if ($phids) { $handles = id(new PhabricatorHandleQuery()) ->setViewer($this->requireViewer()) ->witHPHIDs($phids) ->execute(); } else { $handles = array(); } return $this->renderResultList($objects, $query, $handles); } protected function getRequiredHandlePHIDsForResultList( array $objects, PhabricatorSavedQuery $query) { return array(); } abstract protected function renderResultList( array $objects, PhabricatorSavedQuery $query, array $handles); /* -( Application Search )------------------------------------------------- */ public function getSearchFieldsForConduit() { $standard_fields = $this->buildSearchFields(); $fields = array(); foreach ($standard_fields as $field_key => $field) { $conduit_key = $field->getConduitKey(); if (isset($fields[$conduit_key])) { $other = $fields[$conduit_key]; $other_key = $other->getKey(); throw new Exception( pht( 'SearchFields "%s" (of class "%s") and "%s" (of class "%s") both '. 'define the same Conduit key ("%s"). Keys must be unique.', $field_key, get_class($field), $other_key, get_class($other), $conduit_key)); } $fields[$conduit_key] = $field; } // These are handled separately for Conduit, so don't show them as // supported. unset($fields['order']); unset($fields['limit']); $viewer = $this->requireViewer(); foreach ($fields as $key => $field) { $field->setViewer($viewer); } return $fields; } public function buildConduitResponse( ConduitAPIRequest $request, ConduitAPIMethod $method) { $viewer = $this->requireViewer(); $query_key = $request->getValue('queryKey'); $is_empty_query_key = phutil_string_cast($query_key) === ''; if ($is_empty_query_key) { $saved_query = new PhabricatorSavedQuery(); } else if ($this->isBuiltinQuery($query_key)) { $saved_query = $this->buildSavedQueryFromBuiltin($query_key); } else { $saved_query = id(new PhabricatorSavedQueryQuery()) ->setViewer($viewer) ->withQueryKeys(array($query_key)) ->executeOne(); if (!$saved_query) { throw new Exception( pht( 'Query key "%s" does not correspond to a valid query.', $query_key)); } } $constraints = $request->getValue('constraints', array()); if (!is_array($constraints)) { throw new Exception( pht( 'Parameter "constraints" must be a map of constraints, got "%s".', phutil_describe_type($constraints))); } $fields = $this->getSearchFieldsForConduit(); foreach ($fields as $key => $field) { if (!$field->getConduitParameterType()) { unset($fields[$key]); } } $valid_constraints = array(); foreach ($fields as $field) { foreach ($field->getValidConstraintKeys() as $key) { $valid_constraints[$key] = true; } } foreach ($constraints as $key => $constraint) { if (empty($valid_constraints[$key])) { throw new Exception( pht( 'Constraint "%s" is not a valid constraint for this query.', $key)); } } foreach ($fields as $field) { if (!$field->getValueExistsInConduitRequest($constraints)) { continue; } $value = $field->readValueFromConduitRequest( $constraints, $request->getIsStrictlyTyped()); $saved_query->setParameter($field->getKey(), $value); } // NOTE: Currently, when running an ad-hoc query we never persist it into // a saved query. We might want to add an option to do this in the future // (for example, to enable a CLI-to-Web workflow where user can view more // details about results by following a link), but have no use cases for // it today. If we do identify a use case, we could save the query here. $query = $this->buildQueryFromSavedQuery($saved_query); $pager = $this->newPagerForSavedQuery($saved_query); $attachments = $this->getConduitSearchAttachments(); // TODO: Validate this better. $attachment_specs = $request->getValue('attachments', array()); $attachments = array_select_keys( $attachments, array_keys($attachment_specs)); foreach ($attachments as $key => $attachment) { $attachment->setViewer($viewer); } foreach ($attachments as $key => $attachment) { $attachment->willLoadAttachmentData($query, $attachment_specs[$key]); } $this->setQueryOrderForConduit($query, $request); $this->setPagerLimitForConduit($pager, $request); $this->setPagerOffsetsForConduit($pager, $request); $objects = $this->executeQuery($query, $pager); $data = array(); if ($objects) { $field_extensions = $this->getConduitFieldExtensions(); $extension_data = array(); foreach ($field_extensions as $key => $extension) { $extension_data[$key] = $extension->loadExtensionConduitData($objects); } $attachment_data = array(); foreach ($attachments as $key => $attachment) { $attachment_data[$key] = $attachment->loadAttachmentData( $objects, $attachment_specs[$key]); } foreach ($objects as $object) { $field_map = $this->getObjectWireFieldsForConduit( $object, $field_extensions, $extension_data); $attachment_map = array(); foreach ($attachments as $key => $attachment) { $attachment_map[$key] = $attachment->getAttachmentForObject( $object, $attachment_data[$key], $attachment_specs[$key]); } // If this is empty, we still want to emit a JSON object, not a // JSON list. if (!$attachment_map) { $attachment_map = (object)$attachment_map; } $id = (int)$object->getID(); $phid = $object->getPHID(); $data[] = array( 'id' => $id, 'type' => phid_get_type($phid), 'phid' => $phid, 'fields' => $field_map, 'attachments' => $attachment_map, ); } } return array( 'data' => $data, 'maps' => $method->getQueryMaps($query), 'query' => array( // This may be `null` if we have not saved the query. 'queryKey' => $saved_query->getQueryKey(), ), 'cursor' => array( 'limit' => $pager->getPageSize(), 'after' => $pager->getNextPageID(), 'before' => $pager->getPrevPageID(), 'order' => $request->getValue('order'), ), ); } public function getAllConduitFieldSpecifications() { $extensions = $this->getConduitFieldExtensions(); $object = $this->newQuery()->newResultObject(); $map = array(); foreach ($extensions as $extension) { $specifications = $extension->getFieldSpecificationsForConduit($object); foreach ($specifications as $specification) { $key = $specification->getKey(); if (isset($map[$key])) { throw new Exception( pht( 'Two field specifications share the same key ("%s"). Each '. 'specification must have a unique key.', $key)); } $map[$key] = $specification; } } return $map; } private function getEngineExtensions() { $extensions = PhabricatorSearchEngineExtension::getAllEnabledExtensions(); foreach ($extensions as $key => $extension) { $extension ->setViewer($this->requireViewer()) ->setSearchEngine($this); } $object = $this->newResultObject(); foreach ($extensions as $key => $extension) { if (!$extension->supportsObject($object)) { unset($extensions[$key]); } } return $extensions; } private function getConduitFieldExtensions() { $extensions = $this->getEngineExtensions(); $object = $this->newResultObject(); foreach ($extensions as $key => $extension) { if (!$extension->getFieldSpecificationsForConduit($object)) { unset($extensions[$key]); } } return $extensions; } private function setQueryOrderForConduit($query, ConduitAPIRequest $request) { $order = $request->getValue('order'); if ($order === null) { return; } if (is_scalar($order)) { $query->setOrder($order); } else { $query->setOrderVector($order); } } private function setPagerLimitForConduit($pager, ConduitAPIRequest $request) { $limit = $request->getValue('limit'); // If there's no limit specified and the query uses a weird huge page // size, just leave it at the default gigantic page size. Otherwise, // make sure it's between 1 and 100, inclusive. if ($limit === null) { if ($pager->getPageSize() >= 0xFFFF) { return; } else { $limit = 100; } } if ($limit > 100) { throw new Exception( pht( 'Maximum page size for Conduit API method calls is 100, but '. 'this call specified %s.', $limit)); } if ($limit < 1) { throw new Exception( pht( 'Minimum page size for API searches is 1, but this call '. 'specified %s.', $limit)); } $pager->setPageSize($limit); } private function setPagerOffsetsForConduit( $pager, ConduitAPIRequest $request) { $before_id = $request->getValue('before'); if ($before_id !== null) { $pager->setBeforeID($before_id); } $after_id = $request->getValue('after'); if ($after_id !== null) { $pager->setAfterID($after_id); } } protected function getObjectWireFieldsForConduit( $object, array $field_extensions, array $extension_data) { $fields = array(); foreach ($field_extensions as $key => $extension) { $data = idx($extension_data, $key, array()); $fields += $extension->getFieldValuesForConduit($object, $data); } return $fields; } public function getConduitSearchAttachments() { $extensions = $this->getEngineExtensions(); $object = $this->newResultObject(); $attachments = array(); foreach ($extensions as $extension) { $extension_attachments = $extension->getSearchAttachments($object); foreach ($extension_attachments as $attachment) { $attachment_key = $attachment->getAttachmentKey(); if (isset($attachments[$attachment_key])) { $other = $attachments[$attachment_key]; throw new Exception( pht( 'Two search engine attachments (of classes "%s" and "%s") '. 'specify the same attachment key ("%s"); keys must be unique.', get_class($attachment), get_class($other), $attachment_key)); } $attachments[$attachment_key] = $attachment; } } return $attachments; } final public function renderNewUserView() { $body = $this->getNewUserBody(); if (!$body) { return null; } return $body; } protected function getNewUserHeader() { return null; } protected function getNewUserBody() { return null; } public function newUseResultsActions(PhabricatorSavedQuery $saved) { return array(); } /* -( Export )------------------------------------------------------------- */ public function canExport() { $fields = $this->newExportFields(); return (bool)$fields; } final public function newExportFieldList() { $object = $this->newResultObject(); $builtin_fields = array( id(new PhabricatorIDExportField()) ->setKey('id') ->setLabel(pht('ID')), ); if ($object->getConfigOption(LiskDAO::CONFIG_AUX_PHID)) { $builtin_fields[] = id(new PhabricatorPHIDExportField()) ->setKey('phid') ->setLabel(pht('PHID')); } $fields = mpull($builtin_fields, null, 'getKey'); $export_fields = $this->newExportFields(); foreach ($export_fields as $export_field) { $key = $export_field->getKey(); if (isset($fields[$key])) { throw new Exception( pht( 'Search engine ("%s") defines an export field with a key ("%s") '. 'that collides with another field. Each field must have a '. 'unique key.', get_class($this), $key)); } $fields[$key] = $export_field; } $extensions = $this->newExportExtensions(); foreach ($extensions as $extension) { $extension_fields = $extension->newExportFields(); foreach ($extension_fields as $extension_field) { $key = $extension_field->getKey(); if (isset($fields[$key])) { throw new Exception( pht( 'Export engine extension ("%s") defines an export field with '. 'a key ("%s") that collides with another field. Each field '. 'must have a unique key.', get_class($extension_field), $key)); } $fields[$key] = $extension_field; } } return $fields; } final public function newExport(array $objects) { $object = $this->newResultObject(); $has_phid = $object->getConfigOption(LiskDAO::CONFIG_AUX_PHID); $objects = array_values($objects); $n = count($objects); $maps = array(); foreach ($objects as $object) { $map = array( 'id' => $object->getID(), ); if ($has_phid) { $map['phid'] = $object->getPHID(); } $maps[] = $map; } $export_data = $this->newExportData($objects); $export_data = array_values($export_data); if (count($export_data) !== count($objects)) { throw new Exception( pht( 'Search engine ("%s") exported the wrong number of objects, '. 'expected %s but got %s.', get_class($this), phutil_count($objects), phutil_count($export_data))); } for ($ii = 0; $ii < $n; $ii++) { $maps[$ii] += $export_data[$ii]; } $extensions = $this->newExportExtensions(); foreach ($extensions as $extension) { $extension_data = $extension->newExportData($objects); $extension_data = array_values($extension_data); if (count($export_data) !== count($objects)) { throw new Exception( pht( 'Export engine extension ("%s") exported the wrong number of '. 'objects, expected %s but got %s.', get_class($extension), phutil_count($objects), phutil_count($export_data))); } for ($ii = 0; $ii < $n; $ii++) { $maps[$ii] += $extension_data[$ii]; } } return $maps; } protected function newExportFields() { return array(); } protected function newExportData(array $objects) { throw new PhutilMethodNotImplementedException(); } private function newExportExtensions() { $object = $this->newResultObject(); $viewer = $this->requireViewer(); $extensions = PhabricatorExportEngineExtension::getAllExtensions(); $supported = array(); foreach ($extensions as $extension) { $extension = clone $extension; $extension->setViewer($viewer); if ($extension->supportsObject($object)) { $supported[] = $extension; } } return $supported; } }