diff --git a/src/applications/conduit/controller/PhabricatorConduitAPIController.php b/src/applications/conduit/controller/PhabricatorConduitAPIController.php index e335772b7f..005b23d505 100644 --- a/src/applications/conduit/controller/PhabricatorConduitAPIController.php +++ b/src/applications/conduit/controller/PhabricatorConduitAPIController.php @@ -1,672 +1,685 @@ getURIData('method'); $time_start = microtime(true); $api_request = null; $method_implementation = null; $log = new PhabricatorConduitMethodCallLog(); $log->setMethod($method); $metadata = array(); $multimeter = MultimeterControl::getInstance(); if ($multimeter) { $multimeter->setEventContext('api.'.$method); } try { list($metadata, $params) = $this->decodeConduitParams($request, $method); $call = new ConduitCall($method, $params); $method_implementation = $call->getMethodImplementation(); $result = null; // TODO: The relationship between ConduitAPIRequest and ConduitCall is a // little odd here and could probably be improved. Specifically, the // APIRequest is a sub-object of the Call, which does not parallel the // role of AphrontRequest (which is an indepenent object). // In particular, the setUser() and getUser() existing independently on // the Call and APIRequest is very awkward. $api_request = $call->getAPIRequest(); $allow_unguarded_writes = false; $auth_error = null; $conduit_username = '-'; if ($call->shouldRequireAuthentication()) { $auth_error = $this->authenticateUser($api_request, $metadata, $method); // If we've explicitly authenticated the user here and either done // CSRF validation or are using a non-web authentication mechanism. $allow_unguarded_writes = true; if ($auth_error === null) { $conduit_user = $api_request->getUser(); if ($conduit_user && $conduit_user->getPHID()) { $conduit_username = $conduit_user->getUsername(); } $call->setUser($api_request->getUser()); } } $access_log = PhabricatorAccessLog::getLog(); if ($access_log) { $access_log->setData( array( 'u' => $conduit_username, 'm' => $method, )); } if ($call->shouldAllowUnguardedWrites()) { $allow_unguarded_writes = true; } if ($auth_error === null) { if ($allow_unguarded_writes) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); } try { $result = $call->execute(); $error_code = null; $error_info = null; } catch (ConduitException $ex) { $result = null; $error_code = $ex->getMessage(); if ($ex->getErrorDescription()) { $error_info = $ex->getErrorDescription(); } else { $error_info = $call->getErrorDescription($error_code); } } if ($allow_unguarded_writes) { unset($unguarded); } } else { list($error_code, $error_info) = $auth_error; } } catch (Exception $ex) { if (!($ex instanceof ConduitMethodNotFoundException)) { phlog($ex); } $result = null; $error_code = ($ex instanceof ConduitException ? 'ERR-CONDUIT-CALL' : 'ERR-CONDUIT-CORE'); $error_info = $ex->getMessage(); } $time_end = microtime(true); $log ->setCallerPHID( isset($conduit_user) ? $conduit_user->getPHID() : null) ->setError((string)$error_code) ->setDuration(1000000 * ($time_end - $time_start)); $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $log->save(); unset($unguarded); $response = id(new ConduitAPIResponse()) ->setResult($result) ->setErrorCode($error_code) ->setErrorInfo($error_info); switch ($request->getStr('output')) { case 'human': return $this->buildHumanReadableResponse( $method, $api_request, $response->toDictionary(), $method_implementation); case 'json': default: return id(new AphrontJSONResponse()) ->setAddJSONShield(false) ->setContent($response->toDictionary()); } } /** * Authenticate the client making the request to a Phabricator user account. * * @param ConduitAPIRequest Request being executed. * @param dict Request metadata. * @return null|pair Null to indicate successful authentication, or * an error code and error message pair. */ private function authenticateUser( ConduitAPIRequest $api_request, array $metadata, $method) { $request = $this->getRequest(); if ($request->getUser()->getPHID()) { $request->validateCSRF(); return $this->validateAuthenticatedUser( $api_request, $request->getUser()); } $auth_type = idx($metadata, 'auth.type'); if ($auth_type === ConduitClient::AUTH_ASYMMETRIC) { $host = idx($metadata, 'auth.host'); if (!$host) { return array( 'ERR-INVALID-AUTH', pht( 'Request is missing required "%s" parameter.', 'auth.host'), ); } // TODO: Validate that we are the host! $raw_key = idx($metadata, 'auth.key'); $public_key = PhabricatorAuthSSHPublicKey::newFromRawKey($raw_key); $ssl_public_key = $public_key->toPKCS8(); // First, verify the signature. try { $protocol_data = $metadata; ConduitClient::verifySignature( $method, $api_request->getAllParameters(), $protocol_data, $ssl_public_key); } catch (Exception $ex) { return array( 'ERR-INVALID-AUTH', pht( 'Signature verification failure. %s', $ex->getMessage()), ); } // If the signature is valid, find the user or device which is // associated with this public key. $stored_key = id(new PhabricatorAuthSSHKeyQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withKeys(array($public_key)) ->executeOne(); if (!$stored_key) { return array( 'ERR-INVALID-AUTH', pht('No user or device is associated with that public key.'), ); } $object = $stored_key->getObject(); if ($object instanceof PhabricatorUser) { $user = $object; } else { if (!$stored_key->getIsTrusted()) { return array( 'ERR-INVALID-AUTH', pht( 'The key which signed this request is not trusted. Only '. 'trusted keys can be used to sign API calls.'), ); } if (!PhabricatorEnv::isClusterRemoteAddress()) { return array( 'ERR-INVALID-AUTH', pht( 'This request originates from outside of the Phabricator '. 'cluster address range. Requests signed with trusted '. 'device keys must originate from within the cluster.'), ); } $user = PhabricatorUser::getOmnipotentUser(); // Flag this as an intracluster request. $api_request->setIsClusterRequest(true); } return $this->validateAuthenticatedUser( $api_request, $user); } else if ($auth_type === null) { // No specified authentication type, continue with other authentication // methods below. } else { return array( 'ERR-INVALID-AUTH', pht( 'Provided "%s" ("%s") is not recognized.', 'auth.type', $auth_type), ); } $token_string = idx($metadata, 'token'); if (strlen($token_string)) { if (strlen($token_string) != 32) { return array( 'ERR-INVALID-AUTH', pht( 'API token "%s" has the wrong length. API tokens should be '. '32 characters long.', $token_string), ); } $type = head(explode('-', $token_string)); $valid_types = PhabricatorConduitToken::getAllTokenTypes(); $valid_types = array_fuse($valid_types); if (empty($valid_types[$type])) { return array( 'ERR-INVALID-AUTH', pht( 'API token "%s" has the wrong format. API tokens should be '. '32 characters long and begin with one of these prefixes: %s.', $token_string, implode(', ', $valid_types)), ); } $token = id(new PhabricatorConduitTokenQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withTokens(array($token_string)) ->withExpired(false) ->executeOne(); if (!$token) { $token = id(new PhabricatorConduitTokenQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withTokens(array($token_string)) ->withExpired(true) ->executeOne(); if ($token) { return array( 'ERR-INVALID-AUTH', pht( 'API token "%s" was previously valid, but has expired.', $token_string), ); } else { return array( 'ERR-INVALID-AUTH', pht( 'API token "%s" is not valid.', $token_string), ); } } // If this is a "cli-" token, it expires shortly after it is generated // by default. Once it is actually used, we extend its lifetime and make // it permanent. This allows stray tokens to get cleaned up automatically // if they aren't being used. if ($token->getTokenType() == PhabricatorConduitToken::TYPE_COMMANDLINE) { if ($token->getExpires()) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $token->setExpires(null); $token->save(); unset($unguarded); } } // If this is a "clr-" token, Phabricator must be configured in cluster // mode and the remote address must be a cluster node. if ($token->getTokenType() == PhabricatorConduitToken::TYPE_CLUSTER) { if (!PhabricatorEnv::isClusterRemoteAddress()) { return array( 'ERR-INVALID-AUTH', pht( 'This request originates from outside of the Phabricator '. 'cluster address range. Requests signed with cluster API '. 'tokens must originate from within the cluster.'), ); } // Flag this as an intracluster request. $api_request->setIsClusterRequest(true); } $user = $token->getObject(); if (!($user instanceof PhabricatorUser)) { return array( 'ERR-INVALID-AUTH', pht('API token is not associated with a valid user.'), ); } return $this->validateAuthenticatedUser( $api_request, $user); } $access_token = idx($metadata, 'access_token'); if ($access_token) { $token = id(new PhabricatorOAuthServerAccessToken()) ->loadOneWhere('token = %s', $access_token); if (!$token) { return array( 'ERR-INVALID-AUTH', pht('Access token does not exist.'), ); } $oauth_server = new PhabricatorOAuthServer(); $authorization = $oauth_server->authorizeToken($token); if (!$authorization) { return array( 'ERR-INVALID-AUTH', pht('Access token is invalid or expired.'), ); } $user = id(new PhabricatorPeopleQuery()) ->setViewer(PhabricatorUser::getOmnipotentUser()) ->withPHIDs(array($token->getUserPHID())) ->executeOne(); if (!$user) { return array( 'ERR-INVALID-AUTH', pht('Access token is for invalid user.'), ); } $ok = $this->authorizeOAuthMethodAccess($authorization, $method); if (!$ok) { return array( 'ERR-OAUTH-ACCESS', pht('You do not have authorization to call this method.'), ); } $api_request->setOAuthToken($token); return $this->validateAuthenticatedUser( $api_request, $user); } // Handle sessionless auth. // TODO: This is super messy. // TODO: Remove this in favor of token-based auth. if (isset($metadata['authUser'])) { $user = id(new PhabricatorUser())->loadOneWhere( 'userName = %s', $metadata['authUser']); if (!$user) { return array( 'ERR-INVALID-AUTH', pht('Authentication is invalid.'), ); } $token = idx($metadata, 'authToken'); $signature = idx($metadata, 'authSignature'); $certificate = $user->getConduitCertificate(); $hash = sha1($token.$certificate); if (!phutil_hashes_are_identical($hash, $signature)) { return array( 'ERR-INVALID-AUTH', pht('Authentication is invalid.'), ); } return $this->validateAuthenticatedUser( $api_request, $user); } // Handle session-based auth. // TODO: Remove this in favor of token-based auth. $session_key = idx($metadata, 'sessionKey'); if (!$session_key) { return array( 'ERR-INVALID-SESSION', pht('Session key is not present.'), ); } $user = id(new PhabricatorAuthSessionEngine()) ->loadUserForSession(PhabricatorAuthSession::TYPE_CONDUIT, $session_key); if (!$user) { return array( 'ERR-INVALID-SESSION', pht('Session key is invalid.'), ); } return $this->validateAuthenticatedUser( $api_request, $user); } private function validateAuthenticatedUser( ConduitAPIRequest $request, PhabricatorUser $user) { if (!$user->canEstablishAPISessions()) { return array( 'ERR-INVALID-AUTH', pht('User account is not permitted to use the API.'), ); } $request->setUser($user); return null; } private function buildHumanReadableResponse( $method, ConduitAPIRequest $request = null, $result = null, ConduitAPIMethod $method_implementation = null) { $param_rows = array(); $param_rows[] = array('Method', $this->renderAPIValue($method)); if ($request) { foreach ($request->getAllParameters() as $key => $value) { $param_rows[] = array( $key, $this->renderAPIValue($value), ); } } $param_table = new AphrontTableView($param_rows); $param_table->setColumnClasses( array( 'header', 'wide', )); $result_rows = array(); foreach ($result as $key => $value) { $result_rows[] = array( $key, $this->renderAPIValue($value), ); } $result_table = new AphrontTableView($result_rows); $result_table->setColumnClasses( array( 'header', 'wide', )); - $param_panel = new PHUIObjectBoxView(); - $param_panel->setHeaderText(pht('Method Parameters')); - $param_panel->setTable($param_table); + $param_panel = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Method Parameters')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($param_table); - $result_panel = new PHUIObjectBoxView(); - $result_panel->setHeaderText(pht('Method Result')); - $result_panel->setTable($result_table); + $result_panel = id(new PHUIObjectBoxView()) + ->setHeaderText(pht('Method Result')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) + ->setTable($result_table); $method_uri = $this->getApplicationURI('method/'.$method.'/'); $crumbs = $this->buildApplicationCrumbs() ->addTextCrumb($method, $method_uri) - ->addTextCrumb(pht('Call')); + ->addTextCrumb(pht('Call')) + ->setBorder(true); $example_panel = null; if ($request && $method_implementation) { $params = $request->getAllParameters(); $example_panel = $this->renderExampleBox( $method_implementation, $params); } - return $this->buildApplicationPage( - array( - $crumbs, + $title = pht('Method Call Result'); + $header = id(new PHUIHeaderView()) + ->setHeader($title) + ->setHeaderIcon('fa-exchange'); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter(array( $param_panel, $result_panel, $example_panel, - ), - array( - 'title' => pht('Method Call Result'), )); + + $title = pht('Method Call Result'); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($view); + } private function renderAPIValue($value) { $json = new PhutilJSON(); if (is_array($value)) { $value = $json->encodeFormatted($value); } $value = phutil_tag( 'pre', array('style' => 'white-space: pre-wrap;'), $value); return $value; } private function decodeConduitParams( AphrontRequest $request, $method) { // Look for parameters from the Conduit API Console, which are encoded // as HTTP POST parameters in an array, e.g.: // // params[name]=value¶ms[name2]=value2 // // The fields are individually JSON encoded, since we require users to // enter JSON so that we avoid type ambiguity. $params = $request->getArr('params', null); if ($params !== null) { foreach ($params as $key => $value) { if ($value == '') { // Interpret empty string null (e.g., the user didn't type anything // into the box). $value = 'null'; } $decoded_value = json_decode($value, true); if ($decoded_value === null && strtolower($value) != 'null') { // When json_decode() fails, it returns null. This almost certainly // indicates that a user was using the web UI and didn't put quotes // around a string value. We can either do what we think they meant // (treat it as a string) or fail. For now, err on the side of // caution and fail. In the future, if we make the Conduit API // actually do type checking, it might be reasonable to treat it as // a string if the parameter type is string. throw new Exception( pht( "The value for parameter '%s' is not valid JSON. All ". "parameters must be encoded as JSON values, including strings ". "(which means you need to surround them in double quotes). ". "Check your syntax. Value was: %s.", $key, $value)); } $params[$key] = $decoded_value; } $metadata = idx($params, '__conduit__', array()); unset($params['__conduit__']); return array($metadata, $params); } // Otherwise, look for a single parameter called 'params' which has the // entire param dictionary JSON encoded. $params_json = $request->getStr('params'); if (strlen($params_json)) { $params = null; try { $params = phutil_json_decode($params_json); } catch (PhutilJSONParserException $ex) { throw new PhutilProxyException( pht( "Invalid parameter information was passed to method '%s'.", $method), $ex); } $metadata = idx($params, '__conduit__', array()); unset($params['__conduit__']); return array($metadata, $params); } // If we do not have `params`, assume this is a simple HTTP request with // HTTP key-value pairs. $params = array(); $metadata = array(); foreach ($request->getPassthroughRequestData() as $key => $value) { $meta_key = ConduitAPIMethod::getParameterMetadataKey($key); if ($meta_key !== null) { $metadata[$meta_key] = $value; } else { $params[$key] = $value; } } return array($metadata, $params); } private function authorizeOAuthMethodAccess( PhabricatorOAuthClientAuthorization $authorization, $method_name) { $method = ConduitAPIMethod::getConduitMethod($method_name); if (!$method) { return false; } $required_scope = $method->getRequiredScope(); switch ($required_scope) { case ConduitAPIMethod::SCOPE_ALWAYS: return true; case ConduitAPIMethod::SCOPE_NEVER: return false; } $authorization_scope = $authorization->getScope(); if (!empty($authorization_scope[$required_scope])) { return true; } return false; } } diff --git a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php index f11415795e..5ee76b44f7 100644 --- a/src/applications/conduit/controller/PhabricatorConduitConsoleController.php +++ b/src/applications/conduit/controller/PhabricatorConduitConsoleController.php @@ -1,184 +1,188 @@ getViewer(); $method_name = $request->getURIData('method'); $method = id(new PhabricatorConduitMethodQuery()) ->setViewer($viewer) ->withMethods(array($method_name)) ->executeOne(); if (!$method) { return new Aphront404Response(); } $method->setViewer($viewer); $call_uri = '/api/'.$method->getAPIMethodName(); $status = $method->getMethodStatus(); $reason = $method->getMethodStatusDescription(); $errors = array(); switch ($status) { case ConduitAPIMethod::METHOD_STATUS_DEPRECATED: $reason = nonempty($reason, pht('This method is deprecated.')); $errors[] = pht('Deprecated Method: %s', $reason); break; case ConduitAPIMethod::METHOD_STATUS_UNSTABLE: $reason = nonempty( $reason, pht( 'This method is new and unstable. Its interface is subject '. 'to change.')); $errors[] = pht('Unstable Method: %s', $reason); break; } $form = id(new AphrontFormView()) ->setAction($call_uri) ->setUser($request->getUser()) ->appendRemarkupInstructions( pht( 'Enter parameters using **JSON**. For instance, to enter a '. 'list, type: `%s`', '["apple", "banana", "cherry"]')); $params = $method->getParamTypes(); foreach ($params as $param => $desc) { $form->appendChild( id(new AphrontFormTextControl()) ->setLabel($param) ->setName("params[{$param}]") ->setCaption($desc)); } $must_login = !$viewer->isLoggedIn() && $method->shouldRequireAuthentication(); if ($must_login) { $errors[] = pht( 'Login Required: This method requires authentication. You must '. 'log in before you can make calls to it.'); } else { $form ->appendChild( id(new AphrontFormSelectControl()) ->setLabel(pht('Output Format')) ->setName('output') ->setOptions( array( 'human' => pht('Human Readable'), 'json' => pht('JSON'), ))) ->appendChild( id(new AphrontFormSubmitControl()) ->addCancelButton($this->getApplicationURI()) ->setValue(pht('Call Method'))); } $header = id(new PHUIHeaderView()) ->setUser($viewer) - ->setHeader($method->getAPIMethodName()); + ->setHeader($method->getAPIMethodName()) + ->setHeaderIcon('fa-tty'); $form_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Call Method')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); - $content = array(); - $properties = $this->buildMethodProperties($method); $info_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('API Method: %s', $method->getAPIMethodName())) ->setFormErrors($errors) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($properties); - $content[] = $info_box; - $content[] = $method->getMethodDocumentation(); - $content[] = $form_box; - $content[] = $this->renderExampleBox($method, null); - $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb($method->getAPIMethodName()); - - return $this->buildApplicationPage( - array( - $crumbs, - $content, - ), - array( - 'title' => $method->getAPIMethodName(), + $crumbs->setBorder(true); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter(array( + $info_box, + $method->getMethodDocumentation(), + $form_box, + $this->renderExampleBox($method, null), )); + + $title = $method->getAPIMethodName(); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($view); } private function buildMethodProperties(ConduitAPIMethod $method) { $viewer = $this->getViewer(); $view = id(new PHUIPropertyListView()); $view->addProperty( pht('Returns'), $method->getReturnType()); $error_types = $method->getErrorTypes(); $error_types['ERR-CONDUIT-CORE'] = pht('See error message for details.'); $error_description = array(); foreach ($error_types as $error => $meaning) { $error_description[] = hsprintf( '
  • %s: %s
  • ', $error, $meaning); } $error_description = phutil_tag('ul', array(), $error_description); $view->addProperty( pht('Errors'), $error_description); $scope = $method->getRequiredScope(); switch ($scope) { case ConduitAPIMethod::SCOPE_ALWAYS: $oauth_icon = 'fa-globe green'; $oauth_description = pht( 'OAuth clients may always call this method.'); break; case ConduitAPIMethod::SCOPE_NEVER: $oauth_icon = 'fa-ban red'; $oauth_description = pht( 'OAuth clients may never call this method.'); break; default: $oauth_icon = 'fa-unlock-alt blue'; $oauth_description = pht( 'OAuth clients may call this method after requesting access to '. 'the "%s" scope.', $scope); break; } $view->addProperty( pht('OAuth Scope'), array( id(new PHUIIconView())->setIcon($oauth_icon), ' ', $oauth_description, )); $view->addSectionHeader( pht('Description'), PHUIPropertyListView::ICON_SUMMARY); $view->addTextContent( new PHUIRemarkupView($viewer, $method->getMethodDescription())); return $view; } } diff --git a/src/applications/conduit/controller/PhabricatorConduitController.php b/src/applications/conduit/controller/PhabricatorConduitController.php index 4fa11dbfad..000d01f888 100644 --- a/src/applications/conduit/controller/PhabricatorConduitController.php +++ b/src/applications/conduit/controller/PhabricatorConduitController.php @@ -1,273 +1,274 @@ getRequest()->getUser(); $nav = new AphrontSideNavFilterView(); $nav->setBaseURI(new PhutilURI($this->getApplicationURI())); id(new PhabricatorConduitSearchEngine()) ->setViewer($viewer) ->addNavigationItems($nav->getMenu()); $nav->addLabel('Logs'); $nav->addFilter('log', pht('Call Logs')); $nav->selectFilter(null); return $nav; } public function buildApplicationMenu() { return $this->buildSideNavView()->getMenu(); } protected function renderExampleBox(ConduitAPIMethod $method, $params) { $arc_example = id(new PHUIPropertyListView()) ->addRawContent($this->renderExample($method, 'arc', $params)); $curl_example = id(new PHUIPropertyListView()) ->addRawContent($this->renderExample($method, 'curl', $params)); $php_example = id(new PHUIPropertyListView()) ->addRawContent($this->renderExample($method, 'php', $params)); $panel_link = phutil_tag( 'a', array( 'href' => '/settings/panel/apitokens/', ), pht('Conduit API Tokens')); $panel_link = phutil_tag('strong', array(), $panel_link); $messages = array( pht( 'Use the %s panel in Settings to generate or manage API tokens.', $panel_link), ); $info_view = id(new PHUIInfoView()) ->setErrors($messages) ->setSeverity(PHUIInfoView::SEVERITY_NOTICE); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Examples')) ->setInfoView($info_view) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->addPropertyList($arc_example, pht('arc call-conduit')) ->addPropertyList($curl_example, pht('cURL')) ->addPropertyList($php_example, pht('PHP')); } private function renderExample( ConduitAPIMethod $method, $kind, $params) { switch ($kind) { case 'arc': $example = $this->buildArcanistExample($method, $params); break; case 'php': $example = $this->buildPHPExample($method, $params); break; case 'curl': $example = $this->buildCURLExample($method, $params); break; default: throw new Exception(pht('Conduit client "%s" is not known.', $kind)); } return $example; } private function buildArcanistExample( ConduitAPIMethod $method, $params) { $parts = array(); $parts[] = '$ echo '; if ($params === null) { $parts[] = phutil_tag('strong', array(), ''); } else { $params = $this->simplifyParams($params); $params = id(new PhutilJSON())->encodeFormatted($params); $params = trim($params); $params = csprintf('%s', $params); $parts[] = phutil_tag('strong', array('class' => 'real'), $params); } $parts[] = ' | '; $parts[] = 'arc call-conduit '; $parts[] = '--conduit-uri '; $parts[] = phutil_tag( 'strong', array('class' => 'real'), PhabricatorEnv::getURI('/')); $parts[] = ' '; $parts[] = '--conduit-token '; $parts[] = phutil_tag('strong', array(), ''); $parts[] = ' '; $parts[] = $method->getAPIMethodName(); return $this->renderExampleCode($parts); } private function buildPHPExample( ConduitAPIMethod $method, $params) { $parts = array(); $libphutil_path = 'path/to/libphutil/src/__phutil_library_init__.php'; $parts[] = '')); $parts[] = "\";\n"; $parts[] = '$api_parameters = '; if ($params === null) { $parts[] = 'array('; $parts[] = phutil_tag('strong', array(), pht('')); $parts[] = ');'; } else { $params = $this->simplifyParams($params); $params = phutil_var_export($params, true); $parts[] = phutil_tag('strong', array('class' => 'real'), $params); $parts[] = ';'; } $parts[] = "\n\n"; $parts[] = '$client = new ConduitClient('; $parts[] = phutil_tag( 'strong', array('class' => 'real'), phutil_var_export(PhabricatorEnv::getURI('/'), true)); $parts[] = ");\n"; $parts[] = '$client->setConduitToken($api_token);'; $parts[] = "\n\n"; $parts[] = '$result = $client->callMethodSynchronous('; $parts[] = phutil_tag( 'strong', array('class' => 'real'), phutil_var_export($method->getAPIMethodName(), true)); $parts[] = ', '; $parts[] = '$api_parameters'; $parts[] = ");\n"; $parts[] = 'print_r($result);'; return $this->renderExampleCode($parts); } private function buildCURLExample( ConduitAPIMethod $method, $params) { $call_uri = '/api/'.$method->getAPIMethodName(); $parts = array(); $linebreak = array('\\', phutil_tag('br'), ' '); $parts[] = '$ curl '; $parts[] = phutil_tag( 'strong', array('class' => 'real'), csprintf('%R', PhabricatorEnv::getURI($call_uri))); $parts[] = ' '; $parts[] = $linebreak; $parts[] = '-d api.token='; $parts[] = phutil_tag('strong', array(), 'api-token'); $parts[] = ' '; $parts[] = $linebreak; if ($params === null) { $parts[] = '-d '; $parts[] = phutil_tag('strong', array(), 'param'); $parts[] = '='; $parts[] = phutil_tag('strong', array(), 'value'); $parts[] = ' '; $parts[] = $linebreak; $parts[] = phutil_tag('strong', array(), '...'); } else { $lines = array(); $params = $this->simplifyParams($params); foreach ($params as $key => $value) { $pieces = $this->getQueryStringParts(null, $key, $value); foreach ($pieces as $piece) { $lines[] = array( '-d ', phutil_tag('strong', array('class' => 'real'), $piece), ); } } $parts[] = phutil_implode_html(array(' ', $linebreak), $lines); } return $this->renderExampleCode($parts); } private function renderExampleCode($example) { require_celerity_resource('conduit-api-css'); return phutil_tag( 'div', array( 'class' => 'PhabricatorMonospaced conduit-api-example-code', ), $example); } private function simplifyParams(array $params) { foreach ($params as $key => $value) { if ($value === null) { unset($params[$key]); } } return $params; } private function getQueryStringParts($prefix, $key, $value) { if ($prefix === null) { $head = phutil_escape_uri($key); } else { $head = $prefix.'['.phutil_escape_uri($key).']'; } if (!is_array($value)) { return array( $head.'='.phutil_escape_uri($value), ); } $results = array(); foreach ($value as $subkey => $subvalue) { $subparts = $this->getQueryStringParts($head, $subkey, $subvalue); foreach ($subparts as $subpart) { $results[] = $subpart; } } return $results; } } diff --git a/src/applications/conduit/controller/PhabricatorConduitTokenController.php b/src/applications/conduit/controller/PhabricatorConduitTokenController.php index b5501a2805..fe6d676b68 100644 --- a/src/applications/conduit/controller/PhabricatorConduitTokenController.php +++ b/src/applications/conduit/controller/PhabricatorConduitTokenController.php @@ -1,73 +1,80 @@ getViewer(); id(new PhabricatorAuthSessionEngine())->requireHighSecuritySession( $viewer, $this->getRequest(), '/'); // Ideally we'd like to verify this, but it's fine to leave it unguarded // for now and verifying it would need some Ajax junk or for the user to // click a button or similar. $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $old_token = id(new PhabricatorConduitCertificateToken()) ->loadOneWhere( 'userPHID = %s', $viewer->getPHID()); if ($old_token) { $old_token->delete(); } $token = id(new PhabricatorConduitCertificateToken()) ->setUserPHID($viewer->getPHID()) ->setToken(Filesystem::readRandomCharacters(40)) ->save(); unset($unguarded); $pre_instructions = pht( 'Copy and paste this token into the prompt given to you by '. '`arc install-certificate`'); $post_instructions = pht( 'After you copy and paste this token, `arc` will complete '. 'the certificate install process for you.'); Javelin::initBehavior('select-on-click'); $form = id(new AphrontFormView()) ->setUser($viewer) ->appendRemarkupInstructions($pre_instructions) ->appendChild( id(new AphrontFormTextAreaControl()) ->setLabel(pht('Token')) ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT) ->setReadonly(true) ->setSigil('select-on-click') ->setValue($token->getToken())) ->appendRemarkupInstructions($post_instructions); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addTextCrumb(pht('Install Certificate')); + $crumbs->setBorder(true); $object_box = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Certificate Token')) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->setForm($form); - return $this->buildApplicationPage( - array( - $crumbs, - $object_box, - ), - array( - 'title' => pht('Certificate Install Token'), - )); + $title = pht('Certificate Install Token'); + + $header = id(new PHUIHeaderView()) + ->setHeader($title); + + $view = id(new PHUITwoColumnView()) + ->setHeader($header) + ->setFooter($object_box); + + return $this->newPage() + ->setTitle($title) + ->setCrumbs($crumbs) + ->appendChild($view); } } diff --git a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php index f411c208bf..b5cd52471b 100644 --- a/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php +++ b/src/applications/search/engine/PhabricatorSearchEngineAPIMethod.php @@ -1,595 +1,601 @@ getCustomQueryMaps($query); // Make sure we emit empty maps as objects, not lists. foreach ($maps as $key => $map) { if (!$map) { $maps[$key] = (object)$map; } } if (!$maps) { $maps = (object)$maps; } return $maps; } protected function getCustomQueryMaps($query) { return array(); } public function getApplication() { $engine = $this->newSearchEngine(); $class = $engine->getApplicationClassName(); return PhabricatorApplication::getByClass($class); } public function getMethodStatus() { return self::METHOD_STATUS_UNSTABLE; } public function getMethodStatusDescription() { return pht('ApplicationSearch methods are highly unstable.'); } final protected function defineParamTypes() { return array( 'queryKey' => 'optional string', 'constraints' => 'optional map', 'attachments' => 'optional map', 'order' => 'optional order', ) + $this->getPagerParamTypes(); } final protected function defineReturnType() { return 'map'; } final protected function execute(ConduitAPIRequest $request) { $engine = $this->newSearchEngine() ->setViewer($request->getUser()); return $engine->buildConduitResponse($request, $this); } final public function getMethodDescription() { return pht( 'This is a standard **ApplicationSearch** method which will let you '. 'list, query, or search for objects. For documentation on these '. 'endpoints, see **[[ %s | Conduit API: Using Search Endpoints ]]**.', PhabricatorEnv::getDoclink('Conduit API: Using Edit Endpoints')); } final public function getMethodDocumentation() { $viewer = $this->getViewer(); $engine = $this->newSearchEngine() ->setViewer($viewer); $query = $engine->newQuery(); $out = array(); $out[] = $this->buildQueriesBox($engine); $out[] = $this->buildConstraintsBox($engine); $out[] = $this->buildOrderBox($engine, $query); $out[] = $this->buildFieldsBox($engine); $out[] = $this->buildAttachmentsBox($engine); $out[] = $this->buildPagingBox($engine); return $out; } private function buildQueriesBox( PhabricatorApplicationSearchEngine $engine) { $viewer = $this->getViewer(); $info = pht(<<loadAllNamedQueries(); $rows = array(); foreach ($named_queries as $named_query) { $builtin = $named_query->getIsBuiltin() ? pht('Builtin') : pht('Custom'); $rows[] = array( $named_query->getQueryKey(), $named_query->getQueryName(), $builtin, ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('Query Key'), pht('Name'), pht('Builtin'), )) ->setColumnClasses( array( 'prewrap', 'pri wide', null, )); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Builtin and Saved Queries')) ->setCollapsed(true) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($this->buildRemarkup($info)) ->appendChild($table); } private function buildConstraintsBox( PhabricatorApplicationSearchEngine $engine) { $info = pht(<<getSearchFieldsForConduit(); // As a convenience, put these fields at the very top, even if the engine // specifies and alternate display order for the web UI. These fields are // very important in the API and nearly useless in the web UI. $fields = array_select_keys( $fields, array('ids', 'phids')) + $fields; $rows = array(); foreach ($fields as $field) { $key = $field->getConduitKey(); $label = $field->getLabel(); $type_object = $field->getConduitParameterType(); if ($type_object) { $type = $type_object->getTypeName(); $description = $field->getDescription(); } else { $type = null; $description = phutil_tag('em', array(), pht('Not supported.')); } $rows[] = array( $key, $label, $type, $description, ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('Key'), pht('Label'), pht('Type'), pht('Description'), )) ->setColumnClasses( array( 'prewrap', 'pri', 'prewrap', 'wide', )); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Custom Query Constraints')) ->setCollapsed(true) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($this->buildRemarkup($info)) ->appendChild($table); } private function buildOrderBox( PhabricatorApplicationSearchEngine $engine, $query) { $orders_info = pht(<<getBuiltinOrders(); $rows = array(); foreach ($orders as $key => $order) { $rows[] = array( $key, $order['name'], implode(', ', $order['vector']), ); } $orders_table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('Key'), pht('Description'), pht('Columns'), )) ->setColumnClasses( array( 'pri', '', 'wide', )); $columns_info = pht(<<getOrderableColumns(); $rows = array(); foreach ($columns as $key => $column) { $rows[] = array( $key, idx($column, 'unique') ? pht('Yes') : pht('No'), ); } $columns_table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('Key'), pht('Unique'), )) ->setColumnClasses( array( 'pri', 'wide', )); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Result Ordering')) ->setCollapsed(true) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($this->buildRemarkup($orders_info)) ->appendChild($orders_table) ->appendChild($this->buildRemarkup($columns_info)) ->appendChild($columns_table); } private function buildFieldsBox( PhabricatorApplicationSearchEngine $engine) { $info = pht(<<getAllConduitFieldSpecifications(); $rows = array(); foreach ($specs as $key => $spec) { $type = $spec->getType(); $description = $spec->getDescription(); $rows[] = array( $key, $type, $description, ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('Key'), pht('Type'), pht('Description'), )) ->setColumnClasses( array( 'pri', 'mono', 'wide', )); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Object Fields')) ->setCollapsed(true) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($this->buildRemarkup($info)) ->appendChild($table); } private function buildAttachmentsBox( PhabricatorApplicationSearchEngine $engine) { $info = pht(<<getConduitSearchAttachments(); $rows = array(); foreach ($attachments as $key => $attachment) { $rows[] = array( $key, $attachment->getAttachmentName(), $attachment->getAttachmentDescription(), ); } $table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('Key'), pht('Name'), pht('Description'), )) ->setColumnClasses( array( 'prewrap', 'pri', 'wide', )); return id(new PHUIObjectBoxView()) ->setHeaderText(pht('Attachments')) ->setCollapsed(true) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($this->buildRemarkup($info)) ->appendChild($table); } private function buildPagingBox( PhabricatorApplicationSearchEngine $engine) { $info = pht(<<setHeaderText(pht('Paging and Limits')) ->setCollapsed(true) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($this->buildRemarkup($info)); } private function buildRemarkup($remarkup) { $viewer = $this->getViewer(); $view = new PHUIRemarkupView($viewer, $remarkup); return id(new PHUIBoxView()) ->appendChild($view) ->addPadding(PHUI::PADDING_LARGE); } } diff --git a/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php b/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php index 3cf04aeca8..b6f95ecd1b 100644 --- a/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php +++ b/src/applications/transactions/editengine/PhabricatorEditEngineAPIMethod.php @@ -1,161 +1,163 @@ newEditEngine(); $class = $engine->getEngineApplicationClass(); return PhabricatorApplication::getByClass($class); } public function getMethodStatus() { return self::METHOD_STATUS_UNSTABLE; } public function getMethodStatusDescription() { return pht('ApplicationEditor methods are highly unstable.'); } final protected function defineParamTypes() { return array( 'transactions' => 'list>', 'objectIdentifier' => 'optional id|phid|string', ); } final protected function defineReturnType() { return 'map'; } final protected function execute(ConduitAPIRequest $request) { $engine = $this->newEditEngine() ->setViewer($request->getUser()); return $engine->buildConduitResponse($request); } final public function getMethodDescription() { return pht( 'This is a standard **ApplicationEditor** method which allows you to '. 'create and modify objects by applying transactions. For documentation '. 'on these endpoints, see '. '**[[ %s | Conduit API: Using Edit Endpoints ]]**.', PhabricatorEnv::getDoclink('Conduit API: Using Edit Endpoints')); } final public function getMethodDocumentation() { $viewer = $this->getViewer(); $engine = $this->newEditEngine() ->setViewer($viewer); $types = $engine->getConduitEditTypes(); $out = array(); $out[] = $this->buildEditTypesBoxes($engine, $types); return $out; } private function buildEditTypesBoxes( PhabricatorEditEngine $engine, array $types) { $boxes = array(); $summary_info = pht( 'This endpoint supports these types of transactions. See below for '. 'detailed information about each transaction type.'); $rows = array(); foreach ($types as $type) { $rows[] = array( $type->getEditType(), $type->getConduitDescription(), ); } $summary_table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('Key'), pht('Description'), )) ->setColumnClasses( array( 'prewrap', 'wide', )); $boxes[] = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Transaction Types')) ->setCollapsed(true) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($this->buildRemarkup($summary_info)) ->appendChild($summary_table); foreach ($types as $type) { $section = array(); $section[] = $type->getConduitDescription(); $type_documentation = $type->getConduitDocumentation(); if (strlen($type_documentation)) { $section[] = $type_documentation; } $section = implode("\n\n", $section); $rows = array(); $rows[] = array( 'type', 'const', $type->getEditType(), ); $rows[] = array( 'value', $type->getConduitType(), $type->getConduitTypeDescription(), ); $type_table = id(new AphrontTableView($rows)) ->setHeaders( array( pht('Key'), pht('Type'), pht('Description'), )) ->setColumnClasses( array( 'prewrap', 'prewrap', 'wide', )); $boxes[] = id(new PHUIObjectBoxView()) ->setHeaderText(pht('Transaction Type: %s', $type->getEditType())) ->setCollapsed(true) + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) ->appendChild($this->buildRemarkup($section)) ->appendChild($type_table); } return $boxes; } private function buildRemarkup($remarkup) { $viewer = $this->getViewer(); $view = new PHUIRemarkupView($viewer, $remarkup); return id(new PHUIBoxView()) ->appendChild($view) ->addPadding(PHUI::PADDING_LARGE); } }