From 04920a86a2530504d35da077aacca1f3131e78e2 Mon Sep 17 00:00:00 2001 From: Jeremy Dormitzer Date: Wed, 23 Jan 2019 09:28:15 -0500 Subject: [PATCH] Refactor routing/controller layer for better separation of concerns --- src/ActivityPub.php | 6 +- src/Config/ActivityPubModule.php | 25 ++-- ...ObjectController.php => GetController.php} | 4 +- src/Controllers/InboxController.php | 51 ------- src/Controllers/OutboxController.php | 52 ------- src/Controllers/PostController.php | 111 +++++++++++++++ src/Http/ControllerResolver.php | 93 ------------- src/Http/Router.php | 60 ++++++++ test/Config/ActivityPubModuleTest.php | 9 +- ...ntrollerTest.php => GetControllerTest.php} | 22 +-- test/Controllers/InboxControllerTest.php | 14 -- ...trollerTest.php => PostControllerTest.php} | 2 +- test/Http/ControllerResolverTest.php | 130 ------------------ test/Http/RouterTest.php | 14 ++ 14 files changed, 217 insertions(+), 376 deletions(-) rename src/Controllers/{GetObjectController.php => GetController.php} (95%) delete mode 100644 src/Controllers/InboxController.php delete mode 100644 src/Controllers/OutboxController.php create mode 100644 src/Controllers/PostController.php delete mode 100644 src/Http/ControllerResolver.php create mode 100644 src/Http/Router.php rename test/Controllers/{GetObjectControllerTest.php => GetControllerTest.php} (88%) delete mode 100644 test/Controllers/InboxControllerTest.php rename test/Controllers/{OutboxControllerTest.php => PostControllerTest.php} (82%) delete mode 100644 test/Http/ControllerResolverTest.php create mode 100644 test/Http/RouterTest.php diff --git a/src/ActivityPub.php b/src/ActivityPub.php index 07ba540..c3cbbec 100644 --- a/src/ActivityPub.php +++ b/src/ActivityPub.php @@ -6,7 +6,7 @@ require_once __DIR__ . '/../vendor/autoload.php'; use ActivityPub\Auth\AuthListener; use ActivityPub\Auth\SignatureListener; use ActivityPub\Config\ActivityPubModule; -use ActivityPub\Http\ControllerResolver; +use ActivityPub\Http\Router; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Tools\SchemaTool; use Symfony\Component\HttpFoundation\Request; @@ -14,6 +14,7 @@ use Symfony\Component\HttpKernel\HttpKernel; use Symfony\Component\EventDispatcher; use Symfony\Component\HttpFoundation\RequestStack; use Symfony\Component\HttpKernel\Controller\ArgumentResolver; +use Symfony\Component\HttpKernel\Controller\ControllerResolver; use Symfony\Component\HttpKernel\EventListener\ExceptionListener; class ActivityPub @@ -49,11 +50,12 @@ class ActivityPub } $dispatcher = $this->module->get( EventDispatcher::class ); + $dispatcher->addSubscriber( $this->module->get( Router::class ) ); $dispatcher->addSubscriber( $this->module->get( AuthListener::class ) ); $dispatcher->addSubscriber( $this->module->get( SignatureListener::class ) ); $dispatcher->addSubscriber( new ExceptionListener() ); - $controllerResolver = $this->module->get( ControllerResolver::class ); + $controllerResolver = new ControllerResolver(); $argumentResolver = new ArgumentResolver(); $kernel = new HttpKernel( diff --git a/src/Config/ActivityPubModule.php b/src/Config/ActivityPubModule.php index 3bbba56..72714eb 100644 --- a/src/Config/ActivityPubModule.php +++ b/src/Config/ActivityPubModule.php @@ -4,12 +4,11 @@ namespace ActivityPub\Config; use ActivityPub\Auth\AuthListener; use ActivityPub\Auth\AuthService; use ActivityPub\Auth\SignatureListener; -use ActivityPub\Controllers\GetObjectController; -use ActivityPub\Controllers\InboxController; -use ActivityPub\Controllers\OutboxController; +use ActivityPub\Controllers\GetController; +use ActivityPub\Controllers\PostController; use ActivityPub\Crypto\HttpSignatureService; use ActivityPub\Database\PrefixNamingStrategy; -use ActivityPub\Http\ControllerResolver; +use ActivityPub\Http\Router; use ActivityPub\Objects\ContextProvider; use ActivityPub\Objects\CollectionsService; use ActivityPub\Objects\IdProvider; @@ -104,22 +103,18 @@ class ActivityPubModule ->addArgument( new Reference( ObjectsService::class ) ) ->addArgument( new Reference( RandomProvider::class ) ); - $this->injector->register( GetObjectController::class, GetObjectController::class ) + $this->injector->register( GetController::class, GetController::class ) ->addArgument( new Reference( ObjectsService::class ) ) ->addArgument( new Reference( CollectionsService::class ) ) ->addArgument( new Reference( AuthService::class ) ); - $this->injector->register( InboxController::class, InboxController::class ) - ->addArgument( new Reference( EventDispatcher::class ) ); + $this->injector->register( PostController::class, PostController::class ) + ->addArgument( new Reference( EventDispatcher::class ) ) + ->addArgument( new Reference( ObjectsService::class ) ); - $this->injector->register( OutboxController::class, OutboxController::class ) - ->addArgument( new Reference( EventDispatcher::class ) ); - - $this->injector->register( ControllerResolver::class, ControllerResolver::class ) - ->addArgument( new Reference( ObjectsService::class ) ) - ->addArgument( new Reference( GetObjectController::class ) ) - ->addArgument( new Reference( InboxController::class ) ) - ->addArgument( new Reference( OutboxController::class ) ); + $this->injector->register( Router::class, Router::class ) + ->addArgument( new Reference( GetController::class ) ) + ->addArgument( new Reference( PostController::class ) ); } /** diff --git a/src/Controllers/GetObjectController.php b/src/Controllers/GetController.php similarity index 95% rename from src/Controllers/GetObjectController.php rename to src/Controllers/GetController.php index b8baf7f..87ca4ee 100644 --- a/src/Controllers/GetObjectController.php +++ b/src/Controllers/GetController.php @@ -11,9 +11,9 @@ use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; /** - * The GetObjectController is responsible for rendering ActivityPub objects as JSON + * The GetController is responsible for rendering ActivityPub objects as JSON */ -class GetObjectController +class GetController { /** diff --git a/src/Controllers/InboxController.php b/src/Controllers/InboxController.php deleted file mode 100644 index 25a9b1f..0000000 --- a/src/Controllers/InboxController.php +++ /dev/null @@ -1,51 +0,0 @@ -eventDispatcher = $EventDispatcher; - } - - /** - * Handles the inbox request and returns a proper Response - * - * @param Request $request The request - * @return Response - */ - public function handle( Request $request ) - { - if ( ! $request->attributes->has( 'actor' ) ) { - throw new UnauthorizedHttpException(); - } - $actor = $request->attributes->get( 'actor' ); - $inboxId = $this->getUriWithoutQuery( $request ); - if ( ! $actor->hasField( 'inbox' ) || $actor['inbox']['id'] !== $inboxId ) { - throw new UnauthorizedHttpException(); - } - $activity = $request->attributes->get( 'activity' ); - $event = new InboxActivityEvent( $activity, $actor, $request ); - $this->eventDispatcher->dispatch( InboxActivityEvent::NAME, $event ); - } - - private function getUriWithoutQuery( Request $request ) - { - $uri = $request->getUri(); - $queryPos = strpos( $uri, '?' ); - if ( $queryPos !== false ) { - $uri = substr( $uri, 0, $queryPos ); - } - return $uri; - } -} -?> diff --git a/src/Controllers/OutboxController.php b/src/Controllers/OutboxController.php deleted file mode 100644 index 32a53bd..0000000 --- a/src/Controllers/OutboxController.php +++ /dev/null @@ -1,52 +0,0 @@ -eventDispatcher = $eventDispatcher; - } - - /** - * Handles the outbox request and returns a proper response - * - * @param Request $request The incoming request - * @return Response - */ - public function handle( Request $request ) - { - if ( ! $request->attributes->has( 'actor' ) ) { - throw new UnauthorizedHttpException(); - } - $actor = $request->attributes->get( 'actor' ); - $outboxId = $this->getUriWithoutQuery( $request ); - if ( ! $actor->hasField( 'outbox' ) || $actor['outbox']['id'] !== $outboxId ) { - throw new UnauthorizedHttpException(); - } - $activity = $request->attributes->get( 'activity' ); - $event = new OutboxActivityEvent( $activity, $actor, $request ); - $this->eventDispatcher->dispatch( OutboxActivityEvent::NAME, $event ); - } - - private function getUriWithoutQuery( Request $request ) - { - $uri = $request->getUri(); - $queryPos = strpos( $uri, '?' ); - if ( $queryPos !== false ) { - $uri = substr( $uri, 0, $queryPos ); - } - return $uri; - } -} -?> diff --git a/src/Controllers/PostController.php b/src/Controllers/PostController.php new file mode 100644 index 0000000..263c200 --- /dev/null +++ b/src/Controllers/PostController.php @@ -0,0 +1,111 @@ +eventDispatcher = $eventDispatcher; + $this->objectsService = $objectsService; + } + + /** + * Handles an incoming POST request + * + * Either dispatches an inbox/outbox activity event or throws the appropriate + * HTTP error. + * @param Request $request The request + */ + public function handle( Request $request ) + { + $uri = $this->getUriWithoutQuery( $request ); + $object = $this->objectsService->dereference( $uri ); + if ( ! $object ) { + throw new NotFoundHttpException; + } + $actorWithInbox = $this->objectWithField( 'inbox', $uri ); + if ( $actorWithInbox ) { + if ( ! $request->attributes->has( 'signed' ) || + ! $this->authorized( $request, $actorWithInbox ) ) { + throw new UnauthorizedHttpException(); + } + $activity = json_decode( $request->getContent(), true ); + if ( ! $activity ) { + throw new BadRequestHttpException(); + } + $event = new InboxActivityEvent( $activity, $actorWithInbox, $request ); + $this->eventDispatcher->dispatch( InboxActivityEvent::NAME, $event ); + return; + } + $actorWithOutbox = $this->objectWithField( 'outbox', $uri ); + if ( $actorWithOutbox ) { + if ( ! $this->authorized( $request, $actorWithOutbox ) ) { + throw new UnauthorizedHttpException(); + } + $activity = json_decode( $request->getContent(), true ); + if ( ! $activity ) { + throw new BadRequestHttpException(); + } + $event = new OutboxActivityEvent( $activity, $actorWithOutbox, $request ); + $this->eventDispatcher->dispatch( OutboxActivityEvent::NAME, $event ); + return; + } + throw new MethodNotAllowedHttpException( array( Request::METHOD_GET ) ); + } + + private function authorized( Request $request, ActivityPubObject $activityActor ) + { + if ( ! $request->attributes->has( 'actor' ) ) { + return false; + } + $requestActor = $request->attributes->get( 'actor' ); + if ( $requestActor['id'] !== $activityActor['id'] ) { + return false; + } + return true; + } + + private function objectWithField( string $name, string $value ) + { + $results = $this->objectsService->query( array( $name => $value ) ); + if ( count( $results ) === 0 ) { + return false; + } + return $results[0]; + } + + private function getUriWithoutQuery( Request $request ) + { + $uri = $request->getUri(); + $queryPos = strpos( $uri, '?' ); + if ( $queryPos !== false ) { + $uri = substr( $uri, 0, $queryPos ); + } + return $uri; + } +} +?> diff --git a/src/Http/ControllerResolver.php b/src/Http/ControllerResolver.php deleted file mode 100644 index 21cb9da..0000000 --- a/src/Http/ControllerResolver.php +++ /dev/null @@ -1,93 +0,0 @@ -objectsService = $objectsService; - $this->getObjectController = $getObjectController; - $this->inboxController = $inboxController; - $this->outboxController = $outboxController; - } - - /** - * Returns true if an object with a field named $name with value $value exists - * - * @param string $name The field name to look for - * @param string $value The field value to look for - * @return ActivityPubObject|bool The first result, or false if there are no results - */ - private function objectWithField( string $name, string $value ) - { - $results = $this->objectsService->query( array( $name => $value ) ); - if ( count( $results ) === 0 ) { - return false; - } - return $results[0]; - } - - public function getController( Request $request ) - { - if ( $request->getMethod() == Request::METHOD_GET ) { - return array( $this->getObjectController, 'handle' ); - } else if ( $request->getMethod() == Request::METHOD_POST ) { - $uri = $this->getUriWithoutQuery( $request ); - $actorWithInbox = $this->objectWithField( 'inbox', $uri ); - if ( $actorWithInbox ) { - $activity = json_decode( $request->getContent(), true ); - if ( ! $activity || ! array_key_exists( 'type', $activity ) ) { - throw new BadRequestHttpException( '"type" field not found' ); - } - $request->attributes->set( 'activity', $activity ); - return array( $this->inboxController, 'handle' ); - } else { - $actorWithOutbox = $this->objectWithField( 'outbox', $uri ); - if ( $actorWithOutbox ) { - $activity = json_decode( $request->getContent(), true ); - if ( ! $activity || ! array_key_exists( 'type', $activity ) ) { - throw new BadRequestHttpException( '"type" field not found' ); - } - $request->attributes->set( 'activity', $activity ); - return array( $this->outboxController, 'handle' ); - } else { - throw new NotFoundHttpException(); - } - } - } else { - throw new MethodNotAllowedHttpException( array( - Request::METHOD_GET, - Request::METHOD_POST, - ) ); - } - } - - private function getUriWithoutQuery( Request $request ) - { - $uri = $request->getUri(); - $queryPos = strpos( $uri, '?' ); - if ( $queryPos !== false ) { - $uri = substr( $uri, 0, $queryPos ); - } - return $uri; - } -} -?> diff --git a/src/Http/Router.php b/src/Http/Router.php new file mode 100644 index 0000000..afa2178 --- /dev/null +++ b/src/Http/Router.php @@ -0,0 +1,60 @@ + 'route', + ); + } + + public function __construct( GetController $getController, + PostController $postController ) + { + $this->getController = $getController; + $this->postController = $postController; + } + /** + * Routes the request by setting the _controller attribute + * + * @param GetResponseEvent $event The request event + */ + public function route( GetResponseEvent $event ) + { + $request = $event->getRequest(); + if ( $request->getMethod() === Request::METHOD_GET ) { + $request->attributes->set( + '_controller', array( $this->getController, 'handle' ) + ); + } else if ( $request->getMethod() === Request::METHOD_POST ) { + $request->attributes->set( + '_controller', array( $this->postController, 'handle' ) + ); + } else { + throw new MethodNotAllowedHttpException( array( + Request::METHOD_GET, Request::METHOD_POST + ) ); + } + } +} +?> diff --git a/test/Config/ActivityPubModuleTest.php b/test/Config/ActivityPubModuleTest.php index 2a1aaeb..1f27c77 100644 --- a/test/Config/ActivityPubModuleTest.php +++ b/test/Config/ActivityPubModuleTest.php @@ -2,7 +2,7 @@ namespace ActivityPub\Test\Config; use ActivityPub\Config\ActivityPubModule; -use ActivityPub\Http\ControllerResolver; +use ActivityPub\Http\Router; use Doctrine\ORM\EntityManager; use PHPUnit\Framework\TestCase; @@ -27,10 +27,9 @@ class ActivityPubModuleTest extends TestCase $this->assertNotNull( $entityManager ); $this->assertInstanceOf( EntityManager::class, $entityManager ); - $controllerResolver = $this->module->get( ControllerResolver::class ); - $this->assertNotNull( $controllerResolver ); - $this->assertInstanceOf( ControllerResolver::class, $controllerResolver ); - + $router = $this->module->get( Router::class ); + $this->assertNotNull( $router ); + $this->assertInstanceOf( Router::class, $router ); } } ?> diff --git a/test/Controllers/GetObjectControllerTest.php b/test/Controllers/GetControllerTest.php similarity index 88% rename from test/Controllers/GetObjectControllerTest.php rename to test/Controllers/GetControllerTest.php index 63776a9..f9bad68 100644 --- a/test/Controllers/GetObjectControllerTest.php +++ b/test/Controllers/GetControllerTest.php @@ -2,7 +2,7 @@ namespace ActivityPub\Test\Controllers; use ActivityPub\Auth\AuthService; -use ActivityPub\Controllers\GetObjectController; +use ActivityPub\Controllers\GetController; use ActivityPub\Entities\ActivityPubObject; use ActivityPub\Entities\Field; use ActivityPub\Objects\ContextProvider; @@ -14,7 +14,7 @@ use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use PHPUnit\Framework\TestCase; -class GetObjectControllerTest extends TestCase +class GetControllerTest extends TestCase { const OBJECTS = array( 'https://example.com/objects/1' => array( @@ -51,7 +51,7 @@ class GetObjectControllerTest extends TestCase ), ); - private $getObjectController; + private $getController; public function setUp() { @@ -66,7 +66,7 @@ class GetObjectControllerTest extends TestCase $authService = new AuthService(); $contextProvider = new ContextProvider(); $collectionsService = new CollectionsService( 4, $authService, $contextProvider ); - $this->getObjectController = new GetObjectController( + $this->getController = new GetController( $objectsService, $collectionsService, $authService ); } @@ -74,7 +74,7 @@ class GetObjectControllerTest extends TestCase public function testItRendersPersistedObject() { $request = Request::create( 'https://example.com/objects/1' ); - $response = $this->getObjectController->handle( $request ); + $response = $this->getController->handle( $request ); $this->assertNotNull( $response ); $this->assertEquals( json_encode( self::OBJECTS['https://example.com/objects/1'] ), @@ -87,21 +87,21 @@ class GetObjectControllerTest extends TestCase { $request = Request::create( 'https://example.com/objects/notreal' ); $this->expectException( NotFoundHttpException::class ); - $this->getObjectController->handle( $request ); + $this->getController->handle( $request ); } public function testItDeniesAccess() { $request = Request::create( 'https://example.com/objects/2' ); $this->expectException( UnauthorizedHttpException::class ); - $this->getObjectController->handle( $request ); + $this->getController->handle( $request ); } public function testItAllowsAccessToAuthedActor() { $request = Request::create( 'https://example.com/objects/2' ); $request->attributes->set( 'actor', 'https://example.com/actor/1' ); - $response = $this->getObjectController->handle( $request ); + $response = $this->getController->handle( $request ); $this->assertNotNull( $response ); $this->assertEquals( json_encode( self::OBJECTS['https://example.com/objects/2'] ), @@ -114,7 +114,7 @@ class GetObjectControllerTest extends TestCase { $request = Request::create( 'https://example.com/objects/2' ); $request->attributes->set( 'actor', 'https://example.com/actor/2' ); - $response = $this->getObjectController->handle( $request ); + $response = $this->getController->handle( $request ); $this->assertNotNull( $response ); $this->assertEquals( json_encode( self::OBJECTS['https://example.com/objects/2'] ), @@ -126,7 +126,7 @@ class GetObjectControllerTest extends TestCase public function testItAllowsAccessToNoAudienceObject() { $request = Request::create( 'https://example.com/objects/3' ); - $response = $this->getObjectController->handle( $request ); + $response = $this->getController->handle( $request ); $this->assertNotNull( $response ); $this->assertEquals( json_encode( self::OBJECTS['https://example.com/objects/3'] ), @@ -138,7 +138,7 @@ class GetObjectControllerTest extends TestCase public function testItDisregardsQueryParams() { $request = Request::create( 'https://example.com/objects/1?foo=bar&baz=qux' ); - $response = $this->getObjectController->handle( $request ); + $response = $this->getController->handle( $request ); $this->assertNotNull( $response ); $this->assertEquals( json_encode( self::OBJECTS['https://example.com/objects/1'] ), diff --git a/test/Controllers/InboxControllerTest.php b/test/Controllers/InboxControllerTest.php deleted file mode 100644 index 363e8a6..0000000 --- a/test/Controllers/InboxControllerTest.php +++ /dev/null @@ -1,14 +0,0 @@ -assertTrue( false ); - } -} -?> diff --git a/test/Controllers/OutboxControllerTest.php b/test/Controllers/PostControllerTest.php similarity index 82% rename from test/Controllers/OutboxControllerTest.php rename to test/Controllers/PostControllerTest.php index 25b5aaa..a79c367 100644 --- a/test/Controllers/OutboxControllerTest.php +++ b/test/Controllers/PostControllerTest.php @@ -3,7 +3,7 @@ namespace ActivityPub\Test\Controllers; use PHPUnit\Framework\TestCase; -class OutboxControllerTest extends TestCase +class PostControllerTest extends TestCase { public function testOutboxController() { diff --git a/test/Http/ControllerResolverTest.php b/test/Http/ControllerResolverTest.php deleted file mode 100644 index 63209b3..0000000 --- a/test/Http/ControllerResolverTest.php +++ /dev/null @@ -1,130 +0,0 @@ -createMock( ObjectsService::class ); - $objectsService->method( 'query' )->will( - $this->returnCallback( function ( $query ) { - if ( array_key_exists( 'inbox', $query ) && - $query['inbox'] == self::INBOX_URI ) { - return array( TestUtils::objectFromArray( array( - 'id' => 'https://example.com/actor/1', - 'inbox' => array( - 'id' => 'https://example.com/actor/1/inbox', - ), - ) ) ); - } - if ( array_key_exists( 'outbox', $query ) && - $query['outbox'] == self::OUTBOX_URI ) { - return array( TestUtils::objectFromArray( array( - 'id' => 'https://example.com/actor/1', - 'outbox' => array( - 'id' => 'https://example.com/actor/1/outbox', - ), - ) ) ); - } - return array(); - }) - ); - $this->getObjectController = $this->createMock( GetObjectController::class ); - $this->inboxController = $this->createMock( InboxController::class ); - $this->outboxController = $this->createMock( OutboxController::class ); - $this->controllerResolver = new ControllerResolver( - $objectsService, - $this->getObjectController, - $this->inboxController, - $this->outboxController - ); - } - - private function createRequestWithBody( $uri, $method, $body ) - { - $json = json_encode( $body ); - return Request::create($uri, $method, array(), array(), array(), array(), $json); - } - - public function testItReturnsGetObjectController() - { - $request = Request::create( 'https://example.com/object', Request::METHOD_GET ); - $controller = $this->controllerResolver->getController( $request ); - $this->assertIsCallable( $controller ); - $this->assertEquals( array( $this->getObjectController, 'handle' ), $controller ); - } - - public function testItChecksForType() - { - $request = Request::create( 'https://example.com/inbox', Request::METHOD_POST ); - $this->expectException( BadRequestHttpException::class ); - $controller = $this->controllerResolver->getController( $request ); - } - - public function testItReturnsInboxController() - { - $request = $this->createRequestWithBody( - 'https://example.com/inbox', Request::METHOD_POST, array( 'type' => 'Foo' ) - ); - $controller = $this->controllerResolver->getController( $request ); - $this->assertTrue( $request->attributes->has( 'activity' ) ); - $this->assertEquals( - array( 'type' => 'Foo' ), $request->attributes->get( 'activity' ) - ); - $this->assertEquals( array( $this->inboxController, 'handle' ), $controller ); - } - - public function testItReturnsOutboxController() - { - $request = $this->createRequestWithBody( - 'https://example.com/outbox', Request::METHOD_POST, array( 'type' => 'Foo' ) - ); - $controller = $this->controllerResolver->getController( $request ); - $this->assertTrue( $request->attributes->has( 'activity' ) ); - $this->assertEquals( - array( 'type' => 'Foo' ), $request->attributes->get( 'activity' ) - ); - $this->assertEquals( array( $this->outboxController, 'handle' ), $controller ); - } - - public function testItDisallowsPostToInvalidUrl() - { - $request = $this->createRequestWithBody( - 'https://example.com/object', Request::METHOD_POST, array( 'type' => 'Foo' ) - ); - $this->expectException( NotFoundHttpException::class ); - $this->controllerResolver->getController( $request ); - } - - public function testItDisallowsNonGetPostMethods() - { - $request = $this->createRequestWithBody( - 'https://example.com/inbox', Request::METHOD_PUT, array( 'type' => 'Foo' ) - ); - $this->expectException( MethodNotAllowedHttpException::class ); - $this->controllerResolver->getController( $request ); - } -} -?> diff --git a/test/Http/RouterTest.php b/test/Http/RouterTest.php new file mode 100644 index 0000000..407f6df --- /dev/null +++ b/test/Http/RouterTest.php @@ -0,0 +1,14 @@ +assertTrue( false ); + } +} +?>