diff --git a/src/applications/conduit/method/ConduitAPIMethod.php b/src/applications/conduit/method/ConduitAPIMethod.php index eb972a8d90..4fd7c416bb 100644 --- a/src/applications/conduit/method/ConduitAPIMethod.php +++ b/src/applications/conduit/method/ConduitAPIMethod.php @@ -1,396 +1,406 @@ getMethodDescription(); } /** * Get a detailed description of the method. * * This method should return remarkup. * * @return string Detailed description of the method. * @task info */ abstract public function getMethodDescription(); public function getMethodDocumentation() { return null; } abstract protected function defineParamTypes(); abstract protected function defineReturnType(); protected function defineErrorTypes() { return array(); } abstract protected function execute(ConduitAPIRequest $request); public function getParamTypes() { $types = $this->defineParamTypes(); $query = $this->newQueryObject(); if ($query) { $types['order'] = 'optional order'; $types += $this->getPagerParamTypes(); } return $types; } public function getReturnType() { return $this->defineReturnType(); } public function getErrorTypes() { return $this->defineErrorTypes(); } /** * This is mostly for compatibility with * @{class:PhabricatorCursorPagedPolicyAwareQuery}. */ public function getID() { return $this->getAPIMethodName(); } /** * Get the status for this method (e.g., stable, unstable or deprecated). * Should return a METHOD_STATUS_* constant. By default, methods are * "stable". * * @return const METHOD_STATUS_* constant. * @task status */ public function getMethodStatus() { return self::METHOD_STATUS_STABLE; } /** * Optional description to supplement the method status. In particular, if * a method is deprecated, you can return a string here describing the reason * for deprecation and stable alternatives. * * @return string|null Description of the method status, if available. * @task status */ public function getMethodStatusDescription() { return null; } public function getErrorDescription($error_code) { return idx($this->getErrorTypes(), $error_code, pht('Unknown Error')); } public function getRequiredScope() { // by default, conduit methods are not accessible via OAuth return PhabricatorOAuthServerScope::SCOPE_NOT_ACCESSIBLE; } public function executeMethod(ConduitAPIRequest $request) { $this->setViewer($request->getUser()); return $this->execute($request); } abstract public function getAPIMethodName(); /** * Return a key which sorts methods by application name, then method status, * then method name. */ public function getSortOrder() { $name = $this->getAPIMethodName(); $map = array( self::METHOD_STATUS_STABLE => 0, self::METHOD_STATUS_UNSTABLE => 1, self::METHOD_STATUS_DEPRECATED => 2, ); $ord = idx($map, $this->getMethodStatus(), 0); list($head, $tail) = explode('.', $name, 2); return "{$head}.{$ord}.{$tail}"; } + public static function getMethodStatusMap() { + $map = array( + self::METHOD_STATUS_STABLE => pht('Stable'), + self::METHOD_STATUS_UNSTABLE => pht('Unstable'), + self::METHOD_STATUS_DEPRECATED => pht('Deprecated'), + ); + + return $map; + } + public function getApplicationName() { return head(explode('.', $this->getAPIMethodName(), 2)); } public static function loadAllConduitMethods() { return id(new PhutilClassMapQuery()) ->setAncestorClass(__CLASS__) ->setUniqueMethod('getAPIMethodName') ->execute(); } public static function getConduitMethod($method_name) { $method_map = self::loadAllConduitMethods(); return idx($method_map, $method_name); } public function shouldRequireAuthentication() { return true; } public function shouldAllowPublic() { return false; } public function shouldAllowUnguardedWrites() { return false; } /** * Optionally, return a @{class:PhabricatorApplication} which this call is * part of. The call will be disabled when the application is uninstalled. * * @return PhabricatorApplication|null Related application. */ public function getApplication() { return null; } protected function formatStringConstants($constants) { foreach ($constants as $key => $value) { $constants[$key] = '"'.$value.'"'; } $constants = implode(', ', $constants); return 'string-constant<'.$constants.'>'; } public static function getParameterMetadataKey($key) { if (strncmp($key, 'api.', 4) === 0) { // All keys passed beginning with "api." are always metadata keys. return substr($key, 4); } else { switch ($key) { // These are real keys which always belong to request metadata. case 'access_token': case 'scope': case 'output': // This is not a real metadata key; it is included here only to // prevent Conduit methods from defining it. case '__conduit__': // This is prevented globally as a blanket defense against OAuth // redirection attacks. It is included here to stop Conduit methods // from defining it. case 'code': // This is not a real metadata key, but the presence of this // parameter triggers an alternate request decoding pathway. case 'params': return $key; } } return null; } final public function setViewer(PhabricatorUser $viewer) { $this->viewer = $viewer; return $this; } final public function getViewer() { return $this->viewer; } /* -( Paging Results )----------------------------------------------------- */ /** * @task pager */ protected function getPagerParamTypes() { return array( 'before' => 'optional string', 'after' => 'optional string', 'limit' => 'optional int (default = 100)', ); } /** * @task pager */ protected function newPager(ConduitAPIRequest $request) { $limit = $request->getValue('limit', 100); $limit = min(1000, $limit); $limit = max(1, $limit); $pager = id(new AphrontCursorPagerView()) ->setPageSize($limit); $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); } return $pager; } /** * @task pager */ protected function addPagerResults( array $results, AphrontCursorPagerView $pager) { $results['cursor'] = array( 'limit' => $pager->getPageSize(), 'after' => $pager->getNextPageID(), 'before' => $pager->getPrevPageID(), ); return $results; } /* -( Implementing Query Methods )----------------------------------------- */ public function newQueryObject() { return null; } protected function newQueryForRequest(ConduitAPIRequest $request) { $query = $this->newQueryObject(); if (!$query) { throw new Exception( pht( 'You can not call newQueryFromRequest() in this method ("%s") '. 'because it does not implement newQueryObject().', get_class($this))); } if (!($query instanceof PhabricatorCursorPagedPolicyAwareQuery)) { throw new Exception( pht( 'Call to method newQueryObject() did not return an object of class '. '"%s".', 'PhabricatorCursorPagedPolicyAwareQuery')); } $query->setViewer($request->getUser()); $order = $request->getValue('order'); if ($order !== null) { if (is_scalar($order)) { $query->setOrder($order); } else { $query->setOrderVector($order); } } return $query; } /* -( PhabricatorPolicyInterface )----------------------------------------- */ public function getPHID() { return null; } public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { // Application methods get application visibility; other methods get open // visibility. $application = $this->getApplication(); if ($application) { return $application->getPolicy($capability); } return PhabricatorPolicies::getMostOpenPolicy(); } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { if (!$this->shouldRequireAuthentication()) { // Make unauthenticated methods universally visible. return true; } return false; } public function describeAutomaticCapability($capability) { return null; } protected function hasApplicationCapability( $capability, PhabricatorUser $viewer) { $application = $this->getApplication(); if (!$application) { return false; } return PhabricatorPolicyFilter::hasCapability( $viewer, $application, $capability); } protected function requireApplicationCapability( $capability, PhabricatorUser $viewer) { $application = $this->getApplication(); if (!$application) { return; } PhabricatorPolicyFilter::requireCapability( $viewer, $this->getApplication(), $capability); } } diff --git a/src/applications/conduit/query/PhabricatorConduitLogQuery.php b/src/applications/conduit/query/PhabricatorConduitLogQuery.php index 05b6ad9023..0e457dc74c 100644 --- a/src/applications/conduit/query/PhabricatorConduitLogQuery.php +++ b/src/applications/conduit/query/PhabricatorConduitLogQuery.php @@ -1,38 +1,69 @@ methods = $methods; return $this; } + public function withMethodStatuses(array $statuses) { + $this->methodStatuses = $statuses; + return $this; + } + public function newResultObject() { return new PhabricatorConduitMethodCallLog(); } protected function loadPage() { return $this->loadStandardPage($this->newResultObject()); } protected function buildWhereClauseParts(AphrontDatabaseConnection $conn) { $where = parent::buildWhereClauseParts($conn); - if ($this->methods) { + if ($this->methods !== null) { $where[] = qsprintf( $conn, 'method IN (%Ls)', $this->methods); } + if ($this->methodStatuses !== null) { + $statuses = array_fuse($this->methodStatuses); + + $methods = id(new PhabricatorConduitMethodQuery()) + ->setViewer($this->getViewer()) + ->execute(); + + $method_names = array(); + foreach ($methods as $method) { + $status = $method->getMethodStatus(); + if (isset($statuses[$status])) { + $method_names[] = $method->getAPIMethodName(); + } + } + + if (!$method_names) { + throw new PhabricatorEmptyQueryException(); + } + + $where[] = qsprintf( + $conn, + 'method IN (%Ls)', + $method_names); + } + return $where; } public function getQueryApplicationClass() { return 'PhabricatorConduitApplication'; } } diff --git a/src/applications/conduit/query/PhabricatorConduitLogSearchEngine.php b/src/applications/conduit/query/PhabricatorConduitLogSearchEngine.php index f7e1c54087..cb261e323e 100644 --- a/src/applications/conduit/query/PhabricatorConduitLogSearchEngine.php +++ b/src/applications/conduit/query/PhabricatorConduitLogSearchEngine.php @@ -1,160 +1,176 @@ newQuery(); if ($map['methods']) { $query->withMethods($map['methods']); } + if ($map['statuses']) { + $query->withMethodStatuses($map['statuses']); + } + return $query; } protected function buildCustomSearchFields() { return array( id(new PhabricatorSearchStringListField()) ->setKey('methods') ->setLabel(pht('Methods')) ->setDescription(pht('Find calls to specific methods.')), + id(new PhabricatorSearchCheckboxesField()) + ->setKey('statuses') + ->setLabel(pht('Method Status')) + ->setAliases(array('status')) + ->setOptions(ConduitAPIMethod::getMethodStatusMap()), ); } protected function getURI($path) { return '/conduit/log/'.$path; } protected function getBuiltinQueryNames() { $names = array( 'all' => pht('All Logs'), + 'deprecated' => pht('Deprecated Calls'), ); return $names; } public function buildSavedQueryFromBuiltin($query_key) { $query = $this->newSavedQuery(); $query->setQueryKey($query_key); switch ($query_key) { + case 'deprecated': + return $query->setParameter( + 'statuses', + array( + ConduitAPIMethod::METHOD_STATUS_DEPRECATED, + )); case 'all': return $query; } return parent::buildSavedQueryFromBuiltin($query_key); } protected function renderResultList( array $logs, PhabricatorSavedQuery $query, array $handles) { assert_instances_of($logs, 'PhabricatorConduitMethodCallLog'); $viewer = $this->requireViewer(); $methods = id(new PhabricatorConduitMethodQuery()) ->setViewer($viewer) ->execute(); $methods = mpull($methods, null, 'getAPIMethodName'); Javelin::initBehavior('phabricator-tooltips'); $viewer = $this->requireViewer(); $rows = array(); foreach ($logs as $log) { $caller_phid = $log->getCallerPHID(); if ($caller_phid) { $caller = $viewer->renderHandle($caller_phid); } else { $caller = null; } $method = idx($methods, $log->getMethod()); if ($method) { $method_status = $method->getMethodStatus(); } else { $method_status = null; } switch ($method_status) { case ConduitAPIMethod::METHOD_STATUS_STABLE: $status = null; break; case ConduitAPIMethod::METHOD_STATUS_UNSTABLE: $status = id(new PHUIIconView()) ->setIconFont('fa-exclamation-triangle yellow') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Unstable'), )); break; case ConduitAPIMethod::METHOD_STATUS_DEPRECATED: $status = id(new PHUIIconView()) ->setIconFont('fa-exclamation-triangle red') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Deprecated'), )); break; default: $status = id(new PHUIIconView()) ->setIconFont('fa-question-circle') ->addSigil('has-tooltip') ->setMetadata( array( 'tip' => pht('Unknown ("%s")', $status), )); break; } $rows[] = array( $status, $log->getMethod(), $caller, $log->getError(), pht('%s us', new PhutilNumber($log->getDuration())), phabricator_datetime($log->getDateCreated(), $viewer), ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( null, pht('Method'), pht('Caller'), pht('Error'), pht('Duration'), pht('Date'), )) ->setColumnClasses( array( null, 'pri', null, 'wide right', null, null, )); return id(new PhabricatorApplicationSearchResultView()) ->setTable($table) ->setNoDataString(pht('No matching calls in log.')); } }