OAuthServer polish and random sauce
Summary: This diff makes the OAuthServer more compliant with the spec by - making it return well-formatted error codes with error types from the spec. - making it respect the "state" variable, which is a transparent variable the client passes and the server passes back - making it be super, duper compliant with respect to redirect uris -- if specified in authorization step, check if its valid relative to the client registered URI and if so save it -- if specified in authorization step, check if its been specified in the access step and error if it doesn't match or doesn't exist -- note we don't make any use of it in the access step which seems strange but hey, that's what the spec says! This diff makes the OAuthServer suck less by - making the "cancel" button do something in the user authorization flow - making the client list view and client edit view be a bit more usable around client secrets - fixing a few bugs I managed to introduce along the way Test Plan: - create a test phabricator client, updated my conf, and then linked and unlinked phabricator to itself - wrote some tests for PhabricatorOAuthServer -- they pass! -- these validate the various validate URI checks - tried a few important authorization calls -- http://phabricator.dev/oauthserver/auth/?client_id=X&state=test&redirect_uri=http://www.evil.com --- verified error'd from mismatching redirect uri's --- verified state parameter in response --- verified did not redirect to client redirect uri -- http://phabricator.dev/oauthserver/auth/?client_id=X w/ existing authorization --- got redirected to proper client url with error that response_type not specified -- http://phabricator.dev/oauthserver/auth/?client_id=X&response_type=code w/ existing authorization --- got redirected to proper client url with pertinent code! - tried a few important access calls -- verified appropriate errors if missing any required parameters -- verified good access code with appropriate other variables resulted in an access token - verified that if redirect_uri set correctly in authorization required for access and errors if differs at all / only succeeds if exactly the same Reviewers: epriestley Reviewed By: epriestley CC: aran, epriestley, ajtrichards Maniphest Tasks: T889, T906, T897 Differential Revision: https://secure.phabricator.com/D1727
This commit is contained in:
@@ -27,78 +27,135 @@ extends PhabricatorAuthController {
|
||||
}
|
||||
|
||||
public function processRequest() {
|
||||
$request = $this->getRequest();
|
||||
$current_user = $request->getUser();
|
||||
$server = new PhabricatorOAuthServer($current_user);
|
||||
$client_phid = $request->getStr('client_id');
|
||||
$scope = $request->getStr('scope');
|
||||
$redirect_uri = $request->getStr('redirect_uri');
|
||||
$response = new PhabricatorOAuthResponse();
|
||||
$errors = array();
|
||||
$request = $this->getRequest();
|
||||
$current_user = $request->getUser();
|
||||
$server = new PhabricatorOAuthServer();
|
||||
$client_phid = $request->getStr('client_id');
|
||||
$scope = $request->getStr('scope');
|
||||
$redirect_uri = $request->getStr('redirect_uri');
|
||||
$state = $request->getStr('state');
|
||||
$response_type = $request->getStr('response_type');
|
||||
$response = new PhabricatorOAuthResponse();
|
||||
|
||||
// state is an opaque value the client sent us for their own purposes
|
||||
// we just need to send it right back to them in the response!
|
||||
if ($state) {
|
||||
$response->setState($state);
|
||||
}
|
||||
if (!$client_phid) {
|
||||
return $response->setMalformed(
|
||||
$response->setError('invalid_request');
|
||||
$response->setErrorDescription(
|
||||
'Required parameter client_id not specified.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
$client = id(new PhabricatorOAuthServerClient())
|
||||
->loadOneWhere('phid = %s', $client_phid);
|
||||
if (!$client) {
|
||||
return $response->setNotFound(
|
||||
'Client with id '.$client_phid.' not found. '
|
||||
);
|
||||
}
|
||||
$server->setUser($current_user);
|
||||
|
||||
$server->setClient($client);
|
||||
if ($server->userHasAuthorizedClient()) {
|
||||
$return_auth_code = true;
|
||||
$unguarded_write = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||
} else if ($request->isFormPost()) {
|
||||
$scope = PhabricatorOAuthServerScope::getScopesFromRequest($request);
|
||||
$server->authorizeClient($scope);
|
||||
$return_auth_code = true;
|
||||
$unguarded_write = null;
|
||||
} else {
|
||||
$return_auth_code = false;
|
||||
$unguarded_write = null;
|
||||
}
|
||||
|
||||
if ($return_auth_code) {
|
||||
// step 1 -- generate authorization code
|
||||
$auth_code =
|
||||
$server->generateAuthorizationCode();
|
||||
|
||||
// step 2 -- error or return it
|
||||
if (!$auth_code) {
|
||||
$errors[] = 'Failed to generate an authorization code. '.
|
||||
'Try again.';
|
||||
} else {
|
||||
$client_uri = new PhutilURI($client->getRedirectURI());
|
||||
if (!$redirect_uri) {
|
||||
$uri = $client_uri;
|
||||
} else {
|
||||
$redirect_uri = new PhutilURI($redirect_uri);
|
||||
if ($redirect_uri->getDomain() !=
|
||||
$client_uri->getDomain()) {
|
||||
$uri = $client_uri;
|
||||
} else {
|
||||
$uri = $redirect_uri;
|
||||
}
|
||||
}
|
||||
|
||||
$uri->setQueryParam('code', $auth_code->getCode());
|
||||
return $response->setRedirect($uri);
|
||||
// one giant try / catch around all the exciting database stuff so we
|
||||
// can return a 'server_error' response if something goes wrong!
|
||||
try {
|
||||
$client = id(new PhabricatorOAuthServerClient())
|
||||
->loadOneWhere('phid = %s', $client_phid);
|
||||
if (!$client) {
|
||||
$response->setError('invalid_request');
|
||||
$response->setErrorDescription(
|
||||
'Client with id '.$client_phid.' not found.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
unset($unguarded_write);
|
||||
$server->setClient($client);
|
||||
if ($redirect_uri) {
|
||||
$client_uri = new PhutilURI($client->getRedirectURI());
|
||||
$redirect_uri = new PhutilURI($redirect_uri);
|
||||
if (!($server->validateSecondaryRedirectURI($redirect_uri,
|
||||
$client_uri))) {
|
||||
$response->setError('invalid_request');
|
||||
$response->setErrorDescription(
|
||||
'The specified redirect URI is invalid. The redirect URI '.
|
||||
'must be a fully-qualified domain with no fragments and '.
|
||||
'must have the same domain and at least the same query '.
|
||||
'parameters as the redirect URI the client registered.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
$uri = $redirect_uri;
|
||||
$access_token_uri = $uri;
|
||||
} else {
|
||||
$uri = new PhutilURI($client->getRedirectURI());
|
||||
$access_token_uri = null;
|
||||
}
|
||||
// we've now validated this request enough overall such that we
|
||||
// can safely redirect to the client with the response
|
||||
$response->setClientURI($uri);
|
||||
|
||||
$error_view = null;
|
||||
if ($errors) {
|
||||
$error_view = new AphrontErrorView();
|
||||
$error_view->setTitle('Authorization Code Errors');
|
||||
$error_view->setErrors($errors);
|
||||
if (empty($response_type)) {
|
||||
$response->setError('invalid_request');
|
||||
$response->setErrorDescription(
|
||||
'Required parameter response_type not specified.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
if ($response_type != 'code') {
|
||||
$response->setError('unsupported_response_type');
|
||||
$response->setErrorDescription(
|
||||
'The authorization server does not support obtaining an '.
|
||||
'authorization code using the specified response_type. '.
|
||||
'You must specify the response_type as "code".'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
if ($scope) {
|
||||
if (!PhabricatorOAuthServerScope::validateScopesList($scope)) {
|
||||
$response->setError('invalid_scope');
|
||||
$response->setErrorDescription(
|
||||
'The requested scope is invalid, unknown, or malformed.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
$scope = PhabricatorOAuthServerScope::scopesListToDict($scope);
|
||||
}
|
||||
|
||||
$authorization = $server->userHasAuthorizedClient($scope);
|
||||
if ($authorization) {
|
||||
$return_auth_code = true;
|
||||
$unguarded_write = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||
} else if ($request->isFormPost()) {
|
||||
$scope = PhabricatorOAuthServerScope::getScopesFromRequest($request);
|
||||
$authorization = $server->authorizeClient($scope);
|
||||
$return_auth_code = true;
|
||||
$unguarded_write = null;
|
||||
} else {
|
||||
$return_auth_code = false;
|
||||
$unguarded_write = null;
|
||||
}
|
||||
|
||||
if ($return_auth_code) {
|
||||
// step 1 -- generate authorization code
|
||||
$auth_code =
|
||||
$server->generateAuthorizationCode($access_token_uri);
|
||||
|
||||
// step 2 return it
|
||||
$content = array(
|
||||
'code' => $auth_code->getCode(),
|
||||
'scope' => $authorization->getScopeString(),
|
||||
);
|
||||
$response->setContent($content);
|
||||
return $response->setClientURI($uri);
|
||||
}
|
||||
unset($unguarded_write);
|
||||
} catch (Exception $e) {
|
||||
// Note we could try harder to determine between a server_error
|
||||
// vs temporarily_unavailable. Good enough though.
|
||||
$response->setError('server_error');
|
||||
$response->setErrorDescription(
|
||||
'The authorization server encountered an unexpected condition '.
|
||||
'which prevented it from fulfilling the request. '
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
|
||||
// display time -- make a nice form for the user to grant the client
|
||||
// access to the granularity specified by $scope
|
||||
$name = phutil_escape_html($client->getName());
|
||||
$title = 'Authorize ' . $name . '?';
|
||||
$panel = new AphrontPanelView();
|
||||
@@ -109,10 +166,30 @@ extends PhabricatorAuthController {
|
||||
"Do want to authorize {$name} to access your ".
|
||||
"Phabricator account data?";
|
||||
|
||||
$desired_scopes = array(
|
||||
PhabricatorOAuthServerScope::SCOPE_WHOAMI => 1,
|
||||
PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS => 1
|
||||
if ($scope) {
|
||||
$desired_scopes = $scope;
|
||||
if (!PhabricatorOAuthServerScope::validateScopesDict($desired_scopes)) {
|
||||
$response->setError('invalid_scope');
|
||||
$response->setErrorDescription(
|
||||
'The requested scope is invalid, unknown, or malformed.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
} else {
|
||||
$desired_scopes = array(
|
||||
PhabricatorOAuthServerScope::SCOPE_WHOAMI => 1,
|
||||
PhabricatorOAuthServerScope::SCOPE_OFFLINE_ACCESS => 1
|
||||
);
|
||||
}
|
||||
|
||||
$cancel_uri = $this->getClientURI($client, $redirect_uri);
|
||||
$cancel_params = array(
|
||||
'error' => 'access_denied',
|
||||
'error_description' =>
|
||||
'The resource owner (aka the user) denied the request.'
|
||||
);
|
||||
$cancel_uri->setQueryParams($cancel_params);
|
||||
|
||||
$form = id(new AphrontFormView())
|
||||
->setUser($current_user)
|
||||
->appendChild(
|
||||
@@ -125,15 +202,14 @@ extends PhabricatorAuthController {
|
||||
->appendChild(
|
||||
id(new AphrontFormSubmitControl())
|
||||
->setValue('Authorize')
|
||||
->addCancelButton('/')
|
||||
->addCancelButton($cancel_uri)
|
||||
);
|
||||
// TODO -- T889 (make "cancel" do something more sensible)
|
||||
|
||||
$panel->appendChild($form);
|
||||
|
||||
return $this->buildStandardPageResponse(
|
||||
array($error_view,
|
||||
$panel),
|
||||
$panel,
|
||||
array('title' => $title));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ phutil_require_module('phabricator', 'applications/oauthserver/storage/client');
|
||||
phutil_require_module('phabricator', 'view/form/base');
|
||||
phutil_require_module('phabricator', 'view/form/control/static');
|
||||
phutil_require_module('phabricator', 'view/form/control/submit');
|
||||
phutil_require_module('phabricator', 'view/form/error');
|
||||
phutil_require_module('phabricator', 'view/layout/panel');
|
||||
|
||||
phutil_require_module('phutil', 'markup');
|
||||
|
||||
@@ -78,25 +78,33 @@ extends PhabricatorOAuthClientBaseController {
|
||||
->setForbiddenText($message);
|
||||
}
|
||||
$submit_button = 'Save OAuth Client';
|
||||
$secret = null;
|
||||
// new client - much simpler
|
||||
} else {
|
||||
$client = new PhabricatorOAuthServerClient();
|
||||
$title = 'Create OAuth Client';
|
||||
$client = new PhabricatorOAuthServerClient();
|
||||
$title = 'Create OAuth Client';
|
||||
$submit_button = 'Create OAuth Client';
|
||||
$secret = Filesystem::readRandomCharacters(32);
|
||||
}
|
||||
|
||||
if ($request->isFormPost()) {
|
||||
$redirect_uri = $request->getStr('redirect_uri');
|
||||
$client->setName($request->getStr('name'));
|
||||
$client->setRedirectURI($redirect_uri);
|
||||
$client->setSecret(Filesystem::readRandomCharacters(32));
|
||||
if ($secret) {
|
||||
$client->setSecret($secret);
|
||||
}
|
||||
$client->setCreatorPHID($current_user->getPHID());
|
||||
$uri = new PhutilURI($redirect_uri);
|
||||
if (!$this->validateRedirectURI($uri)) {
|
||||
$server = new PhabricatorOAuthServer();
|
||||
if (!$server->validateRedirectURI($uri)) {
|
||||
$error = new AphrontErrorView();
|
||||
$error->setSeverity(AphrontErrorView::SEVERITY_ERROR);
|
||||
$error->setTitle(
|
||||
'Redirect URI must be a fully qualified domain name.'
|
||||
'Redirect URI must be a fully qualified domain name '.
|
||||
'with no fragments. See '.
|
||||
'http://tools.ietf.org/html/draft-ietf-oauth-v2-23#section-3.1.2 '.
|
||||
'for more information on the correct format.'
|
||||
);
|
||||
$bad_redirect = true;
|
||||
} else {
|
||||
@@ -140,7 +148,7 @@ extends PhabricatorOAuthClientBaseController {
|
||||
->setValue($phid)
|
||||
)
|
||||
->appendChild(
|
||||
id(new AphrontFormTextControl())
|
||||
id(new AphrontFormStaticControl())
|
||||
->setLabel('Secret')
|
||||
->setValue($client->getSecret())
|
||||
);
|
||||
@@ -185,13 +193,4 @@ extends PhabricatorOAuthClientBaseController {
|
||||
);
|
||||
}
|
||||
|
||||
private function validateRedirectURI(PhutilURI $uri) {
|
||||
if (PhabricatorEnv::isValidRemoteWebResource($uri)) {
|
||||
if ($uri->getDomain()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ phutil_require_module('phabricator', 'aphront/response/403');
|
||||
phutil_require_module('phabricator', 'aphront/response/404');
|
||||
phutil_require_module('phabricator', 'aphront/response/redirect');
|
||||
phutil_require_module('phabricator', 'applications/oauthserver/controller/client/base');
|
||||
phutil_require_module('phabricator', 'applications/oauthserver/server');
|
||||
phutil_require_module('phabricator', 'applications/oauthserver/storage/client');
|
||||
phutil_require_module('phabricator', 'infrastructure/env');
|
||||
phutil_require_module('phabricator', 'view/form/base');
|
||||
phutil_require_module('phabricator', 'view/form/control/static');
|
||||
phutil_require_module('phabricator', 'view/form/control/submit');
|
||||
|
||||
@@ -47,6 +47,7 @@ extends PhabricatorOAuthClientBaseController {
|
||||
phutil_escape_html($client->getName())
|
||||
),
|
||||
$client->getPHID(),
|
||||
$client->getSecret(),
|
||||
phutil_render_tag(
|
||||
'a',
|
||||
array(
|
||||
@@ -88,6 +89,7 @@ extends PhabricatorOAuthClientBaseController {
|
||||
array(
|
||||
'Client',
|
||||
'ID',
|
||||
'Secret',
|
||||
'Redirect URI',
|
||||
'',
|
||||
));
|
||||
@@ -96,6 +98,7 @@ extends PhabricatorOAuthClientBaseController {
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'',
|
||||
'action',
|
||||
));
|
||||
if (empty($rows)) {
|
||||
|
||||
@@ -28,77 +28,133 @@ extends PhabricatorAuthController {
|
||||
|
||||
public function processRequest() {
|
||||
$request = $this->getRequest();
|
||||
$grant_type = $request->getStr('grant_type');
|
||||
$code = $request->getStr('code');
|
||||
$redirect_uri = $request->getStr('redirect_uri');
|
||||
$client_phid = $request->getStr('client_id');
|
||||
$client_secret = $request->getStr('client_secret');
|
||||
$response = new PhabricatorOAuthResponse();
|
||||
$server = new PhabricatorOAuthServer();
|
||||
if ($grant_type != 'authorization_code') {
|
||||
$response->setError('unsupported_grant_type');
|
||||
$response->setErrorDescription(
|
||||
'Only grant_type authorization_code is supported.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
if (!$code) {
|
||||
return $response->setMalformed(
|
||||
$response->setError('invalid_request');
|
||||
$response->setErrorDescription(
|
||||
'Required parameter code missing.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
if (!$client_phid) {
|
||||
return $response->setMalformed(
|
||||
$response->setError('invalid_request');
|
||||
$response->setErrorDescription(
|
||||
'Required parameter client_id missing.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
if (!$client_secret) {
|
||||
return $response->setMalformed(
|
||||
$response->setError('invalid_request');
|
||||
$response->setErrorDescription(
|
||||
'Required parameter client_secret missing.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
// one giant try / catch around all the exciting database stuff so we
|
||||
// can return a 'server_error' response if something goes wrong!
|
||||
try {
|
||||
$auth_code = id(new PhabricatorOAuthServerAuthorizationCode())
|
||||
->loadOneWhere('code = %s',
|
||||
$code);
|
||||
if (!$auth_code) {
|
||||
$response->setError('invalid_grant');
|
||||
$response->setErrorDescription(
|
||||
'Authorization code '.$code.' not found.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
|
||||
$client = id(new PhabricatorOAuthServerClient())
|
||||
->loadOneWhere('phid = %s', $client_phid);
|
||||
if (!$client) {
|
||||
return $response->setNotFound(
|
||||
'Client with client_id '.$client_phid.' not found.'
|
||||
);
|
||||
}
|
||||
$server->setClient($client);
|
||||
// if we have an auth code redirect URI, there must be a redirect_uri
|
||||
// in the request and it must match the auth code redirect uri *exactly*
|
||||
$auth_code_redirect_uri = $auth_code->getRedirectURI();
|
||||
if ($auth_code_redirect_uri) {
|
||||
$auth_code_redirect_uri = new PhutilURI($auth_code_redirect_uri);
|
||||
$redirect_uri = new PhutilURI($redirect_uri);
|
||||
if (!$redirect_uri->getDomain() ||
|
||||
$redirect_uri != $auth_code_redirect_uri) {
|
||||
$response->setError('invalid_grant');
|
||||
$response->setErrorDescription(
|
||||
'Redirect uri in request must exactly match redirect uri '.
|
||||
'from authorization code.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
} else if ($redirect_uri) {
|
||||
$response->setError('invalid_grant');
|
||||
$response->setErrorDescription(
|
||||
'Redirect uri in request and no redirect uri in authorization '.
|
||||
'code. The two must exactly match.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
|
||||
$auth_code = id(new PhabricatorOAuthServerAuthorizationCode())
|
||||
->loadOneWhere('code = %s', $code);
|
||||
if (!$auth_code) {
|
||||
return $response->setNotFound(
|
||||
'Authorization code '.$code.' not found.'
|
||||
);
|
||||
}
|
||||
$client = id(new PhabricatorOAuthServerClient())
|
||||
->loadOneWhere('phid = %s',
|
||||
$client_phid);
|
||||
if (!$client) {
|
||||
$response->setError('invalid_client');
|
||||
$response->setErrorDescription(
|
||||
'Client with client_id '.$client_phid.' not found.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
$server->setClient($client);
|
||||
|
||||
$user_phid = $auth_code->getUserPHID();
|
||||
$user = id(new PhabricatorUser())
|
||||
->loadOneWhere('phid = %s', $user_phid);
|
||||
if (!$user) {
|
||||
return $response->setNotFound(
|
||||
'User with phid '.$user_phid.' not found.'
|
||||
);
|
||||
}
|
||||
$server->setUser($user);
|
||||
$user_phid = $auth_code->getUserPHID();
|
||||
$user = id(new PhabricatorUser())
|
||||
->loadOneWhere('phid = %s', $user_phid);
|
||||
if (!$user) {
|
||||
$response->setError('invalid_grant');
|
||||
$response->setErrorDescription(
|
||||
'User with phid '.$user_phid.' not found.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
$server->setUser($user);
|
||||
|
||||
$test_code = new PhabricatorOAuthServerAuthorizationCode();
|
||||
$test_code->setClientSecret($client_secret);
|
||||
$test_code->setClientPHID($client_phid);
|
||||
$is_good_code = $server->validateAuthorizationCode($auth_code,
|
||||
$test_code);
|
||||
if (!$is_good_code) {
|
||||
return $response->setMalformed(
|
||||
'Invalid authorization code '.$code.'.'
|
||||
);
|
||||
}
|
||||
$test_code = new PhabricatorOAuthServerAuthorizationCode();
|
||||
$test_code->setClientSecret($client_secret);
|
||||
$test_code->setClientPHID($client_phid);
|
||||
$is_good_code = $server->validateAuthorizationCode($auth_code,
|
||||
$test_code);
|
||||
if (!$is_good_code) {
|
||||
$response->setError('invalid_grant');
|
||||
$response->setErrorDescription(
|
||||
'Invalid authorization code '.$code.'.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
|
||||
$scope = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||
$access_token = $server->generateAccessToken();
|
||||
if ($access_token) {
|
||||
$unguarded = AphrontWriteGuard::beginScopedUnguardedWrites();
|
||||
$access_token = $server->generateAccessToken();
|
||||
$auth_code->delete();
|
||||
unset($unguarded);
|
||||
$result = array(
|
||||
'access_token' => $access_token->getToken(),
|
||||
'token_type' => 'Bearer',
|
||||
'expires_in' => PhabricatorOAuthServer::ACCESS_TOKEN_TIMEOUT,
|
||||
);
|
||||
'access_token' => $access_token->getToken(),
|
||||
'token_type' => 'Bearer',
|
||||
'expires_in' => PhabricatorOAuthServer::ACCESS_TOKEN_TIMEOUT,
|
||||
);
|
||||
return $response->setContent($result);
|
||||
} catch (Exception $e) {
|
||||
$response->setError('server_error');
|
||||
$response->setErrorDescription(
|
||||
'The authorization server encountered an unexpected condition '.
|
||||
'which prevented it from fulfilling the request.'
|
||||
);
|
||||
return $response;
|
||||
}
|
||||
|
||||
return $response->setMalformed('Request is malformed in some way.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ phutil_require_module('phabricator', 'applications/oauthserver/storage/authoriza
|
||||
phutil_require_module('phabricator', 'applications/oauthserver/storage/client');
|
||||
phutil_require_module('phabricator', 'applications/people/storage/user');
|
||||
|
||||
phutil_require_module('phutil', 'parser/uri');
|
||||
phutil_require_module('phutil', 'utils');
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user