From 23b993f05d2f3bb3dfdc19c1c5a7a9b72fb9c7a8 Mon Sep 17 00:00:00 2001 From: Jeremy Dormitzer Date: Mon, 28 Jan 2019 21:24:51 -0500 Subject: [PATCH] Implement collection normalizing --- src/Config/ActivityPubModule.php | 3 +- src/Objects/CollectionsService.php | 85 +++++++++++++++- test/Controllers/GetControllerTest.php | 4 +- test/Objects/CollectionsServiceTest.php | 124 +++++++++++++++++++++++- 4 files changed, 209 insertions(+), 7 deletions(-) diff --git a/src/Config/ActivityPubModule.php b/src/Config/ActivityPubModule.php index c5eeeb7..58f31a9 100644 --- a/src/Config/ActivityPubModule.php +++ b/src/Config/ActivityPubModule.php @@ -82,7 +82,8 @@ class ActivityPubModule $this->injector->register( CollectionsService::class, CollectionsService::class ) ->addArgument( self::COLLECTION_PAGE_SIZE ) ->addArgument( new Reference( AuthService::class ) ) - ->addArgument( new Reference( ContextProvider::class ) ); + ->addArgument( new Reference( ContextProvider::class ) ) + ->addArgument( new Reference( Client::class ) ); $this->injector->register( RandomProvider::class, RandomProvider::class ); diff --git a/src/Objects/CollectionsService.php b/src/Objects/CollectionsService.php index 113e6e5..8b55c29 100644 --- a/src/Objects/CollectionsService.php +++ b/src/Objects/CollectionsService.php @@ -4,7 +4,10 @@ namespace ActivityPub\Objects; use ActivityPub\Auth\AuthService; use ActivityPub\Entities\ActivityPubObject; use ActivityPub\Objects\ContextProvider; +use GuzzleHttp\Client; +use GuzzleHttp\Psr7\Request as Psr7Request; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; class CollectionsService @@ -24,12 +27,20 @@ class CollectionsService */ private $contextProvider; - public function __construct( int $pageSize, AuthService $authService, - ContextProvider $contextProvider ) + /** + * @var Client + */ + private $httpClient; + + public function __construct( int $pageSize, + AuthService $authService, + ContextProvider $contextProvider, + Client $httpClient ) { $this->pageSize = $pageSize; $this->authService = $authService; $this->contextProvider = $contextProvider; + $this->httpClient = $httpClient; } /** @@ -63,6 +74,73 @@ class CollectionsService return $colArr; } + /** + * Given a collection as an array, normalize the collection by collapsing + * collection pages into a single `items` or `orderedItems` array + * + * @param array $collection The collection to normalize + * @return array The normalized collection + */ + public function normalizeCollection( array $collection ) + { + if ( $collection['type'] !== 'Collection' && + $collection['type'] !== 'OrderedCollection' ) { + return $collection; + } + if ( ! array_key_exists( 'first', $collection ) ) { + return $collection; + } + $first = $collection['first']; + if ( is_string( $first ) ) { + $first = $this->fetchPage( $first ); + if ( ! $first ) { + throw new BadRequestHttpException( + "Unable to retrieve collection page '$first'" + ); + } + } + $items = $this->getPageItems( $collection['first'] ); + $itemsField = $collection['type'] === 'Collection' ? 'items' : 'orderedItems'; + $collection[$itemsField] = $items; + unset( $collection['first'] ); + if ( array_key_exists( 'last', $collection ) ) { + unset( $collection['last'] ); + } + return $collection; + } + + private function getPageItems( array $collectionPage ) + { + $items = array(); + if ( array_key_exists( 'items', $collectionPage ) ) { + $items = array_merge( $items, $collectionPage['items'] ); + } else if ( array_key_exists( 'orderedItems', $collectionPage ) ) { + $items = array_merge( $items, $collectionPage['orderedItems'] ); + } + if ( array_key_exists( 'next', $collectionPage ) ) { + $nextPage = $collectionPage['next']; + if ( is_string( $nextPage ) ) { + $nextPage = $this->fetchPage( $nextPage ); + } + if ( $nextPage ) { + $items = array_merge( $items, $this->getPageItems( $nextPage ) ); + } + } + return $items; + } + + private function fetchPage( string $pageId ) + { + $request = new Psr7Request( 'GET', $pageId, array( + 'Accept' => 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' + ) ); + $response = $this->httpClient->send( $request ); + if ( $response->getStatusCode() !== 200 || empty( $response->getBody() ) ) { + return; + } + return json_decode( $response->getBody(), true ); + } + private function getCollectionPage( ActivityPubObject $collection, Request $request, int $offset, @@ -77,7 +155,7 @@ class CollectionsService } if ( ! $collection->hasField( $itemsKey ) ) { throw new InvalidArgumentException( - "Collection does not have an \"$field\" key" + "Collection does not have an \"$itemsKey\" key" ); } $collectionItems = $collection->getFieldValue( $itemsKey ); @@ -108,6 +186,7 @@ class CollectionsService $itemsKey => $pageItems, 'partOf' => $collection['id'], ); + // TODO set 'first' and 'last' on the page $nextIdx = $this->hasNextItem( $request, $collectionItems, $idx ); if ( $nextIdx ) { $page['next'] = $collection['id'] . "?offset=$nextIdx"; diff --git a/test/Controllers/GetControllerTest.php b/test/Controllers/GetControllerTest.php index dda226d..3e69d65 100644 --- a/test/Controllers/GetControllerTest.php +++ b/test/Controllers/GetControllerTest.php @@ -9,6 +9,7 @@ use ActivityPub\Objects\ContextProvider; use ActivityPub\Objects\CollectionsService; use ActivityPub\Objects\ObjectsService; use ActivityPub\Test\TestUtils\TestActivityPubObject; +use GuzzleHttp\Client; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -65,7 +66,8 @@ class GetControllerTest extends TestCase ); $authService = new AuthService(); $contextProvider = new ContextProvider(); - $collectionsService = new CollectionsService( 4, $authService, $contextProvider ); + $httpClient = $this->createMock( Client::class ); + $collectionsService = new CollectionsService( 4, $authService, $contextProvider, $httpClient ); $this->getController = new GetController( $objectsService, $collectionsService, $authService ); diff --git a/test/Objects/CollectionsServiceTest.php b/test/Objects/CollectionsServiceTest.php index c66e899..6de722e 100644 --- a/test/Objects/CollectionsServiceTest.php +++ b/test/Objects/CollectionsServiceTest.php @@ -6,6 +6,8 @@ use ActivityPub\Auth\AuthService; use ActivityPub\Objects\ContextProvider; use ActivityPub\Objects\CollectionsService; use ActivityPub\Test\TestUtils\TestActivityPubObject; +use GuzzleHttp\Client; +use GuzzleHttp\Psr7\Response as Psr7Response; use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; @@ -18,12 +20,22 @@ class CollectionsServiceTest extends TestCase { $authService = new AuthService(); $contextProvider = new ContextProvider(); + $httpClient = $this->createMock( Client::class ); + $httpClient->method( 'send' )->willReturn( + new Psr7Response( 200, array(), json_encode( array( + 'type' => 'OrderedCollectionPage', + 'orderedItems' => array( + 'item3', + 'item4', + ), + ) ) ) + ); $this->collectionsService = new CollectionsService( - 4, $authService, $contextProvider + 4, $authService, $contextProvider, $httpClient ); } - public function testCollectionsService() + public function testCollectionPaging() { $testCases = array( array( @@ -378,5 +390,113 @@ class CollectionsServiceTest extends TestCase ); } } + + public function testCollectionNormalizing() + { + $testCases = array( + array( + 'id' => 'basicNormalizingTest', + 'collection' => array( + 'type' => 'Collection', + 'first' => array( + 'type' => 'CollectionPage', + 'items' => array( + 'item1', + 'item2', + ), + ), + ), + 'expectedResult' => array( + 'type' => 'Collection', + 'items' => array( + 'item1', + 'item2', + ), + ), + ), + array( + 'id' => 'orderedNormalizingTest', + 'collection' => array( + 'type' => 'OrderedCollection', + 'first' => array( + 'type' => 'OrderedCollectionPage', + 'orderedItems' => array( + 'item1', + 'item2', + ), + ), + ), + 'expectedResult' => array( + 'type' => 'OrderedCollection', + 'orderedItems' => array( + 'item1', + 'item2', + ), + ), + ), + array( + 'id' => 'pageTraversal', + 'collection' => array( + 'type' => 'OrderedCollection', + 'first' => array( + 'type' => 'OrderedCollectionPage', + 'orderedItems' => array( + 'item1', + 'item2', + ), + 'next' => array( + 'type' => 'OrderedCollectionPage', + 'orderedItems' => array( + 'item3', + 'item4', + ), + ), + ), + ), + 'expectedResult' => array( + 'type' => 'OrderedCollection', + 'orderedItems' => array( + 'item1', + 'item2', + 'item3', + 'item4', + ), + ), + ), + array( + 'id' => 'pageTraversal', + 'collection' => array( + 'type' => 'OrderedCollection', + 'first' => array( + 'type' => 'OrderedCollectionPage', + 'orderedItems' => array( + 'item1', + 'item2', + ), + 'next' => 'https://example.com/collection/1?page=2', + ), + ), + 'expectedResult' => array( + 'type' => 'OrderedCollection', + 'orderedItems' => array( + 'item1', + 'item2', + 'item3', + 'item4', + ), + ), + ), + ); + foreach ( $testCases as $testCase ) { + $collection = $testCase['collection']; + if ( array_key_exists( 'expectedException', $testCase ) ) { + $this->expectException( $testCase['expectedException'] ); + } + $actual = $this->collectionsService->normalizeCollection( $collection ); + $this->assertEquals( + $testCase['expectedResult'], $actual, "Error on test $testCase[id]" + ); + } + } } ?>