diff --git a/src/Activities/DeleteHandler.php b/src/Activities/DeleteHandler.php index b61ec08..00f3202 100644 --- a/src/Activities/DeleteHandler.php +++ b/src/Activities/DeleteHandler.php @@ -4,10 +4,13 @@ namespace ActivityPub\Activities; use ActivityPub\Activities\ActivityEvent; use ActivityPub\Activities\InboxActivityEvent; use ActivityPub\Activities\OutboxActivityEvent; +use ActivityPub\Objects\ObjectsService; use ActivityPub\Utils\DateTimeProvider; use DateTime; use Symfony\Component\EventDispatcher\EventSubscriberInterface; +use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; +use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException; class DeleteHandler implements EventSubscriberInterface { @@ -36,7 +39,7 @@ class DeleteHandler implements EventSubscriberInterface $this->objectsService = $objectsService; } - private function handleDelete( ActivityEvent $event ) + public function handleDelete( ActivityEvent $event ) { $activity = $event->getActivity(); if ( $activity['type'] !== 'Delete' ) { @@ -50,8 +53,14 @@ class DeleteHandler implements EventSubscriberInterface throw new BadRequestHttpException( 'Object must have an "id" field' ); } } + if ( ! $this->authorized( $event->getRequest(), $objectId ) ) { + throw new UnauthorizedHttpException( + 'Signature realm="ActivityPub",headers="(request-target) host date"' + ); + } $tombstone = array( '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => $objectId, 'type' => 'Tombstone', 'deleted' => $this->getNowTimestamp(), ); @@ -67,5 +76,22 @@ class DeleteHandler implements EventSubscriberInterface return $this->dateTimeProvider->getTime( 'activities.delete' ) ->format( DateTime::ISO8601 ); } + + public function authorized( Request $request, string $objectId ) + { + if ( ! $request->attributes->has( 'actor' ) ) { + return false; + } + $requestActor = $request->attributes->get( 'actor' ); + $object = $this->objectsService->dereference( $objectId ); + if ( ! $object || ! $object->hasField( 'attributedTo' ) ) { + return false; + } + $attributedActorId = $object['attributedTo']; + if ( ! is_string( $attributedActorId ) ) { + $attributedActorId = $attributedActorId['id']; + } + return $requestActor['id'] === $attributedActorId; + } } ?> diff --git a/test/Activities/DeleteHandlerTest.php b/test/Activities/DeleteHandlerTest.php new file mode 100644 index 0000000..2ae164c --- /dev/null +++ b/test/Activities/DeleteHandlerTest.php @@ -0,0 +1,172 @@ + array( + 'id' => 'https://elsewhere.com/objects/1', + 'type' => 'Note', + 'attributedTo' => 'https://elsewhere.com/actors/1', + ), + 'https://example.com/objects/1' => array( + 'id' => 'https://example.com/objects/1', + 'type' => 'Note', + 'attributedTo' => 'https://example.com/actors/1', + ) + ); + + public function testDeleteHandler() + { + $testCases = array( + array( + 'id' => 'basicInboxTest', + 'eventName' => InboxActivityEvent::NAME, + 'event' => new InboxActivityEvent( + array( + 'id' => 'https://elsewhere.com/activities/1', + 'type' => 'Delete', + 'object' => 'https://elsewhere.com/objects/1' + ), + TestActivityPubObject::fromArray( array( + 'id' => 'https://example.com/actor/1', + ) ), + self::requestWithAttributes( + 'https://example.com/inbox', + array( 'actor' => TestActivityPubObject::fromArray( array( + 'id' => 'https://elsewhere.com/actors/1', + ) ) ) + ) + ), + 'expectedTombstone' => array( + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://elsewhere.com/objects/1', + 'formerType' => 'Note', + 'type' => 'Tombstone', + 'deleted' => '2014-01-05T21:31:40+0000', + ), + ), + array( + 'id' => 'basicOutboxTest', + 'eventName' => OutboxActivityEvent::NAME, + 'event' => new OutboxActivityEvent( + array( + 'id' => 'https://example.com/activities/1', + 'type' => 'Delete', + 'object' => 'https://example.com/objects/1' + ), + TestActivityPubObject::fromArray( array( + 'id' => 'https://example.com/actor/1', + ) ), + self::requestWithAttributes( + 'https://example.com/outbox', + array( 'actor' => TestActivityPubObject::fromArray( array( + 'id' => 'https://example.com/actors/1', + ) ) ) + ) + ), + 'expectedTombstone' => array( + '@context' => 'https://www.w3.org/ns/activitystreams', + 'id' => 'https://example.com/objects/1', + 'formerType' => 'Note', + 'type' => 'Tombstone', + 'deleted' => '2014-01-05T21:31:40+0000', + ), + ), + array( + 'id' => 'outboxAuthTest', + 'eventName' => OutboxActivityEvent::NAME, + 'event' => new OutboxActivityEvent( + array( + 'id' => 'https://example.com/activities/1', + 'type' => 'Delete', + 'object' => 'https://example.com/objects/1' + ), + TestActivityPubObject::fromArray( array( + 'id' => 'https://example.com/actor/1', + ) ), + self::requestWithAttributes( + 'https://example.com/outbox', + array( 'actor' => TestActivityPubObject::fromArray( array( + 'id' => 'https://example.com/actors/2', + ) ) ) + ) + ), + 'expectedException' => UnauthorizedHttpException::class, + ), + array( + 'id' => 'inboxAuthTest', + 'eventName' => InboxActivityEvent::NAME, + 'event' => new InboxActivityEvent( + array( + 'id' => 'https://elsewhere.com/activities/1', + 'type' => 'Delete', + 'object' => 'https://elsewhere.com/objects/1' + ), + TestActivityPubObject::fromArray( array( + 'id' => 'https://example.com/actor/1', + ) ), + self::requestWithAttributes( + 'https://example.com/inbox', + array( 'actor' => TestActivityPubObject::fromArray( array( + 'id' => 'https://elsewhere.com/actors/2', + ) ) ) + ) + ), + 'expectedException' => UnauthorizedHttpException::class, + ), + ); + foreach ( $testCases as $testCase ) { + $eventDispatcher = new EventDispatcher(); + $dateTimeProvider = new TestDateTimeProvider( array( + 'activities.delete' => DateTime::createFromFormat( + DateTime::RFC2822, 'Sun, 05 Jan 2014 21:31:40 GMT' + ), + ) ); + $objectsService = $this->getMockBuilder( ObjectsService::class ) + ->disableOriginalConstructor() + ->setMethods( array( 'dereference', 'replace' ) ) + ->getMock(); + $objectsService->method( 'dereference' )->will( $this->returnCallback( + function( $id ) { + if ( array_key_exists( $id, self::OBJECTS ) ) { + return TestActivityPubObject::fromArray( self::OBJECTS[$id] ); + } + } + ) ); + if ( array_key_exists( 'expectedException', $testCase ) ) { + $this->expectException( $testCase['expectedException'] ); + } else { + $objectsService->expects( $this->once() ) + ->method( 'replace' ) + ->with( + $this->anything(), + $this->equalTo( $testCase['expectedTombstone'] ) + ); + } + $deleteHandler = new DeleteHandler( $dateTimeProvider, $objectsService ); + $eventDispatcher->addSubscriber( $deleteHandler ); + $eventDispatcher->dispatch( $testCase['eventName'], $testCase['event'] ); + } + } + + public static function requestWithAttributes( $uri, $attributes ) + { + $request = Request::create( $uri ); + $request->attributes->add( $attributes ); + return $request; + } +} +?>