diff --git a/src/Objects/CollectionsService.php b/src/Objects/CollectionsService.php index db7bd2b..1832293 100644 --- a/src/Objects/CollectionsService.php +++ b/src/Objects/CollectionsService.php @@ -87,9 +87,18 @@ class CollectionsService ActivityPubObject $collection, Closure $filterFunc ) { + $sort = 'desc'; + if ( $request->query->has( 'sort' ) && $request->query->get( 'sort' ) == 'asc' ) { + $sort = 'asc'; + } if ( $request->query->has( 'offset' ) ) { return $this->getCollectionPage( - $collection, $request, intval( $request->query->get( 'offset' ) ), $this->pageSize, $filterFunc + $collection, + $request, + intval( $request->query->get( 'offset' ) ), + $this->pageSize, + $filterFunc, + $sort ); } $colArr = array(); @@ -103,7 +112,7 @@ class CollectionsService } } $firstPage = $this->getCollectionPage( - $collection, $request, 0, $this->pageSize, $filterFunc + $collection, $request, 0, $this->pageSize, $filterFunc, $sort ); $colArr['first'] = $firstPage; return $colArr; @@ -113,8 +122,10 @@ class CollectionsService Request $request, $offset, $pageSize, - Closure $filterFunc ) + Closure $filterFunc, + $sort ) { + $asc = $sort == 'asc'; $itemsKey = 'items'; $pageType = 'CollectionPage'; $isOrdered = $this->isOrdered( $collection ); @@ -129,7 +140,11 @@ class CollectionsService } $collectionItems = $collection->getFieldValue( $itemsKey ); $pageItems = array(); - $idx = $offset; + if ( $asc ) { + $idx = $offset; + } else { + $idx = $this->getCollectionSize( $collection ) - $offset - 1; + } $count = 0; while ( $count < $pageSize ) { $item = $collectionItems->getFieldValue( $idx ); @@ -143,22 +158,29 @@ class CollectionsService $pageItems[] = $item->asArray( 1 ); $count++; } - $idx++; + if ( $asc ) { + $idx++; + } else { + $idx--; + } } if ( $count === 0 ) { throw new NotFoundHttpException(); } $page = array( '@context' => $this->contextProvider->getContext(), - 'id' => $collection['id'] . "?offset=$offset", + 'id' => $collection['id'] . "?offset=$offset&sort=$sort", 'type' => $pageType, $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"; + $nextIdx = $this->hasNextItem( $request, $collectionItems, $idx, $sort ); + if ( is_numeric( $nextIdx ) ) { + if ( ! $asc ) { + $nextIdx = $this->getCollectionSize( $collection ) - $nextIdx - 1; + } + $page['next'] = $collection['id'] . "?offset=$nextIdx&sort=$sort"; } if ( $isOrdered ) { $page['startIndex'] = $offset; @@ -179,15 +201,20 @@ class CollectionsService } } - private function hasNextItem( Request $request, ActivityPubObject $collectionItems, $idx ) + private function hasNextItem( Request $request, ActivityPubObject $collectionItems, $idx, $sort ) { + $asc = $sort == 'asc'; $next = $collectionItems->getFieldValue( $idx ); while ( $next ) { if ( is_string( $next ) || $this->authService->isAuthorized( $request, $next ) ) { return $idx; } - $idx++; + if ( $asc ) { + $idx++; + } else { + $idx--; + } $next = $collectionItems->getFieldValue( $idx ); } return false; @@ -383,5 +410,31 @@ class CollectionsService $this->entityManager->persist( $collection ); $this->entityManager->flush(); } + + public function getCollectionSize( ActivityPubObject &$collection ) + { + if ( $collection->hasField( 'totalItems' ) && is_numeric( $collection['totalItems'] ) ) { + return intval( $collection['totalItems'] ); + } else { + $itemsField = 'items'; + if ( $collection->hasField( 'type' ) && $collection['type'] == 'OrderedCollection' ) { + $itemsField = 'orderedItems'; + } + if ( ! ( $collection->hasField( $itemsField ) && $collection[$itemsField] instanceof ActivityPubObject ) ) { + return 0; + } + $items = $collection[$itemsField]; + $count = 0; + $idx = 0; + $currentItem = $items[$idx]; + while ( $currentItem ) { + $count++; + $idx++; + $currentItem = $items[$idx]; + } + $collection = $this->objectsService->update( $collection['id'], array( 'totalItems' => strval( $count ) ) ); + return $count; + } + } } diff --git a/test/Controllers/GetControllerTest.php b/test/Controllers/GetControllerTest.php index f3386aa..842f89c 100644 --- a/test/Controllers/GetControllerTest.php +++ b/test/Controllers/GetControllerTest.php @@ -42,6 +42,14 @@ class GetControllerTest extends APTestCase return null; } ) ); + $objectsService->method( 'update' )->will( + $this->returnCallback( function ( $uri ) { + if ( array_key_exists( $uri, $this->objects ) ) { + return $this->objects[$uri]; + } + return null; + } ) + ); $authService = new AuthService(); $contextProvider = new ContextProvider(); $httpClient = $this->getMock( Client::class ); @@ -235,7 +243,7 @@ class GetControllerTest extends APTestCase 'https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', ), - 'id' => 'https://example.com/actors/1/inbox?offset=0', + 'id' => 'https://example.com/actors/1/inbox?offset=0&sort=desc', 'type' => 'OrderedCollectionPage', 'orderedItems' => array( array( diff --git a/test/Objects/CollectionsServiceTest.php b/test/Objects/CollectionsServiceTest.php index a5c8dca..0da494a 100644 --- a/test/Objects/CollectionsServiceTest.php +++ b/test/Objects/CollectionsServiceTest.php @@ -95,19 +95,19 @@ class CollectionsServiceTest extends APTestCase 'https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', ), - 'id' => 'https://example.com/objects/1?offset=0', + 'id' => 'https://example.com/objects/1?offset=0&sort=desc', 'type' => 'OrderedCollectionPage', 'partOf' => 'https://example.com/objects/1', 'startIndex' => 0, 'orderedItems' => array( array( - 'id' => 'https://example.com/objects/2', + 'id' => 'https://example.com/objects/4', ), array( 'id' => 'https://example.com/objects/3', ), array( - 'id' => 'https://example.com/objects/4', + 'id' => 'https://example.com/objects/2', ), ), ), @@ -156,23 +156,23 @@ class CollectionsServiceTest extends APTestCase 'https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', ), - 'id' => 'https://example.com/objects/1?offset=0', + 'id' => 'https://example.com/objects/1?offset=0&sort=desc', 'type' => 'OrderedCollectionPage', 'partOf' => 'https://example.com/objects/1', 'startIndex' => 0, - 'next' => 'https://example.com/objects/1?offset=4', + 'next' => 'https://example.com/objects/1?offset=4&sort=desc', 'orderedItems' => array( array( - 'id' => 'https://example.com/objects/2', + 'id' => 'https://example.com/objects/6', ), array( - 'id' => 'https://example.com/objects/3', + 'id' => 'https://example.com/objects/5', ), array( 'id' => 'https://example.com/objects/4', ), array( - 'id' => 'https://example.com/objects/5', + 'id' => 'https://example.com/objects/3', ), ), ), @@ -214,46 +214,19 @@ class CollectionsServiceTest extends APTestCase 'https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', ), - 'id' => 'https://example.com/objects/1?offset=3', + 'id' => 'https://example.com/objects/1?offset=3&sort=desc', 'type' => 'OrderedCollectionPage', 'partOf' => 'https://example.com/objects/1', 'startIndex' => 3, 'orderedItems' => array( - array( - 'id' => 'https://example.com/objects/5', - ), - array( - 'id' => 'https://example.com/objects/6', - ), - ), - ), - ), - array( - 'id' => 'nonExistentPage', - 'collection' => array( - '@context' => array( - 'https://www.w3.org/ns/activitystreams', - 'https://w3id.org/security/v1', - ), - 'id' => 'https://example.com/objects/1', - 'type' => 'OrderedCollection', - 'orderedItems' => array( - array( - 'id' => 'https://example.com/objects/2', - ), array( 'id' => 'https://example.com/objects/3', ), array( - 'id' => 'https://example.com/objects/4', + 'id' => 'https://example.com/objects/2', ), - ) + ), ), - 'request' => Request::create( - 'https://example.com/objects/1?offset=3', - Request::METHOD_GET - ), - 'expectedException' => NotFoundHttpException::class, ), array( 'id' => 'authFilteringPublic', @@ -301,20 +274,20 @@ class CollectionsServiceTest extends APTestCase 'https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', ), - 'id' => 'https://example.com/objects/1?offset=0', + 'id' => 'https://example.com/objects/1?offset=0&sort=desc', 'type' => 'OrderedCollectionPage', 'partOf' => 'https://example.com/objects/1', 'startIndex' => 0, 'orderedItems' => array( array( - 'id' => 'https://example.com/objects/3', + 'id' => 'https://example.com/objects/5', ), array( 'id' => 'https://example.com/objects/4', 'to' => 'https://www.w3.org/ns/activitystreams#Public', ), array( - 'id' => 'https://example.com/objects/5', + 'id' => 'https://example.com/objects/3', ), ), ), @@ -369,7 +342,144 @@ class CollectionsServiceTest extends APTestCase 'https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1', ), - 'id' => 'https://example.com/objects/1?offset=0', + 'id' => 'https://example.com/objects/1?offset=0&sort=desc', + 'type' => 'OrderedCollectionPage', + 'partOf' => 'https://example.com/objects/1', + 'startIndex' => 0, + 'orderedItems' => array( + array( + 'id' => 'https://example.com/objects/6', + 'to' => 'https://example.com/actors/2', + ), + array( + 'id' => 'https://example.com/objects/5', + ), + array( + 'id' => 'https://example.com/objects/4', + 'to' => 'https://www.w3.org/ns/activitystreams#Public', + ), + array( + 'id' => 'https://example.com/objects/3', + ), + ), + ), + ), + ), + array( + 'id' => 'sortAsc', + 'collection' => array( + '@context' => array( + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ), + 'id' => 'https://example.com/objects/1', + 'type' => 'OrderedCollection', + 'orderedItems' => array( + array( + 'id' => 'https://example.com/objects/2', + ), + array( + 'id' => 'https://example.com/objects/3', + ), + array( + 'id' => 'https://example.com/objects/4', + ), + array( + 'id' => 'https://example.com/objects/5', + ), + array( + 'id' => 'https://example.com/objects/6', + ), + ) + ), + 'request' => Request::create( + 'https://example.com/objects/1?sort=asc', + Request::METHOD_GET + ), + 'expectedResult' => array( + '@context' => array( + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ), + 'id' => 'https://example.com/objects/1', + 'type' => 'OrderedCollection', + 'first' => array( + '@context' => array( + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ), + 'id' => 'https://example.com/objects/1?offset=0&sort=asc', + 'type' => 'OrderedCollectionPage', + 'partOf' => 'https://example.com/objects/1', + 'startIndex' => 0, + 'next' => 'https://example.com/objects/1?offset=4&sort=asc', + 'orderedItems' => array( + array( + 'id' => 'https://example.com/objects/2', + ), + array( + 'id' => 'https://example.com/objects/3', + ), + array( + 'id' => 'https://example.com/objects/4', + ), + array( + 'id' => 'https://example.com/objects/5', + ), + ), + ), + ), + ), + array( + 'id' => 'authFilteringSpecificActorSortAsc', + 'collection' => array( + '@context' => array( + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ), + 'id' => 'https://example.com/objects/1', + 'type' => 'OrderedCollection', + 'orderedItems' => array( + array( + 'id' => 'https://example.com/objects/2', + 'to' => 'https://example.com/actors/1', + ), + array( + 'id' => 'https://example.com/objects/3', + ), + array( + 'id' => 'https://example.com/objects/4', + 'to' => 'https://www.w3.org/ns/activitystreams#Public', + ), + array( + 'id' => 'https://example.com/objects/5', + ), + array( + 'id' => 'https://example.com/objects/6', + 'to' => 'https://example.com/actors/2', + ), + ) + ), + 'request' => Request::create( + 'https://example.com/objects/1?sort=asc', + Request::METHOD_GET + ), + 'requestAttributes' => array( + 'actor' => 'https://example.com/actors/2', + ), + 'expectedResult' => array( + '@context' => array( + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ), + 'id' => 'https://example.com/objects/1', + 'type' => 'OrderedCollection', + 'first' => array( + '@context' => array( + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ), + 'id' => 'https://example.com/objects/1?offset=0&sort=asc', 'type' => 'OrderedCollectionPage', 'partOf' => 'https://example.com/objects/1', 'startIndex' => 0, @@ -392,8 +502,60 @@ class CollectionsServiceTest extends APTestCase ), ), ), + array( + 'id' => 'nonExistentPage', + 'collection' => array( + '@context' => array( + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ), + 'id' => 'https://example.com/objects/1', + 'type' => 'OrderedCollection', + 'orderedItems' => array( + array( + 'id' => 'https://example.com/objects/2', + ), + array( + 'id' => 'https://example.com/objects/3', + ), + array( + 'id' => 'https://example.com/objects/4', + ), + ) + ), + 'request' => Request::create( + 'https://example.com/objects/1?offset=3', + Request::METHOD_GET + ), + 'expectedException' => NotFoundHttpException::class, + ), ); foreach ( $testCases as $testCase ) { + $this->authService = new AuthService(); + $contextProvider = new ContextProvider(); + $httpClient = $this->getMock( Client::class ); + $httpClient->method( 'send' )->willReturn( + new Psr7Response( 200, array(), json_encode( array( + 'type' => 'OrderedCollectionPage', + 'orderedItems' => array( + 'item3', + 'item4', + ), + ) ) ) + ); + $entityManager = $this->getMock( EntityManager::class ); + $collection = $testCase['collection']; + $objectsService = $this->getMock( ObjectsService::class ); + $objectsService->method( 'update' )->willReturn( TestActivityPubObject::fromArray( $collection ) ); + $this->collectionsService = new CollectionsService( + 4, + $this->authService, + $contextProvider, + $httpClient, + new SimpleDateTimeProvider(), + $entityManager, + $objectsService + ); if ( array_key_exists( 'expectedException', $testCase ) ) { $this->setExpectedException( $testCase['expectedException'] ); }