diff --git a/framework/core/src/Admin/Middleware/RequireAdministrateAbility.php b/framework/core/src/Admin/Middleware/RequireAdministrateAbility.php index db91ec3c4..6d99e16d5 100644 --- a/framework/core/src/Admin/Middleware/RequireAdministrateAbility.php +++ b/framework/core/src/Admin/Middleware/RequireAdministrateAbility.php @@ -10,38 +10,22 @@ namespace Flarum\Admin\Middleware; -use Flarum\Core\Access\Gate; +use Flarum\Core\Access\AssertPermissionTrait; use Illuminate\Contracts\Container\Container; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; -use Flarum\Core\Exception\PermissionDeniedException; use Zend\Stratigility\MiddlewareInterface; class RequireAdministrateAbility implements MiddlewareInterface { - /** - * @var Gate - */ - protected $gate; - - /** - * @param Gate $gate - */ - public function __construct(Gate $gate) - { - $this->gate = $gate; - } + use AssertPermissionTrait; /** * {@inheritdoc} */ public function __invoke(Request $request, Response $response, callable $out = null) { - $actor = $request->getAttribute('actor'); - - if (! $this->gate->forUser($actor)->allows('administrate')) { - throw new PermissionDeniedException; - } + $this->assertAdminAndSudo($request); return $out ? $out($request, $response) : $response; } diff --git a/framework/core/src/Api/AccessToken.php b/framework/core/src/Api/AccessToken.php index eac23459a..79e8a1ab9 100644 --- a/framework/core/src/Api/AccessToken.php +++ b/framework/core/src/Api/AccessToken.php @@ -18,6 +18,7 @@ use DateTime; * @property int $user_id * @property \Carbon\Carbon $created_at * @property \Carbon\Carbon $expires_at + * @property \Carbon\Carbon $sudo_expires_at * @property \Flarum\Core\User|null $user */ class AccessToken extends AbstractModel @@ -37,7 +38,7 @@ class AccessToken extends AbstractModel /** * {@inheritdoc} */ - protected $dates = ['created_at', 'expires_at']; + protected $dates = ['created_at', 'expires_at', 'sudo_expires_at']; /** * Generate an access token for the specified user. @@ -54,6 +55,7 @@ class AccessToken extends AbstractModel $token->user_id = $userId; $token->created_at = time(); $token->expires_at = time() + $minutes * 60; + $token->sudo_expires_at = time() + $minutes * 30; return $token; } @@ -68,6 +70,11 @@ class AccessToken extends AbstractModel return $this->expires_at > new DateTime; } + public function isSudo() + { + return $this->sudo_expires_at > new DateTime; + } + /** * Define the relationship with the owner of this access token. * diff --git a/framework/core/src/Api/ApiServiceProvider.php b/framework/core/src/Api/ApiServiceProvider.php index f8c4ebcd2..6d815b535 100644 --- a/framework/core/src/Api/ApiServiceProvider.php +++ b/framework/core/src/Api/ApiServiceProvider.php @@ -45,8 +45,11 @@ class ApiServiceProvider extends AbstractServiceProvider $handler->registerHandler(new Handler\FloodingExceptionHandler); $handler->registerHandler(new Handler\IlluminateValidationExceptionHandler); $handler->registerHandler(new Handler\InvalidConfirmationTokenExceptionHandler); + $handler->registerHandler(new Handler\MethodNotAllowedExceptionHandler); $handler->registerHandler(new Handler\ModelNotFoundExceptionHandler); $handler->registerHandler(new Handler\PermissionDeniedExceptionHandler); + $handler->registerHandler(new Handler\RouteNotFoundExceptionHandler); + $handler->registerHandler(new Handler\SudoRequiredExceptionHandler); $handler->registerHandler(new Handler\ValidationExceptionHandler); $handler->registerHandler(new InvalidParameterExceptionHandler); $handler->registerHandler(new FallbackExceptionHandler($this->app->inDebugMode())); diff --git a/framework/core/src/Api/Controller/DeleteDiscussionController.php b/framework/core/src/Api/Controller/DeleteDiscussionController.php index 06a20c04b..043261f1d 100644 --- a/framework/core/src/Api/Controller/DeleteDiscussionController.php +++ b/framework/core/src/Api/Controller/DeleteDiscussionController.php @@ -10,12 +10,15 @@ namespace Flarum\Api\Controller; +use Flarum\Core\Access\AssertPermissionTrait; use Flarum\Core\Command\DeleteDiscussion; use Illuminate\Contracts\Bus\Dispatcher; use Psr\Http\Message\ServerRequestInterface; class DeleteDiscussionController extends AbstractDeleteController { + use AssertPermissionTrait; + /** * @var Dispatcher */ @@ -38,6 +41,8 @@ class DeleteDiscussionController extends AbstractDeleteController $actor = $request->getAttribute('actor'); $input = $request->getParsedBody(); + $this->assertSudo($request); + $this->bus->dispatch( new DeleteDiscussion($id, $actor, $input) ); diff --git a/framework/core/src/Api/Controller/DeleteGroupController.php b/framework/core/src/Api/Controller/DeleteGroupController.php index 6f9ab7435..593300fa7 100644 --- a/framework/core/src/Api/Controller/DeleteGroupController.php +++ b/framework/core/src/Api/Controller/DeleteGroupController.php @@ -10,12 +10,15 @@ namespace Flarum\Api\Controller; +use Flarum\Core\Access\AssertPermissionTrait; use Flarum\Core\Command\DeleteGroup; use Illuminate\Contracts\Bus\Dispatcher; use Psr\Http\Message\ServerRequestInterface; class DeleteGroupController extends AbstractDeleteController { + use AssertPermissionTrait; + /** * @var Dispatcher */ @@ -34,6 +37,8 @@ class DeleteGroupController extends AbstractDeleteController */ protected function delete(ServerRequestInterface $request) { + $this->assertSudo($request); + $this->bus->dispatch( new DeleteGroup(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor')) ); diff --git a/framework/core/src/Api/Controller/DeletePostController.php b/framework/core/src/Api/Controller/DeletePostController.php index 7e31838f9..b32751a28 100644 --- a/framework/core/src/Api/Controller/DeletePostController.php +++ b/framework/core/src/Api/Controller/DeletePostController.php @@ -10,12 +10,15 @@ namespace Flarum\Api\Controller; +use Flarum\Core\Access\AssertPermissionTrait; use Flarum\Core\Command\DeletePost; use Illuminate\Contracts\Bus\Dispatcher; use Psr\Http\Message\ServerRequestInterface; class DeletePostController extends AbstractDeleteController { + use AssertPermissionTrait; + /** * @var Dispatcher */ @@ -34,6 +37,8 @@ class DeletePostController extends AbstractDeleteController */ protected function delete(ServerRequestInterface $request) { + $this->assertSudo($request); + $this->bus->dispatch( new DeletePost(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor')) ); diff --git a/framework/core/src/Api/Controller/DeleteUserController.php b/framework/core/src/Api/Controller/DeleteUserController.php index cb214e531..306e5567a 100644 --- a/framework/core/src/Api/Controller/DeleteUserController.php +++ b/framework/core/src/Api/Controller/DeleteUserController.php @@ -10,12 +10,15 @@ namespace Flarum\Api\Controller; +use Flarum\Core\Access\AssertPermissionTrait; use Flarum\Core\Command\DeleteUser; use Illuminate\Contracts\Bus\Dispatcher; use Psr\Http\Message\ServerRequestInterface; class DeleteUserController extends AbstractDeleteController { + use AssertPermissionTrait; + /** * @var Dispatcher */ @@ -34,6 +37,8 @@ class DeleteUserController extends AbstractDeleteController */ protected function delete(ServerRequestInterface $request) { + $this->assertSudo($request); + $this->bus->dispatch( new DeleteUser(array_get($request->getQueryParams(), 'id'), $request->getAttribute('actor')) ); diff --git a/framework/core/src/Api/Controller/SetPermissionController.php b/framework/core/src/Api/Controller/SetPermissionController.php index 783157b94..b709f08bb 100644 --- a/framework/core/src/Api/Controller/SetPermissionController.php +++ b/framework/core/src/Api/Controller/SetPermissionController.php @@ -25,7 +25,7 @@ class SetPermissionController implements ControllerInterface */ public function handle(ServerRequestInterface $request) { - $this->assertAdmin($request->getAttribute('actor')); + $this->assertAdminAndSudo($request); $body = $request->getParsedBody(); $permission = array_get($body, 'permission'); diff --git a/framework/core/src/Api/Controller/SetSettingsController.php b/framework/core/src/Api/Controller/SetSettingsController.php index 3d5289439..418184b4e 100644 --- a/framework/core/src/Api/Controller/SetSettingsController.php +++ b/framework/core/src/Api/Controller/SetSettingsController.php @@ -47,7 +47,7 @@ class SetSettingsController implements ControllerInterface */ public function handle(ServerRequestInterface $request) { - $this->assertAdmin($request->getAttribute('actor')); + $this->assertAdminAndSudo($request); $settings = $request->getParsedBody(); diff --git a/framework/core/src/Api/Controller/UninstallExtensionController.php b/framework/core/src/Api/Controller/UninstallExtensionController.php index 3b7082817..f1cf8113a 100644 --- a/framework/core/src/Api/Controller/UninstallExtensionController.php +++ b/framework/core/src/Api/Controller/UninstallExtensionController.php @@ -33,7 +33,7 @@ class UninstallExtensionController extends AbstractDeleteController protected function delete(ServerRequestInterface $request) { - $this->assertAdmin($request->getAttribute('actor')); + $this->assertAdminAndSudo($request); $name = array_get($request->getQueryParams(), 'name'); diff --git a/framework/core/src/Api/Controller/UpdateExtensionController.php b/framework/core/src/Api/Controller/UpdateExtensionController.php index 3d221f88c..e5227a6f5 100644 --- a/framework/core/src/Api/Controller/UpdateExtensionController.php +++ b/framework/core/src/Api/Controller/UpdateExtensionController.php @@ -38,7 +38,7 @@ class UpdateExtensionController implements ControllerInterface */ public function handle(ServerRequestInterface $request) { - $this->assertAdmin($request->getAttribute('actor')); + $this->assertAdminAndSudo($request); $enabled = array_get($request->getParsedBody(), 'enabled'); $name = array_get($request->getQueryParams(), 'name'); diff --git a/framework/core/src/Api/Exception/InvalidAccessTokenException.php b/framework/core/src/Api/Exception/InvalidAccessTokenException.php new file mode 100644 index 000000000..340e47b76 --- /dev/null +++ b/framework/core/src/Api/Exception/InvalidAccessTokenException.php @@ -0,0 +1,17 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Api\Exception; + +use Exception; + +class InvalidAccessTokenException extends Exception +{ +} diff --git a/framework/core/src/Api/Handler/FloodingExceptionHandler.php b/framework/core/src/Api/Handler/FloodingExceptionHandler.php index 9f5e8440c..86a419669 100644 --- a/framework/core/src/Api/Handler/FloodingExceptionHandler.php +++ b/framework/core/src/Api/Handler/FloodingExceptionHandler.php @@ -31,7 +31,10 @@ class FloodingExceptionHandler implements ExceptionHandlerInterface public function handle(Exception $e) { $status = 429; - $error = []; + $error = [ + 'status' => (string) $status, + 'code' => 'too_many_requests' + ]; return new ResponseBag($status, [$error]); } diff --git a/framework/core/src/Api/Handler/IlluminateValidationExceptionHandler.php b/framework/core/src/Api/Handler/IlluminateValidationExceptionHandler.php index 743b4ca78..c3b3fe84f 100644 --- a/framework/core/src/Api/Handler/IlluminateValidationExceptionHandler.php +++ b/framework/core/src/Api/Handler/IlluminateValidationExceptionHandler.php @@ -44,8 +44,10 @@ class IlluminateValidationExceptionHandler implements ExceptionHandlerInterface { $errors = array_map(function ($field, $messages) { return [ + 'status' => '422', + 'code' => 'validation_error', 'detail' => implode("\n", $messages), - 'source' => ['pointer' => '/data/attributes/' . $field], + 'source' => ['pointer' => "/data/attributes/$field"] ]; }, array_keys($errors), $errors); diff --git a/framework/core/src/Api/Handler/InvalidAccessTokenExceptionHandler.php b/framework/core/src/Api/Handler/InvalidAccessTokenExceptionHandler.php new file mode 100644 index 000000000..366e57c7c --- /dev/null +++ b/framework/core/src/Api/Handler/InvalidAccessTokenExceptionHandler.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Api\Handler; + +use Exception; +use Flarum\Api\Exception\InvalidAccessTokenException; +use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface; +use Tobscure\JsonApi\Exception\Handler\ResponseBag; + +class InvalidAccessTokenExceptionHandler implements ExceptionHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function manages(Exception $e) + { + return $e instanceof InvalidAccessTokenException; + } + + /** + * {@inheritdoc} + */ + public function handle(Exception $e) + { + $status = 401; + $error = [ + 'status' => (string) $status, + 'code' => 'invalid_access_token' + ]; + + return new ResponseBag($status, [$error]); + } +} diff --git a/framework/core/src/Api/Handler/InvalidConfirmationTokenExceptionHandler.php b/framework/core/src/Api/Handler/InvalidConfirmationTokenExceptionHandler.php index 9202e487f..52ff1fab9 100644 --- a/framework/core/src/Api/Handler/InvalidConfirmationTokenExceptionHandler.php +++ b/framework/core/src/Api/Handler/InvalidConfirmationTokenExceptionHandler.php @@ -31,7 +31,10 @@ class InvalidConfirmationTokenExceptionHandler implements ExceptionHandlerInterf public function handle(Exception $e) { $status = 403; - $error = ['code' => 'invalid_confirmation_token']; + $error = [ + 'status' => (string) $status, + 'code' => 'invalid_confirmation_token' + ]; return new ResponseBag($status, [$error]); } diff --git a/framework/core/src/Api/Handler/MethodNotAllowedExceptionHandler.php b/framework/core/src/Api/Handler/MethodNotAllowedExceptionHandler.php new file mode 100644 index 000000000..d4e1b1eff --- /dev/null +++ b/framework/core/src/Api/Handler/MethodNotAllowedExceptionHandler.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Api\Handler; + +use Exception; +use Flarum\Http\Exception\MethodNotAllowedException; +use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface; +use Tobscure\JsonApi\Exception\Handler\ResponseBag; + +class MethodNotAllowedExceptionHandler implements ExceptionHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function manages(Exception $e) + { + return $e instanceof MethodNotAllowedException; + } + + /** + * {@inheritdoc} + */ + public function handle(Exception $e) + { + $status = 405; + $error = [ + 'status' => (string) $status, + 'code' => 'method_not_allowed' + ]; + + return new ResponseBag($status, [$error]); + } +} diff --git a/framework/core/src/Api/Handler/ModelNotFoundExceptionHandler.php b/framework/core/src/Api/Handler/ModelNotFoundExceptionHandler.php index 315f71111..ea80d07a7 100644 --- a/framework/core/src/Api/Handler/ModelNotFoundExceptionHandler.php +++ b/framework/core/src/Api/Handler/ModelNotFoundExceptionHandler.php @@ -11,6 +11,7 @@ namespace Flarum\Api\Handler; use Exception; +use Flarum\Http\Exception\RouteNotFoundException; use Illuminate\Database\Eloquent\ModelNotFoundException; use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface; use Tobscure\JsonApi\Exception\Handler\ResponseBag; @@ -31,7 +32,10 @@ class ModelNotFoundExceptionHandler implements ExceptionHandlerInterface public function handle(Exception $e) { $status = 404; - $error = []; + $error = [ + 'status' => '404', + 'code' => 'resource_not_found' + ]; return new ResponseBag($status, [$error]); } diff --git a/framework/core/src/Api/Handler/PermissionDeniedExceptionHandler.php b/framework/core/src/Api/Handler/PermissionDeniedExceptionHandler.php index ac689d837..238dc45fd 100644 --- a/framework/core/src/Api/Handler/PermissionDeniedExceptionHandler.php +++ b/framework/core/src/Api/Handler/PermissionDeniedExceptionHandler.php @@ -31,7 +31,10 @@ class PermissionDeniedExceptionHandler implements ExceptionHandlerInterface public function handle(Exception $e) { $status = 401; - $error = []; + $error = [ + 'status' => (string) $status, + 'code' => 'permission_denied' + ]; return new ResponseBag($status, [$error]); } diff --git a/framework/core/src/Api/Handler/RouteNotFoundExceptionHandler.php b/framework/core/src/Api/Handler/RouteNotFoundExceptionHandler.php new file mode 100644 index 000000000..5d27e0b0e --- /dev/null +++ b/framework/core/src/Api/Handler/RouteNotFoundExceptionHandler.php @@ -0,0 +1,41 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Api\Handler; + +use Exception; +use Flarum\Http\Exception\RouteNotFoundException; +use Tobscure\JsonApi\Exception\Handler\ExceptionHandlerInterface; +use Tobscure\JsonApi\Exception\Handler\ResponseBag; + +class RouteNotFoundExceptionHandler implements ExceptionHandlerInterface +{ + /** + * {@inheritdoc} + */ + public function manages(Exception $e) + { + return $e instanceof RouteNotFoundException; + } + + /** + * {@inheritdoc} + */ + public function handle(Exception $e) + { + $status = 404; + $error = [ + 'status' => (string) $status, + 'code' => 'route_not_found' + ]; + + return new ResponseBag($status, [$error]); + } +} diff --git a/framework/core/src/Api/Handler/ValidationExceptionHandler.php b/framework/core/src/Api/Handler/ValidationExceptionHandler.php index 4e0ad26e8..7b2ce70d1 100644 --- a/framework/core/src/Api/Handler/ValidationExceptionHandler.php +++ b/framework/core/src/Api/Handler/ValidationExceptionHandler.php @@ -33,10 +33,13 @@ class ValidationExceptionHandler implements ExceptionHandlerInterface $status = 422; $messages = $e->getMessages(); - $errors = array_map(function ($path, $detail) { - $source = ['pointer' => '/data/attributes/' . $path]; - - return compact('source', 'detail'); + $errors = array_map(function ($path, $detail) use ($status) { + return [ + 'status' => (string) $status, + 'code' => 'validation_error', + 'detail' => $detail, + 'source' => ['pointer' => "/data/attributes/$path"] + ]; }, array_keys($messages), $messages); return new ResponseBag($status, $errors); diff --git a/framework/core/src/Api/Middleware/AuthenticateWithHeader.php b/framework/core/src/Api/Middleware/AuthenticateWithHeader.php index c76d42f4f..d3a9a704e 100644 --- a/framework/core/src/Api/Middleware/AuthenticateWithHeader.php +++ b/framework/core/src/Api/Middleware/AuthenticateWithHeader.php @@ -69,6 +69,8 @@ class AuthenticateWithHeader implements MiddlewareInterface $actor = $accessToken->user; $actor->updateLastSeen()->save(); + + $request = $request->withAttribute('sudo', $accessToken->isSudo()); } elseif (isset($parts[1]) && ($apiKey = ApiKey::valid($token))) { $userParts = explode('=', trim($parts[1])); diff --git a/framework/core/src/Core/Access/AssertPermissionTrait.php b/framework/core/src/Core/Access/AssertPermissionTrait.php index a88506993..e3f041077 100644 --- a/framework/core/src/Core/Access/AssertPermissionTrait.php +++ b/framework/core/src/Core/Access/AssertPermissionTrait.php @@ -10,8 +10,10 @@ namespace Flarum\Core\Access; +use Flarum\Api\Exception\InvalidAccessTokenException; use Flarum\Core\Exception\PermissionDeniedException; use Flarum\Core\User; +use Psr\Http\Message\ServerRequestInterface; trait AssertPermissionTrait { @@ -61,6 +63,28 @@ trait AssertPermissionTrait */ protected function assertAdmin(User $actor) { - $this->assertPermission($actor->isAdmin()); + $this->assertCan($actor, 'administrate'); + } + + /** + * @param ServerRequestInterface $request + * @throws InvalidAccessTokenException + */ + protected function assertSudo(ServerRequestInterface $request) + { + if (! $request->getAttribute('sudo')) { + throw new InvalidAccessTokenException; + } + } + + /** + * @param ServerRequestInterface $request + * @throws PermissionDeniedException + */ + protected function assertAdminAndSudo(ServerRequestInterface $request) + { + $this->assertAdmin($request->getAttribute('actor')); + + $this->assertSudo($request); } } diff --git a/framework/core/src/Http/Exception/MethodNotAllowedException.php b/framework/core/src/Http/Exception/MethodNotAllowedException.php new file mode 100644 index 000000000..0b7cd3f8e --- /dev/null +++ b/framework/core/src/Http/Exception/MethodNotAllowedException.php @@ -0,0 +1,22 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Flarum\Http\Exception; + +use Exception; + +class MethodNotAllowedException extends Exception +{ + public function __construct($message = null, $code = 405, Exception $previous = null) + { + parent::__construct($message, $code, $previous); + } +} diff --git a/framework/core/src/Http/Middleware/AuthenticateWithCookie.php b/framework/core/src/Http/Middleware/AuthenticateWithCookie.php index 87d06d8a8..97606a8ec 100644 --- a/framework/core/src/Http/Middleware/AuthenticateWithCookie.php +++ b/framework/core/src/Http/Middleware/AuthenticateWithCookie.php @@ -11,6 +11,7 @@ namespace Flarum\Http\Middleware; use Flarum\Api\AccessToken; +use Flarum\Api\Exception\InvalidAccessTokenException; use Flarum\Core\Guest; use Flarum\Locale\LocaleManager; use Psr\Http\Message\ResponseInterface as Response; @@ -47,6 +48,7 @@ class AuthenticateWithCookie implements MiddlewareInterface * * @param Request $request * @return Request + * @throws InvalidAccessTokenException */ protected function logIn(Request $request) { @@ -54,9 +56,11 @@ class AuthenticateWithCookie implements MiddlewareInterface if ($token = $this->getToken($request)) { if (! $token->isValid()) { - // TODO: https://github.com/flarum/core/issues/253 + throw new InvalidAccessTokenException; } elseif ($actor = $token->user) { $actor->updateLastSeen()->save(); + + $request = $request->withAttribute('sudo', $token->isSudo()); } } diff --git a/framework/core/src/Http/Middleware/DispatchRoute.php b/framework/core/src/Http/Middleware/DispatchRoute.php index b63208fc2..f8f8d55e4 100644 --- a/framework/core/src/Http/Middleware/DispatchRoute.php +++ b/framework/core/src/Http/Middleware/DispatchRoute.php @@ -13,8 +13,9 @@ namespace Flarum\Http\Middleware; use FastRoute\Dispatcher; use FastRoute\RouteParser; -use Flarum\Http\RouteCollection; +use Flarum\Http\Exception\MethodNotAllowedException; use Flarum\Http\Exception\RouteNotFoundException; +use Flarum\Http\RouteCollection; use Psr\Http\Message\ResponseInterface as Response; use Psr\Http\Message\ServerRequestInterface as Request; @@ -47,6 +48,7 @@ class DispatchRoute * @param Response $response * @param callable $out * @return Response + * @throws MethodNotAllowedException * @throws RouteNotFoundException */ public function __invoke(Request $request, Response $response, callable $out = null) @@ -58,8 +60,11 @@ class DispatchRoute switch ($routeInfo[0]) { case Dispatcher::NOT_FOUND: - case Dispatcher::METHOD_NOT_ALLOWED: throw new RouteNotFoundException; + + case Dispatcher::METHOD_NOT_ALLOWED: + throw new MethodNotAllowedException; + case Dispatcher::FOUND: $handler = $routeInfo[1]; $parameters = $routeInfo[2];