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(); final public function getDocumentationPages(PhabricatorUser $viewer) { $pages = $this->newDocumentationPages($viewer); return $pages; } protected function newDocumentationPages(PhabricatorUser $viewer) { return array(); } /** * @return ConduitAPIDocumentationPage */ final protected function newDocumentationPage(PhabricatorUser $viewer) { return id(new ConduitAPIDocumentationPage()) ->setIconIcon('fa-chevron-right'); } /** * @return ConduitAPIDocumentationPage */ final protected function newDocumentationBoxPage( PhabricatorUser $viewer, $title, $content) { $box_view = id(new PHUIObjectBoxView()) ->setHeaderText($title) ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setTable($content); return $this->newDocumentationPage($viewer) ->setName($title) ->setContent($box_view); } /** * @return array Pairs of parameter name and their type */ abstract protected function defineParamTypes(); /** * @return string Human-readable text description of the return format, * for example 'array | null' */ abstract protected function defineReturnType(); /** * @return array Pairs of error code and the corresponding error message */ protected function defineErrorTypes() { return array(); } abstract protected function execute(ConduitAPIRequest $request); public function isInternalAPI() { return false; } public function getParamTypes() { $types = $this->defineParamTypes(); $query = $this->newQueryObject(); if ($query) { $types['order'] = 'optional order'; $types += $this->getPagerParamTypes(); } return $types; } /** * @return string Human-readable text description of the return format, * for example 'array | null' */ public function getReturnType() { return $this->defineReturnType(); } /** * @return array Pairs of error code and the corresponding error message */ 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 string 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() { return self::SCOPE_NEVER; } public function executeMethod(ConduitAPIRequest $request) { $this->setViewer($request->getUser()); $client = $this->newConduitCallProxyClient($request); if ($client) { // We're proxying, so just make an intracluster call. return $client->callMethodSynchronous( $this->getAPIMethodName(), $request->getAllParameters()); } return $this->execute($request); } protected function newConduitCallProxyClient(ConduitAPIRequest $request) { return null; } abstract public function getAPIMethodName(); /** * Return a key which sorts methods by application name, then method status, * then method name. * * @return string For example 'almanac.0.namespace.edit' or 'user.2.enable' */ 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 self::newClassMapQuery()->execute(); } private static function newClassMapQuery() { return id(new PhutilClassMapQuery()) ->setAncestorClass(self::class) ->setUniqueMethod('getAPIMethodName'); } public static function getConduitMethod($method_name) { return id(new PhabricatorCachedClassMapQuery()) ->setClassMapQuery(self::newClassMapQuery()) ->setMapKeyMethod('getAPIMethodName') ->loadClass($method_name); } /** * Whether to require a session key for calling the API method. * * @return bool Defaults to true */ public function shouldRequireAuthentication() { return true; } /** * Whether to allow public access. Related to the `policy.allow-public` * global setting and policies set for the corresponding application. * * @return bool Defaults to false */ public function shouldAllowPublic() { return false; } /** * Whether not to guard writes against CSRF. See @{class:AphrontWriteGuard}. * * @return bool Defaults to 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 disabled. * * @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)', ); } /** * @return AphrontCursorPagerView * @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; } 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); } final protected function newRemarkupDocumentationView($remarkup) { $viewer = $this->getViewer(); $view = new PHUIRemarkupView($viewer, $remarkup); $view->setRemarkupOptions( array( PHUIRemarkupView::OPTION_PRESERVE_LINEBREAKS => false, )); return id(new PHUIBoxView()) ->appendChild($view) ->addPadding(PHUI::PADDING_LARGE); } }