Reformat all code

This commit is contained in:
Jeremy Dormitzer 2019-02-16 12:51:24 -05:00
parent 9aab13701c
commit 510df450e9
63 changed files with 4554 additions and 4494 deletions

6114
composer.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Activities;
use ActivityPub\Objects\CollectionsService;
@ -16,14 +17,6 @@ class AcceptHandler implements EventSubscriberInterface
* @var CollectionsService
*/
private $collectionsService;
public static function getSubscribedEvents()
{
return array(
InboxActivityEvent::NAME => 'handleInbox',
OutboxActivityEvent::NAME => 'handleOutbox',
);
}
public function __construct( ObjectsService $objectsService,
CollectionsService $collectionsService )
@ -32,6 +25,14 @@ class AcceptHandler implements EventSubscriberInterface
$this->collectionsService = $collectionsService;
}
public static function getSubscribedEvents()
{
return array(
InboxActivityEvent::NAME => 'handleInbox',
OutboxActivityEvent::NAME => 'handleOutbox',
);
}
public function handleInbox( InboxActivityEvent $event )
{
$activity = $event->getActivity();
@ -54,17 +55,17 @@ class AcceptHandler implements EventSubscriberInterface
// or there isn't, in which case this is an ordinary Accept
// sent by a client and the Follow is in the database
$follow = $request->attributes->get( 'follow' );
if ( ! $follow ) {
if ( !$follow ) {
$followId = $activity['object'];
if ( is_array( $followId ) && array_key_exists( 'id', $followId ) ) {
$followId = $followId['id'];
}
if ( ! is_string( $followId ) ) {
if ( !is_string( $followId ) ) {
return;
}
$follow = $this->objectsService->dereference( $followId )->asArray( -1 );
}
if ( ! $follow || ! array_key_exists( 'object', $follow ) ) {
if ( !$follow || !array_key_exists( 'object', $follow ) ) {
return;
}
$followObjectId = $follow['object'];

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Activities;
use ActivityPub\Entities\ActivityPubObject;
@ -37,7 +38,7 @@ class ActivityEvent extends Event
protected $response;
public function __construct( array $activity, ActivityPubObject $actor,
Request $request )
Request $request )
{
$this->activity = $activity;
$this->actor = $actor;

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Activities;
use ActivityPub\Objects\CollectionsService;
@ -23,14 +24,6 @@ class CreateHandler implements EventSubscriberInterface
*/
private $collectionsService;
public static function getSubscribedEvents()
{
return array(
InboxActivityEvent::NAME => 'handleInbox',
OutboxActivityEvent::NAME => 'handleOutbox',
);
}
public function __construct( ObjectsService $objectsService,
IdProvider $idProvider,
CollectionsService $collectionsService )
@ -39,6 +32,15 @@ class CreateHandler implements EventSubscriberInterface
$this->idProvider = $idProvider;
$this->collectionsService = $collectionsService;
}
public static function getSubscribedEvents()
{
return array(
InboxActivityEvent::NAME => 'handleInbox',
OutboxActivityEvent::NAME => 'handleOutbox',
);
}
public function handleInbox( InboxActivityEvent $event )
{
$activity = $event->getActivity();
@ -61,7 +63,7 @@ class CreateHandler implements EventSubscriberInterface
return;
}
$object = $activity['object'];
if ( ! array_key_exists( 'id', $object ) ) {
if ( !array_key_exists( 'id', $object ) ) {
$object['id'] = $this->idProvider->getId(
$event->getRequest(),
strtolower( $object['type'] )
@ -94,31 +96,31 @@ class CreateHandler implements EventSubscriberInterface
private function copyFields( array $fields, array $sourceObj, array $targetObj )
{
foreach( $fields as $field ) {
if ( ! array_key_exists( $field, $sourceObj ) ) {
foreach ( $fields as $field ) {
if ( !array_key_exists( $field, $sourceObj ) ) {
continue;
}
if ( array_key_exists( $field, $targetObj ) &&
$sourceObj[$field] === $targetObj[$field] ) {
$sourceObj[$field] === $targetObj[$field] ) {
continue;
} else if ( ! array_key_exists( $field, $targetObj ) ) {
} else if ( !array_key_exists( $field, $targetObj ) ) {
$targetObj[$field] = $sourceObj[$field];
} else if ( is_array( $sourceObj[$field] ) &&
is_array( $targetObj[$field] ) ) {
is_array( $targetObj[$field] ) ) {
$targetObj[$field] = array_unique(
array_merge( $sourceObj[$field], $targetObj[$field] )
);
} else if ( is_array( $sourceObj[$field] ) &&
! is_array( $targetObj[$field] ) ) {
!is_array( $targetObj[$field] ) ) {
$targetObj[$field] = array( $targetObj[$field] );
$targetObj[$field] = array_unique(
array_merge( $sourceObj[$field], $targetObj[$field] )
);
} else if ( ! is_array( $sourceObj[$field] ) &&
is_array( $targetObj[$field] ) ) {
} else if ( !is_array( $sourceObj[$field] ) &&
is_array( $targetObj[$field] ) ) {
$targetObj[$field][] = $sourceObj[$field];
} else if ( ! is_array( $sourceObj[$field] ) &&
! is_array( $targetObj[$field] ) ) {
} else if ( !is_array( $sourceObj[$field] ) &&
!is_array( $targetObj[$field] ) ) {
$targetObj[$field] = array( $targetObj[$field] );
$targetObj[$field][] = $sourceObj[$field];
}

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Activities;
use ActivityPub\Objects\ObjectsService;
@ -21,6 +22,13 @@ class DeleteHandler implements EventSubscriberInterface
*/
private $objectsService;
public function __construct( DateTimeProvider $dateTimeProvider,
ObjectsService $objectsService )
{
$this->dateTimeProvider = $dateTimeProvider;
$this->objectsService = $objectsService;
}
public static function getSubscribedEvents()
{
return array(
@ -29,13 +37,6 @@ class DeleteHandler implements EventSubscriberInterface
);
}
public function __construct( DateTimeProvider $dateTimeProvider,
ObjectsService $objectsService )
{
$this->dateTimeProvider = $dateTimeProvider;
$this->objectsService = $objectsService;
}
public function handleDelete( ActivityEvent $event )
{
$activity = $event->getActivity();
@ -43,14 +44,14 @@ class DeleteHandler implements EventSubscriberInterface
return;
}
$objectId = $activity['object'];
if ( ! is_string( $objectId ) ) {
if ( !is_string( $objectId ) ) {
if ( is_array( $objectId ) && array_key_exists( 'id', $objectId ) ) {
$objectId = $objectId['id'];
} else {
throw new BadRequestHttpException( 'Object must have an "id" field' );
}
}
if ( ! $this->authorized( $event->getRequest(), $objectId ) ) {
if ( !$this->authorized( $event->getRequest(), $objectId ) ) {
throw new UnauthorizedHttpException(
'Signature realm="ActivityPub",headers="(request-target) host date"'
);
@ -68,27 +69,27 @@ class DeleteHandler implements EventSubscriberInterface
$this->objectsService->replace( $objectId, $tombstone );
}
public function authorized( Request $request, $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;
}
private function getNowTimestamp()
{
return $this->dateTimeProvider->getTime( 'activities.delete' )
->format( DateTime::ISO8601 );
}
public function authorized( Request $request, $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;
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Activities;
use ActivityPub\Objects\ContextProvider;
@ -18,13 +19,6 @@ class FollowHandler implements EventSubscriberInterface
*/
private $contextProvider;
public static function getSubscribedEvents()
{
return array(
InboxActivityEvent::NAME => 'handleInbox',
);
}
public function __construct( $autoAccepts,
ContextProvider $contextProvider )
{
@ -32,14 +26,20 @@ class FollowHandler implements EventSubscriberInterface
$this->contextProvider = $contextProvider;
}
public static function getSubscribedEvents()
{
return array(
InboxActivityEvent::NAME => 'handleInbox',
);
}
public function handleInbox(InboxActivityEvent $event,
/** @noinspection PhpUnusedParameterInspection */
$eventName,
EventDispatcher $eventDispatcher )
public function handleInbox( InboxActivityEvent $event,
/** @noinspection PhpUnusedParameterInspection */
$eventName,
EventDispatcher $eventDispatcher )
{
$activity = $event->getActivity();
if ( ! $activity['type'] === 'Follow' ) {
if ( !$activity['type'] === 'Follow' ) {
return;
}
if ( $this->autoAccepts ) {

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Activities;
class InboxActivityEvent extends ActivityEvent

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Activities;
use ActivityPub\Entities\ActivityPubObject;
@ -16,19 +17,11 @@ class NonActivityHandler implements EventSubscriberInterface
*/
private $contextProvider;
public static function activityTypes()
public function __construct( ContextProvider $contextProvider )
{
return array(
'Accept', 'Add', 'Announce', 'Arrive',
'Block', 'Create', 'Delete', 'Dislike',
'Flag', 'Follow', 'Ignore', 'Invite',
'Join', 'Leave', 'Like', 'Listen',
'Move', 'Offer', 'Question', 'Reject',
'Read', 'Remove', 'TentativeReject', 'TentativeAccept',
'Travel', 'Undo', 'Update', 'View',
);
$this->contextProvider = $contextProvider;
}
public static function getSubscribedEvents()
{
return array(
@ -36,11 +29,6 @@ class NonActivityHandler implements EventSubscriberInterface
);
}
public function __construct( ContextProvider $contextProvider )
{
$this->contextProvider = $contextProvider;
}
public function handle( OutboxActivityEvent $event )
{
$object = $event->getActivity();
@ -52,6 +40,19 @@ class NonActivityHandler implements EventSubscriberInterface
$event->setActivity( $create );
}
public static function activityTypes()
{
return array(
'Accept', 'Add', 'Announce', 'Arrive',
'Block', 'Create', 'Delete', 'Dislike',
'Flag', 'Follow', 'Ignore', 'Invite',
'Join', 'Leave', 'Like', 'Listen',
'Move', 'Offer', 'Question', 'Reject',
'Read', 'Remove', 'TentativeReject', 'TentativeAccept',
'Travel', 'Undo', 'Update', 'View',
);
}
/**
* Makes a new Create activity with $object as the object
*

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Activities;
class OutboxActivityEvent extends ActivityEvent

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Activities;
use ActivityPub\Objects\ObjectsService;
@ -14,6 +15,11 @@ class UpdateHandler implements EventSubscriberInterface
*/
private $objectsService;
public function __construct( ObjectsService $objectsService )
{
$this->objectsService = $objectsService;
}
public static function getSubscribedEvents()
{
return array(
@ -22,11 +28,6 @@ class UpdateHandler implements EventSubscriberInterface
);
}
public function __construct( ObjectsService $objectsService )
{
$this->objectsService = $objectsService;
}
public function handleInbox( InboxActivityEvent $event )
{
$activity = $event->getActivity();
@ -34,10 +35,10 @@ class UpdateHandler implements EventSubscriberInterface
return;
}
$object = $activity['object'];
if ( ! array_key_exists( 'id', $object ) ) {
if ( !array_key_exists( 'id', $object ) ) {
throw new BadRequestHttpException( 'Update object has no "id" field' );
}
if ( ! $this->authorized( $event->getRequest(), $object ) ) {
if ( !$this->authorized( $event->getRequest(), $object ) ) {
throw new UnauthorizedHttpException(
'Signature realm="ActivityPub",headers="(request-target) host date"'
);
@ -45,26 +46,6 @@ class UpdateHandler implements EventSubscriberInterface
$this->objectsService->replace( $object['id'], $object );
}
public function handleOutbox( OutboxActivityEvent $event )
{
$activity = $event->getActivity();
if ( $activity['type'] !== 'Update' ) {
return;
}
$updateFields = $activity['object'];
if ( ! array_key_exists( 'id', $updateFields ) ) {
throw new BadRequestHttpException( 'Update object has no "id" field' );
}
if ( ! $this->authorized( $event->getRequest(), $updateFields ) ) {
throw new UnauthorizedHttpException(
'Signature realm="ActivityPub",headers="(request-target) host date"'
);
}
$updated = $this->objectsService->update( $updateFields['id'], $updateFields );
$activity['object'] = $updated->asArray();
$event->setActivity( $activity );
}
/**
* Returns true if $request is authorized to update $object
*
@ -74,25 +55,45 @@ class UpdateHandler implements EventSubscriberInterface
*/
private function authorized( Request $request, array $object )
{
if ( ! $request->attributes->has( 'actor' ) ) {
if ( !$request->attributes->has( 'actor' ) ) {
return false;
}
if ( ! array_key_exists( 'id', $object ) ) {
if ( !array_key_exists( 'id', $object ) ) {
return false;
}
$object = $this->objectsService->dereference( $object['id'] );
if ( ! $object->hasField( 'attributedTo' ) ) {
if ( !$object->hasField( 'attributedTo' ) ) {
return false;
}
$attributedActorId = $object['attributedTo'];
if ( is_array( $attributedActorId ) &&
array_key_exists( 'id', $attributedActorId ) ) {
array_key_exists( 'id', $attributedActorId ) ) {
$attributedActorId = $attributedActorId['id'];
}
if ( ! is_string( $attributedActorId ) ) {
if ( !is_string( $attributedActorId ) ) {
return false;
}
$requestActor = $request->attributes->get( 'actor' );
return $requestActor['id'] === $attributedActorId;
}
public function handleOutbox( OutboxActivityEvent $event )
{
$activity = $event->getActivity();
if ( $activity['type'] !== 'Update' ) {
return;
}
$updateFields = $activity['object'];
if ( !array_key_exists( 'id', $updateFields ) ) {
throw new BadRequestHttpException( 'Update object has no "id" field' );
}
if ( !$this->authorized( $event->getRequest(), $updateFields ) ) {
throw new UnauthorizedHttpException(
'Signature realm="ActivityPub",headers="(request-target) host date"'
);
}
$updated = $this->objectsService->update( $updateFields['id'], $updateFields );
$activity['object'] = $updated->asArray();
$event->setActivity( $activity );
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Activities;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
@ -6,6 +7,29 @@ use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
class ValidationHandler implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return array(
InboxActivityEvent::NAME => 'verifyInboxActivity',
OutboxActivityEvent::NAME => 'verifyOutboxActivity',
);
}
public function verifyInboxActivity( InboxActivityEvent $event )
{
$activity = $event->getActivity();
$requiredFields = array( 'type', 'id', 'actor' );
if ( array_key_exists( 'type', $activity ) &&
in_array( $activity['type'], self::getObjectRequiredTypes() ) ) {
$requiredFields[] = 'object';
}
if ( array_key_exists( 'type', $activity ) &&
in_array( $activity['type'], self::getTargetRequiredTypes() ) ) {
$requiredFields[] = 'target';
}
$this->requireFields( $activity, $requiredFields );
}
public static function getObjectRequiredTypes()
{
return array(
@ -21,49 +45,11 @@ class ValidationHandler implements EventSubscriberInterface
);
}
public static function getSubscribedEvents()
{
return array(
InboxActivityEvent::NAME => 'verifyInboxActivity',
OutboxActivityEvent::NAME => 'verifyOutboxActivity',
);
}
public function verifyInboxActivity( InboxActivityEvent $event )
{
$activity = $event->getActivity();
$requiredFields = array( 'type', 'id', 'actor' );
if ( array_key_exists( 'type', $activity ) &&
in_array( $activity['type'], self::getObjectRequiredTypes() ) ) {
$requiredFields[] = 'object';
}
if ( array_key_exists( 'type', $activity ) &&
in_array( $activity['type'], self::getTargetRequiredTypes() ) ) {
$requiredFields[] = 'target';
}
$this->requireFields( $activity, $requiredFields );
}
public function verifyOutboxActivity( OutboxActivityEvent $event )
{
$activity = $event->getActivity();
$requiredFields = array( 'type', 'actor' );
if ( array_key_exists( 'type', $activity ) &&
in_array( $activity['type'], self::getObjectRequiredTypes() ) ) {
$requiredFields[] = 'object';
}
if ( array_key_exists( 'type', $activity ) &&
in_array( $activity['type'], self::getTargetRequiredTypes() ) ) {
$requiredFields[] = 'target';
}
$this->requireFields( $activity, $requiredFields );
}
private function requireFields( array $activity, array $fields )
{
$missing = array();
foreach ( $fields as $field ) {
if ( ! array_key_exists( $field, $activity ) ) {
if ( !array_key_exists( $field, $activity ) ) {
$missing[] = $field;
}
}
@ -73,5 +59,20 @@ class ValidationHandler implements EventSubscriberInterface
);
}
}
public function verifyOutboxActivity( OutboxActivityEvent $event )
{
$activity = $event->getActivity();
$requiredFields = array( 'type', 'actor' );
if ( array_key_exists( 'type', $activity ) &&
in_array( $activity['type'], self::getObjectRequiredTypes() ) ) {
$requiredFields[] = 'object';
}
if ( array_key_exists( 'type', $activity ) &&
in_array( $activity['type'], self::getTargetRequiredTypes() ) ) {
$requiredFields[] = 'target';
}
$this->requireFields( $activity, $requiredFields );
}
}

View File

@ -39,7 +39,7 @@ class ActivityPub
*/
public function __construct( ActivityPubConfig $config )
{
$this->module = new ActivityPubModule( $config);
$this->module = new ActivityPubModule( $config );
}
/**
@ -52,7 +52,7 @@ class ActivityPub
*/
public function handle( $request = null )
{
if ( ! $request ) {
if ( !$request ) {
$request = Request::createFromGlobals();
}
@ -73,21 +73,6 @@ class ActivityPub
return $kernel->handle( $request );
}
/**
* Creates the database tables necessary for the library to function,
* if they have not already been created.
*
* For best performance, this should only get called once in an application
* (for example, when other database migrations get run).
*/
public function updateSchema()
{
$entityManager = $this->module->get( EntityManager::class );
$schemaTool = new SchemaTool( $entityManager );
$classes = $entityManager->getMetadataFactory()->getAllMetadata();
$schemaTool->updateSchema( $classes );
}
/**
* Sets up the activity handling pipeline
*
@ -102,5 +87,20 @@ class ActivityPub
$dispatcher->addSubscriber( $this->module->get( UpdateHandler::class ) );
$dispatcher->addSubscriber( $this->module->get( DeleteHandler::class ) );
}
/**
* Creates the database tables necessary for the library to function,
* if they have not already been created.
*
* For best performance, this should only get called once in an application
* (for example, when other database migrations get run).
*/
public function updateSchema()
{
$entityManager = $this->module->get( EntityManager::class );
$schemaTool = new SchemaTool( $entityManager );
$classes = $entityManager->getMetadataFactory()->getAllMetadata();
$schemaTool->updateSchema( $classes );
}
}

View File

@ -2,14 +2,14 @@
namespace ActivityPub\Auth;
use Exception;
use ActivityPub\Objects\ObjectsService;
use Exception;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Component\HttpKernel\KernelEvents;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use Symfony\Component\HttpKernel\KernelEvents;
/**
* The AuthListener class answers the question, "is this request authorized
* The AuthListener class answers the question, "is this request authorized
* to act on behalf of this Actor?"
*
* It delegates most of the work to a passed-in Callable to allow library clients to
@ -30,13 +30,6 @@ class AuthListener implements EventSubscriberInterface
*/
private $objectsService;
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => 'checkAuth'
);
}
/**
* Constructs a new AuthenticationService
*
@ -49,6 +42,13 @@ class AuthListener implements EventSubscriberInterface
$this->objectsService = $objectsService;
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => 'checkAuth'
);
}
public function checkAuth( GetResponseEvent $event )
{
$request = $event->getRequest();
@ -56,9 +56,9 @@ class AuthListener implements EventSubscriberInterface
return;
}
$actorId = call_user_func( $this->authFunction );
if ( $actorId && ! empty( $actorId ) ) {
if ( $actorId && !empty( $actorId ) ) {
$actor = $this->objectsService->dereference( $actorId );
if ( ! $actor ) {
if ( !$actor ) {
throw new Exception( "Actor $actorId does not exist" );
}
$request->attributes->set( 'actor', $actor );

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Auth;
use ActivityPub\Entities\ActivityPubObject;
@ -9,7 +10,7 @@ class AuthService
public function isAuthorized( Request $request,
ActivityPubObject $object )
{
if ( ! $this->hasAudience( $object ) ) {
if ( !$this->hasAudience( $object ) ) {
return true;
}
$audience = $this->getAudience( $object );
@ -43,8 +44,8 @@ class AuthService
// TODO do I need to traverse the inReplyTo chain here?
$objectArr = $object->asArray( 0 );
$audience = array();
foreach( array( 'to', 'bto', 'cc', 'bcc', 'audience', 'attributedTo', 'actor' )
as $attribute ) {
foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience', 'attributedTo', 'actor' )
as $attribute ) {
$audience = $this->checkAudienceAttribute( $audience, $attribute, $objectArr );
}
return $audience;
@ -54,7 +55,7 @@ class AuthService
{
if ( array_key_exists( $attribute, $objectArr ) ) {
$audienceValue = $objectArr[$attribute];
if ( ! is_array( $audienceValue ) ) {
if ( !is_array( $audienceValue ) ) {
$audienceValue = array( $audienceValue );
}
return array_merge( $audience, $audienceValue );

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Auth;
use ActivityPub\Crypto\HttpSignatureService;
@ -24,13 +25,6 @@ class SignatureListener implements EventSubscriberInterface
*/
private $objectsService;
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => 'validateHttpSignature'
);
}
public function __construct( HttpSignatureService $httpSignatureService,
ObjectsService $objectsService )
{
@ -38,6 +32,13 @@ class SignatureListener implements EventSubscriberInterface
$this->objectsService = $objectsService;
}
public static function getSubscribedEvents()
{
return array(
KernelEvents::REQUEST => 'validateHttpSignature'
);
}
/**
* Check for a valid HTTP signature on the request. If the request has a valid
* signature, set the 'signed' and 'signedBy' keys on the request ('signedBy' is
@ -52,33 +53,33 @@ class SignatureListener implements EventSubscriberInterface
if ( $headers->has( 'signature' ) ) {
$signatureHeader = $headers->get( 'signature' );
} else if ( $headers->has( 'authorization' ) &&
substr( $headers->get( 'authorization' ), 0, 9 ) === 'Signature' ) {
substr( $headers->get( 'authorization' ), 0, 9 ) === 'Signature' ) {
$signatureHeader = substr( $headers->get( 'authorization' ), 10 );
}
if ( ! $signatureHeader ) {
if ( !$signatureHeader ) {
return;
}
$matches = array();
if ( ! preg_match( '/keyId="([^"]*)"/', $signatureHeader, $matches) ) {
if ( !preg_match( '/keyId="([^"]*)"/', $signatureHeader, $matches ) ) {
return;
}
$keyId = $matches[1];
$key = $this->objectsService->dereference( $keyId );
if ( ! $key || ! $key->hasField( 'owner' ) || ! $key->hasField( 'publicKeyPem' ) ) {
if ( !$key || !$key->hasField( 'owner' ) || !$key->hasField( 'publicKeyPem' ) ) {
return;
}
$owner = $key['owner'];
if ( is_string( $owner ) ) {
$owner = $this->objectsService->dereference( $owner );
}
if ( ! $owner ) {
if ( !$owner ) {
return;
}
if ( ! $this->httpSignatureService->verify( $request, $key['publicKeyPem'] ) ) {
if ( !$this->httpSignatureService->verify( $request, $key['publicKeyPem'] ) ) {
return;
}
$request->attributes->set( 'signed', true );
if ( ! $request->attributes->has( 'actor' ) ) {
if ( !$request->attributes->has( 'actor' ) ) {
$request->attributes->set( 'actor', $owner );
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Config;
/**
@ -37,7 +38,7 @@ class ActivityPubConfig
private $idPathPrefix;
/**
* Don't call this directly - instead, use
* Don't call this directly - instead, use
* ActivityPubConfig->createBuilder()->build()
*
* @param ActivityPubConfigBuilder $builder

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Config;
use ActivityPub\Objects\ContextProvider;
@ -61,7 +62,7 @@ class ActivityPubConfigBuilder
{
$this->isDevMode = false;
$this->dbPrefix = '';
$this->authFunction = function() {
$this->authFunction = function () {
return false;
};
$this->jsonLdContext = ContextProvider::getDefaultContext();
@ -85,11 +86,21 @@ class ActivityPubConfigBuilder
*/
private function validate()
{
if ( ! $this->dbConnectionParams ) {
if ( !$this->dbConnectionParams ) {
throw new Exception( "Missing required option 'dbConnectionParams'" );
}
}
/**
* @return array
*
*
*/
public function getDbConnectionParams()
{
return $this->dbConnectionParams;
}
/**
* The `dbConnectionParams` are the Doctrine connection parameters,
* passed directly through to EntityManager::create(). See
@ -106,13 +117,13 @@ class ActivityPubConfigBuilder
}
/**
* @return array
* @return bool
*
*
*/
public function getDbConnectionParams()
public function getIsDevMode()
{
return $this->dbConnectionParams;
return $this->isDevMode;
}
/**
@ -130,13 +141,11 @@ class ActivityPubConfigBuilder
}
/**
* @return bool
*
*
* @return string
*/
public function getIsDevMode()
public function getDbPrefix()
{
return $this->isDevMode;
return $this->dbPrefix;
}
/**
@ -156,11 +165,11 @@ class ActivityPubConfigBuilder
}
/**
* @return string
* @return Callable
*/
public function getDbPrefix()
public function getAuthFunction()
{
return $this->dbPrefix;
return $this->authFunction;
}
/**
@ -182,11 +191,11 @@ class ActivityPubConfigBuilder
}
/**
* @return Callable
* @return array
*/
public function getAuthFunction()
public function getJsonLdContext()
{
return $this->authFunction;
return $this->jsonLdContext;
}
/**
@ -205,11 +214,11 @@ class ActivityPubConfigBuilder
}
/**
* @return array
* @return string
*/
public function getJsonLdContext()
public function getIdPathPrefix()
{
return $this->jsonLdContext;
return $this->idPathPrefix;
}
/**
@ -226,13 +235,5 @@ class ActivityPubConfigBuilder
$this->idPathPrefix = $idPathPrefix;
return $this;
}
/**
* @return string
*/
public function getIdPathPrefix()
{
return $this->idPathPrefix;
}
}

View File

@ -5,8 +5,8 @@
namespace ActivityPub\Config;
use ActivityPub\Activities\CreateHandler;
use ActivityPub\Activities\NonActivityHandler;
use ActivityPub\Activities\DeleteHandler;
use ActivityPub\Activities\NonActivityHandler;
use ActivityPub\Activities\UpdateHandler;
use ActivityPub\Activities\ValidationHandler;
use ActivityPub\Auth\AuthListener;
@ -17,8 +17,8 @@ use ActivityPub\Controllers\PostController;
use ActivityPub\Crypto\HttpSignatureService;
use ActivityPub\Database\PrefixNamingStrategy;
use ActivityPub\Http\Router;
use ActivityPub\Objects\ContextProvider;
use ActivityPub\Objects\CollectionsService;
use ActivityPub\Objects\ContextProvider;
use ActivityPub\Objects\IdProvider;
use ActivityPub\Objects\ObjectsService;
use ActivityPub\Utils\RandomProvider;
@ -94,7 +94,7 @@ class ActivityPubModule
->addArgument( new Reference( AuthService::class ) )
->addArgument( new Reference( ContextProvider::class ) )
->addArgument( new Reference( Client::class ) )
->addArgument( new Reference( SimpleDateTimeProvider::class ));
->addArgument( new Reference( SimpleDateTimeProvider::class ) );
$this->injector->register( RandomProvider::class, RandomProvider::class );

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Controllers;
use ActivityPub\Auth\AuthService;
@ -15,7 +16,7 @@ use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
*/
class GetController
{
/**
* @var ObjectsService
*/
@ -54,23 +55,23 @@ class GetController
$uri = substr( $uri, 0, $queryPos );
}
$object = $this->objectsService->dereference( $uri );
if ( ! $object ) {
if ( !$object ) {
throw new NotFoundHttpException();
}
if ( ! $this->authService->isAuthorized( $request, $object ) ) {
if ( !$this->authService->isAuthorized( $request, $object ) ) {
throw new UnauthorizedHttpException(
'Signature realm="ActivityPub",headers="(request-target) host date"'
);
}
if ( $object->hasField( 'type' ) &&
( $object['type'] === 'Collection' ||
$object['type'] === 'OrderedCollection' ) ) {
( $object['type'] === 'Collection' ||
$object['type'] === 'OrderedCollection' ) ) {
$pagedCollection = $this->collectionsService->pageAndFilterCollection( $request, $object );
return new JsonResponse( $pagedCollection );
}
$response = new JsonResponse( $object->asArray() );
if ( $object->hasField( 'type' ) &&
$object['type'] === 'Tombstone' ) {
$object['type'] === 'Tombstone' ) {
$response->setStatusCode( 410 );
}
return $response;

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Controllers;
use ActivityPub\Activities\InboxActivityEvent;
@ -54,15 +55,15 @@ class PostController
$inboxField = $object->getReferencingField( 'inbox' );
if ( $inboxField ) {
$activity = json_decode( $request->getContent(), true );
if ( ! $activity || ! array_key_exists( 'actor', $activity ) ) {
if ( !$activity || !array_key_exists( 'actor', $activity ) ) {
throw new BadRequestHttpException();
}
$activityActor = $this->getActivityActor( $activity );
if ( ! $activityActor) {
if ( !$activityActor ) {
throw new BadRequestHttpException();
}
if ( ! $request->attributes->has( 'signed' ) ||
! $this->authorized( $request, $activityActor ) ) {
if ( !$request->attributes->has( 'signed' ) ||
!$this->authorized( $request, $activityActor ) ) {
throw new UnauthorizedHttpException(
'Signature realm="ActivityPub",headers="(request-target) host date"'
);
@ -75,22 +76,32 @@ class PostController
$outboxField = $object->getReferencingField( 'outbox' );
if ( $outboxField ) {
$actorWithOutbox = $outboxField->getObject();
if ( ! $this->authorized( $request, $actorWithOutbox ) ) {
if ( !$this->authorized( $request, $actorWithOutbox ) ) {
throw new UnauthorizedHttpException(
'Signature realm="ActivityPub",headers="(request-target) host date"'
);
}
$activity = json_decode( $request->getContent(), true );
if ( ! $activity ) {
if ( !$activity ) {
throw new BadRequestHttpException();
}
$event = new OutboxActivityEvent( $activity, $actorWithOutbox, $request );
$this->eventDispatcher->dispatch( OutboxActivityEvent::NAME, $event );
return $event->getResponse();
}
}
throw new MethodNotAllowedHttpException( array( Request::METHOD_GET ) );
}
private function getUriWithoutQuery( Request $request )
{
$uri = $request->getUri();
$queryPos = strpos( $uri, '?' );
if ( $queryPos !== false ) {
$uri = substr( $uri, 0, $queryPos );
}
return $uri;
}
private function getActivityActor( array $activity )
{
$actor = $activity['actor'];
@ -104,7 +115,7 @@ class PostController
private function authorized( Request $request, ActivityPubObject $activityActor )
{
if ( ! $request->attributes->has( 'actor' ) ) {
if ( !$request->attributes->has( 'actor' ) ) {
return false;
}
$requestActor = $request->attributes->get( 'actor' );
@ -113,15 +124,5 @@ class PostController
}
return true;
}
private function getUriWithoutQuery( Request $request )
{
$uri = $request->getUri();
$queryPos = strpos( $uri, '?' );
if ( $queryPos !== false ) {
$uri = substr( $uri, 0, $queryPos );
}
return $uri;
}
}

View File

@ -1,10 +1,11 @@
<?php
namespace ActivityPub\Crypto;
use ActivityPub\Utils\HeaderUtils;
use DateTime;
use ActivityPub\Utils\DateTimeProvider;
use ActivityPub\Utils\HeaderUtils;
use ActivityPub\Utils\SimpleDateTimeProvider;
use DateTime;
use Psr\Http\Message\RequestInterface;
use Symfony\Bridge\PsrHttpMessage\Factory\DiactorosFactory;
use Symfony\Component\HttpFoundation\Request;
@ -30,41 +31,32 @@ class HttpSignatureService
/**
* Constructs a new HttpSignatureService
*
* @param DateTimeProvider $dateTimeProvider The DateTimeProvider,
* @param DateTimeProvider $dateTimeProvider The DateTimeProvider,
* defaults to SimpleDateTimeProvider
*/
public function __construct( DateTimeProvider $dateTimeProvider = null )
{
if ( ! $dateTimeProvider ) {
if ( !$dateTimeProvider ) {
$dateTimeProvider = new SimpleDateTimeProvider();
}
$this->dateTimeProvider = $dateTimeProvider;
$this->psr7Factory = new DiactorosFactory();
}
public static function getDefaultHeaders()
{
return array(
'(request-target)',
'host',
'date',
);
}
/**
* Generates a signature given the request and private key
*
* @param RequestInterface $request The request to be signed
* @param string $privateKey The private key to use to sign the request
* @param string $keyId The id of the signing key
* @param array $headers|null The headers to use in the signature
* @param array $headers |null The headers to use in the signature
* (default ['(request-target)', 'host', 'date'])
* @return string The Signature header value
*/
public function sign( RequestInterface $request, $privateKey,
public function sign( RequestInterface $request, $privateKey,
$keyId, $headers = null )
{
if ( ! $headers ) {
if ( !$headers ) {
$headers = self::getDefaultHeaders();
}
$headers = array_map( 'strtolower', $headers );
@ -78,6 +70,45 @@ class HttpSignatureService
"signature=\"$signature\"";
}
public static function getDefaultHeaders()
{
return array(
'(request-target)',
'host',
'date',
);
}
/**
* Returns the signing string from the request
*
* @param RequestInterface $request The request
* @param array $headers The headers to use to generate the signing string
* @return string The signing string
*/
private function getSigningString( RequestInterface $request, $headers )
{
$signingComponents = array();
foreach ( $headers as $header ) {
$component = "${header}: ";
if ( $header == '(request-target)' ) {
$method = strtolower( $request->getMethod() );
$path = $request->getUri()->getPath();
$query = $request->getUri()->getQuery();
if ( !empty( $query ) ) {
$path = "$path?$query";
}
$component = $component . $method . ' ' . $path;
} else {
// TODO handle 'digest' specially here too
$values = $request->getHeader( $header );
$component = $component . implode( ', ', $values );
}
$signingComponents[] = $component;
}
return implode( "\n", $signingComponents );
}
/**
* Verifies the HTTP signature of $request
*
@ -85,12 +116,12 @@ class HttpSignatureService
* @param string $publicKey The public key to use to verify the request
* @return bool True if the signature is valid, false if it is missing or invalid
*/
public function verify( Request $request, $publicKey )
public function verify( Request $request, $publicKey )
{
$params = array();
$headers = $request->headers;
if ( ! $headers->has( 'date' ) ) {
if ( !$headers->has( 'date' ) ) {
return false;
}
$now = $this->dateTimeProvider->getTime( 'http-signature.verify' );
@ -102,7 +133,7 @@ class HttpSignatureService
if ( $headers->has( 'signature' ) ) {
$params = $this->parseSignatureParams( $headers->get( 'signature' ) );
} else if ( $headers->has( 'authorization' ) &&
substr($headers->get( 'authorization' ), 0, 9) === 'Signature' ) {
substr( $headers->get( 'authorization' ), 0, 9 ) === 'Signature' ) {
$paramsStr = substr( $headers->get( 'authorization' ), 10 );
$params = $this->parseSignatureParams( $paramsStr );
}
@ -121,43 +152,13 @@ class HttpSignatureService
$signature = base64_decode( $params['signature'] );
// TODO handle different algorithms here, checking the 'algorithm' param and the key headers
$keypair = RsaKeypair::fromPublicKey( $publicKey );
return $keypair->verify($signingString, $signature, 'sha256');
}
/**
* Returns the signing string from the request
*
* @param RequestInterface $request The request
* @param array $headers The headers to use to generate the signing string
* @return string The signing string
*/
private function getSigningString( RequestInterface $request, $headers )
{
$signingComponents = array();
foreach ( $headers as $header ) {
$component = "${header}: ";
if ( $header == '(request-target)' ) {
$method = strtolower( $request->getMethod());
$path = $request->getUri()->getPath();
$query = $request->getUri()->getQuery();
if ( ! empty( $query ) ) {
$path = "$path?$query";
}
$component = $component . $method . ' ' . $path;
} else {
// TODO handle 'digest' specially here too
$values = $request->getHeader( $header );
$component = $component . implode( ', ', $values );
}
$signingComponents[] = $component;
}
return implode( "\n", $signingComponents );
return $keypair->verify( $signingString, $signature, 'sha256' );
}
/**
* Parses the signature params from the provided params string
*
* @param string $paramsStr The params represented as a string,
* @param string $paramsStr The params represented as a string,
* e.g. 'keyId="theKey",algorithm="rsa-sha256"'
* @return array The params as an associative array
*/
@ -169,7 +170,7 @@ class HttpSignatureService
$paramName = $paramArr[0];
$paramValue = $paramArr[1];
if ( $paramName == 'headers' ) {
$paramValue = explode(' ', $paramValue);
$paramValue = explode( ' ', $paramValue );
}
$params[$paramName] = $paramValue;
}

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Crypto;
use BadMethodCallException;
@ -21,13 +22,53 @@ class RsaKeypair
*
*/
private $privateKey;
public function __construct( $publicKey, $privateKey )
public function __construct( $publicKey, $privateKey )
{
$this->publicKey = $publicKey;
$this->privateKey = $privateKey;
}
/**
* Generates a new keypair
*
* @return RsaKeypair
*/
public static function generate()
{
$rsa = new RSA();
$key = $rsa->createKey( 2048 );
return new RsaKeypair( $key['publickey'], $key['privatekey'] );
}
/**
* Generates an RsaKeypair with the given public key
*
* The generated RsaKeypair will be able to verify signatures but
* not sign data, since it won't have a private key.
*
* @param string $publicKey The public key
* @return RsaKeypair
*/
public static function fromPublicKey( $publicKey )
{
return new RsaKeypair( $publicKey, '' );
}
/**
* Generates an RsaKeypair with the given private key
*
* The generated RsaKeypair will be able to sign data but
* not verify signatures, since it won't have a public key.
*
* @param string $privateKey The private key
* @return RsaKeypair
*/
public static function fromPrivateKey( $privateKey )
{
return new RsaKeypair( '', $privateKey );
}
/**
* Returns the public key as a string
*
@ -66,7 +107,7 @@ class RsaKeypair
}
$rsa = new RSA();
$rsa->setHash( $hash );
$rsa->setSignatureMode(RSA::SIGNATURE_PKCS1);
$rsa->setSignatureMode( RSA::SIGNATURE_PKCS1 );
$rsa->loadKey( $this->privateKey );
return $rsa->sign( $data );
}
@ -86,49 +127,9 @@ class RsaKeypair
// I have no idea what that means or how to fix it
$rsa = new RSA();
$rsa->setHash( $hash );
$rsa->setSignatureMode(RSA::SIGNATURE_PKCS1);
$rsa->setSignatureMode( RSA::SIGNATURE_PKCS1 );
$rsa->loadKey( $this->publicKey );
return $rsa->verify( $data, $signature );
}
/**
* Generates a new keypair
*
* @return RsaKeypair
*/
public static function generate()
{
$rsa = new RSA();
$key = $rsa->createKey( 2048 );
return new RsaKeypair( $key['publickey'], $key['privatekey'] );
}
/**
* Generates an RsaKeypair with the given public key
*
* The generated RsaKeypair will be able to verify signatures but
* not sign data, since it won't have a private key.
*
* @param string $publicKey The public key
* @return RsaKeypair
*/
public static function fromPublicKey( $publicKey )
{
return new RsaKeypair( $publicKey, '' );
}
/**
* Generates an RsaKeypair with the given private key
*
* The generated RsaKeypair will be able to sign data but
* not verify signatures, since it won't have a public key.
*
* @param string $privateKey The private key
* @return RsaKeypair
*/
public static function fromPrivateKey( $privateKey)
{
return new RsaKeypair( '', $privateKey );
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Database;
use Doctrine\ORM\Mapping\NamingStrategy;
@ -12,41 +13,41 @@ class PrefixNamingStrategy implements NamingStrategy
$this->prefix = $prefix;
}
public function classToTableName($className)
{
return $this->prefix . substr($className, strrpos($className, '\\') + 1);
}
public function propertyToColumnName($propertyName, $className = null)
public function propertyToColumnName( $propertyName, $className = null )
{
return $propertyName;
}
public function joinColumnName( $propertyName, $className = null )
{
return $propertyName . '_' . $this->referenceColumnName();
}
public function referenceColumnName()
{
return 'id';
}
public function joinColumnName($propertyName, $className = null)
public function joinTableName( $sourceEntity, $targetEntity, $propertyName = null )
{
return $propertyName . '_' . $this->referenceColumnName();
return strtolower( $this->classToTableName( $sourceEntity ) . '_' .
$this->classToTableName( $targetEntity ) );
}
public function joinTableName($sourceEntity, $targetEntity, $propertyName = null)
public function classToTableName( $className )
{
return strtolower($this->classToTableName($sourceEntity) . '_' .
$this->classToTableName($targetEntity));
return $this->prefix . substr( $className, strrpos( $className, '\\' ) + 1 );
}
public function joinKeyColumnName($entityName, $referencedColumnName = null)
public function joinKeyColumnName( $entityName, $referencedColumnName = null )
{
return strtolower($this->classToTableName($entityName) . '_' .
($referencedColumnName ?: $this->referenceColumnName()));
return strtolower( $this->classToTableName( $entityName ) . '_' .
( $referencedColumnName ?: $this->referenceColumnName() ) );
}
public function embeddedFieldToColumnName($propertyName, $embeddedColumnName, $className = null, $embeddedClassName = null)
public function embeddedFieldToColumnName( $propertyName, $embeddedColumnName, $className = null, $embeddedClassName = null )
{
return $propertyName.'_'.$embeddedColumnName;
return $propertyName . '_' . $embeddedColumnName;
}
}

View File

@ -58,8 +58,9 @@ class ActivityPubObject implements ArrayAccess
*/
protected $privateKey;
public function __construct( DateTime $time = null ) {
if ( ! $time ) {
public function __construct( DateTime $time = null )
{
if ( !$time ) {
$time = new DateTime( "now" );
}
$this->fields = new ArrayCollection();
@ -75,7 +76,8 @@ class ActivityPubObject implements ArrayAccess
*
* @return array|string Either the object or its id if $depth is < 0
*/
public function asArray( $depth = 1 ) {
public function asArray( $depth = 1 )
{
if ( $depth < 0 && $this->hasField( 'id' ) ) {
return $this->getFieldValue( 'id' );
}
@ -91,9 +93,20 @@ class ActivityPubObject implements ArrayAccess
return $arr;
}
public function getId()
/**
* Returns true if the object contains a field with key $name
*
* @param mixed $name
* @return boolean
*/
public function hasField( $name )
{
return $this->id;
foreach ( $this->getFields() as $field ) {
if ( $field->getName() === $name ) {
return true;
}
}
return false;
}
/**
@ -107,13 +120,28 @@ class ActivityPubObject implements ArrayAccess
}
/**
* Returns the fields which reference this object
* Returns the value of the field with key $name
*
* @return Field[]
* The value is either a string, another ActivityPubObject, or null
* if no such key exists.
*
* @param mixed $name
* @return string|ActivityPubObject|null The field's value, or null if
* the field is not found
*/
public function getReferencingFields()
public function getFieldValue( $name )
{
return $this->referencingFields;
foreach ( $this->getFields() as $field ) {
if ( $field->getName() === $name ) {
return $field->getValueOrTargetObject();
}
}
return null;
}
public function getId()
{
return $this->id;
}
/**
@ -137,55 +165,14 @@ class ActivityPubObject implements ArrayAccess
}
/**
* Returns true if the object contains a field with key $name
* Sets the last updated timestamp
*
* @param DateTime $lastUpdated The new last updated timestamp
*
* @param mixed $name
* @return boolean
*/
public function hasField( $name )
public function setLastUpdated( $lastUpdated )
{
foreach( $this->getFields() as $field ) {
if ( $field->getName() === $name ) {
return true;
}
}
return false;
}
/**
* Returns the fields named $field, if it exists
*
* @param string $name The name of the field to get
* @return Field|null
*/
public function getField( $name )
{
foreach( $this->getFields() as $field ) {
if ( $field->getName() === $name ) {
return $field;
}
}
return null;
}
/**
* Returns the value of the field with key $name
*
* The value is either a string, another ActivityPubObject, or null
* if no such key exists.
*
* @param mixed $name
* @return string|ActivityPubObject|null The field's value, or null if
* the field is not found
*/
public function getFieldValue( $name )
{
foreach( $this->getFields() as $field ) {
if ( $field->getName() === $name ) {
return $field->getValueOrTargetObject();
}
}
return null;
$this->lastUpdated = $lastUpdated;
}
/**
@ -200,7 +187,7 @@ class ActivityPubObject implements ArrayAccess
*/
public function addField( Field $field, DateTime $time = null )
{
if ( ! $time ) {
if ( !$time ) {
$time = new DateTime( "now" );
}
$this->fields[] = $field;
@ -215,7 +202,7 @@ class ActivityPubObject implements ArrayAccess
*/
public function hasReferencingField( $name )
{
foreach( $this->getReferencingFields() as $field ) {
foreach ( $this->getReferencingFields() as $field ) {
if ( $field->getName() === $name ) {
return true;
}
@ -223,6 +210,16 @@ class ActivityPubObject implements ArrayAccess
return false;
}
/**
* Returns the fields which reference this object
*
* @return Field[]
*/
public function getReferencingFields()
{
return $this->referencingFields;
}
/**
* Returns the referencing field named $field, if it exists
*
@ -231,7 +228,7 @@ class ActivityPubObject implements ArrayAccess
*/
public function getReferencingField( $name )
{
foreach( $this->getReferencingFields() as $field ) {
foreach ( $this->getReferencingFields() as $field ) {
if ( $field->getName() === $name ) {
return $field;
}
@ -274,34 +271,13 @@ class ActivityPubObject implements ArrayAccess
*/
public function removeField( Field $field, DateTime $time = null )
{
if ( ! $time ) {
if ( !$time ) {
$time = new DateTime( "now" );
}
$this->fields->removeElement( $field );
$this->lastUpdated = $time;
}
/**
* Sets the last updated timestamp
*
* @param DateTime $lastUpdated The new last updated timestamp
*
*/
public function setLastUpdated( $lastUpdated )
{
$this->lastUpdated = $lastUpdated;
}
/**
* Returns true if this object has an associated private key, false if otherwise
*
* @return bool
*/
public function hasPrivateKey()
{
return $this->privateKey !== null;
}
/**
* Sets the object's private key
*
@ -316,6 +292,16 @@ class ActivityPubObject implements ArrayAccess
}
}
/**
* Returns true if this object has an associated private key, false if otherwise
*
* @return bool
*/
public function hasPrivateKey()
{
return $this->privateKey !== null;
}
public function offsetExists( $offset )
{
return $this->hasField( $offset );
@ -344,21 +330,37 @@ class ActivityPubObject implements ArrayAccess
* Returns true if $other has all the same fields as $this
*
* @param ActivityPubObject $other The other object to compare to
* @return bool Whether or not this object has the same fields and values as
* @return bool Whether or not this object has the same fields and values as
* the other
*/
public function equals( ActivityPubObject $other )
{
foreach( $other->getFields() as $otherField ) {
foreach ( $other->getFields() as $otherField ) {
$thisField = $this->getField( $otherField->getName() );
if ( ! $thisField ) {
if ( !$thisField ) {
return false;
}
if ( ! $thisField->equals( $otherField ) ) {
if ( !$thisField->equals( $otherField ) ) {
return false;
}
}
return true;
}
/**
* Returns the fields named $field, if it exists
*
* @param string $name The name of the field to get
* @return Field|null
*/
public function getField( $name )
{
foreach ( $this->getFields() as $field ) {
if ( $field->getName() === $name ) {
return $field;
}
}
return null;
}
}

View File

@ -8,7 +8,7 @@ use DateTime;
/**
* The field table hold the JSON-LD object graph.
*
*
* Its structure is based on https://changelog.com/posts/graph-databases-101:
* Every row has a subject, which is a foreign key into the Objects table,
* a predicate, which is a the JSON field that describes the graph edge relationship
@ -51,7 +51,7 @@ class Field
protected $value;
/**
* @ManyToOne(targetEntity="ActivityPubObject", inversedBy="referencingFields")
* @var ActivityPubObject The value of the field if it holds another object;
* @var ActivityPubObject The value of the field if it holds another object;
* mutually exclusive with $value
*/
protected $targetObject;
@ -72,7 +72,7 @@ class Field
protected function __construct( DateTime $time = null )
{
if ( ! $time ) {
if ( !$time ) {
$time = new DateTime( "now" );
}
$this->created = $time;
@ -89,9 +89,9 @@ class Field
* @return Field The new field
* @throws \Exception
*/
public static function withValue( ActivityPubObject $object, $name, $value, DateTime $time = null )
public static function withValue( ActivityPubObject $object, $name, $value, DateTime $time = null )
{
if ( ! $time ) {
if ( !$time ) {
$time = new DateTime( "now" );
}
$field = new Field( $time );
@ -116,7 +116,7 @@ class Field
ActivityPubObject $targetObject,
DateTime $time = null )
{
if ( ! $time ) {
if ( !$time ) {
$time = new DateTime( "now" );
}
$field = new Field( $time );
@ -126,59 +126,6 @@ class Field
return $field;
}
protected function setObject( ActivityPubObject $object, DateTime $time = null )
{
if ( ! $time ) {
$time = new DateTime( "now" );
}
$object->addField( $this, $time );
$this->object= $object;
}
public function setTargetObject( ActivityPubObject $targetObject, DateTime $time = null )
{
if ( ! $time ) {
$time = new DateTime( "now" );
}
$this->value = null;
$oldTargetObject = $this->getTargetObject();
if ( $oldTargetObject ) {
$oldTargetObject->removeReferencingField( $this );
}
$targetObject->addReferencingField( $this );
$this->targetObject = $targetObject;
$this->lastUpdated = $time;
}
protected function setName( $name )
{
$this->name= $name;
}
public function setValue( $value, DateTime $time = null )
{
if ( ! $time ) {
$time = new DateTime( "now" );
}
$oldTargetObject = $this->getTargetObject();
if ( $oldTargetObject ) {
$oldTargetObject->removeReferencingField( $this );
}
$this->targetObject = null;
$this->value = $value;
$this->lastUpdated = $time;
}
protected function setCreated( DateTime $timestamp )
{
$this->created = $timestamp;
}
protected function setLastUpdated( DateTime $timestamp )
{
$this->lastUpdated = $timestamp;
}
/**
* Returns the object to which this field belongs
*
@ -189,54 +136,13 @@ class Field
return $this->object;
}
/**
* Returns the name of the field
*
* @return string
*/
public function getName()
protected function setObject( ActivityPubObject $object, DateTime $time = null )
{
return $this->name;
}
/**
* Returns the target object of the field or null if there isn't one
*
* @return ActivityPubObject|null
*/
public function getTargetObject()
{
return $this->targetObject;
}
/**
* Returns the value of the field or null if there isn't one
*
* @return string|null
*/
public function getValue()
{
return $this->value;
}
/**
* Returns true if the field has a value
*
* @return bool
*/
public function hasValue()
{
return $this->value !== null;
}
/**
* Returns true if the field has a target object
*
* @return bool
*/
public function hasTargetObject()
{
return $this->targetObject !== null;
if ( !$time ) {
$time = new DateTime( "now" );
}
$object->addField( $this, $time );
$this->object = $object;
}
/**
@ -246,7 +152,7 @@ class Field
*/
public function getValueOrTargetObject()
{
if ( ! is_null( $this->targetObject) ) {
if ( !is_null( $this->targetObject ) ) {
return $this->targetObject;
} else {
return $this->value;
@ -263,6 +169,11 @@ class Field
return $this->created;
}
protected function setCreated( DateTime $timestamp )
{
$this->created = $timestamp;
}
/**
* Returns the field's last updated timestamp
*
@ -273,6 +184,11 @@ class Field
return $this->lastUpdated;
}
protected function setLastUpdated( DateTime $timestamp )
{
$this->lastUpdated = $timestamp;
}
/**
* Returns true if $this is equal to $other
*
@ -291,5 +207,89 @@ class Field
$this->getTargetObject()->equals( $other->getTargetObject() );
}
}
/**
* Returns the name of the field
*
* @return string
*/
public function getName()
{
return $this->name;
}
protected function setName( $name )
{
$this->name = $name;
}
/**
* Returns true if the field has a value
*
* @return bool
*/
public function hasValue()
{
return $this->value !== null;
}
/**
* Returns the value of the field or null if there isn't one
*
* @return string|null
*/
public function getValue()
{
return $this->value;
}
public function setValue( $value, DateTime $time = null )
{
if ( !$time ) {
$time = new DateTime( "now" );
}
$oldTargetObject = $this->getTargetObject();
if ( $oldTargetObject ) {
$oldTargetObject->removeReferencingField( $this );
}
$this->targetObject = null;
$this->value = $value;
$this->lastUpdated = $time;
}
/**
* Returns true if the field has a target object
*
* @return bool
*/
public function hasTargetObject()
{
return $this->targetObject !== null;
}
/**
* Returns the target object of the field or null if there isn't one
*
* @return ActivityPubObject|null
*/
public function getTargetObject()
{
return $this->targetObject;
}
public function setTargetObject( ActivityPubObject $targetObject, DateTime $time = null )
{
if ( !$time ) {
$time = new DateTime( "now" );
}
$this->value = null;
$oldTargetObject = $this->getTargetObject();
if ( $oldTargetObject ) {
$oldTargetObject->removeReferencingField( $this );
}
$targetObject->addReferencingField( $this );
$this->targetObject = $targetObject;
$this->lastUpdated = $time;
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Entities;
/**

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Http;
use ActivityPub\Controllers\GetController;
@ -21,6 +22,13 @@ class Router implements EventSubscriberInterface
*/
private $postController;
public function __construct( GetController $getController,
PostController $postController )
{
$this->getController = $getController;
$this->postController = $postController;
}
public static function getSubscribedEvents()
{
return array(
@ -28,12 +36,6 @@ class Router implements EventSubscriberInterface
);
}
public function __construct( GetController $getController,
PostController $postController )
{
$this->getController = $getController;
$this->postController = $postController;
}
/**
* Routes the request by setting the _controller attribute
*

View File

@ -104,6 +104,89 @@ class CollectionsService
return $colArr;
}
private function getCollectionPage( ActivityPubObject $collection,
Request $request,
$offset,
$pageSize )
{
$itemsKey = 'items';
$pageType = 'CollectionPage';
$isOrdered = $this->isOrdered( $collection );
if ( $isOrdered ) {
$itemsKey = 'orderedItems';
$pageType = 'OrderedCollectionPage';
}
if ( !$collection->hasField( $itemsKey ) ) {
throw new InvalidArgumentException(
"Collection does not have an \"$itemsKey\" key"
);
}
$collectionItems = $collection->getFieldValue( $itemsKey );
$pageItems = array();
$idx = $offset;
$count = 0;
while ( $count < $pageSize ) {
$item = $collectionItems->getFieldValue( $idx );
if ( !$item ) {
break;
}
if ( is_string( $item ) ) {
$pageItems[] = $item;
$count++;
} else if ( $this->authService->isAuthorized( $request, $item ) ) {
$pageItems[] = $item->asArray( 1 );
$count++;
}
$idx++;
}
if ( $count === 0 ) {
throw new NotFoundHttpException();
}
$page = array(
'@context' => $this->contextProvider->getContext(),
'id' => $collection['id'] . "?offset=$offset",
'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";
}
if ( $isOrdered ) {
$page['startIndex'] = $offset;
}
return $page;
}
private function isOrdered( ActivityPubObject $collection )
{
if ( $collection->hasField( 'type' ) &&
$collection['type'] === 'OrderedCollection' ) {
return true;
} else if ( $collection->hasField( 'type' ) &&
$collection['type'] === 'Collection' ) {
return false;
} else {
throw new InvalidArgumentException( 'Not a collection' );
}
}
private function hasNextItem( Request $request, ActivityPubObject $collectionItems, $idx )
{
$next = $collectionItems->getFieldValue( $idx );
while ( $next ) {
if ( is_string( $next ) ||
$this->authService->isAuthorized( $request, $next ) ) {
return $idx;
}
$idx++;
$next = $collectionItems->getFieldValue( $idx );
}
return false;
}
/**
* Given a collection as an array, normalize the collection by collapsing
* collection pages into a single `items` or `orderedItems` array
@ -139,6 +222,38 @@ class CollectionsService
return $collection;
}
private function fetchPage( $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 null;
}
return json_decode( $response->getBody(), true );
}
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;
}
/**
* Adds $item to $collection
*
@ -195,120 +310,5 @@ class CollectionsService
$this->entityManager->persist( $collection );
$this->entityManager->flush();
}
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( $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 null;
}
return json_decode( $response->getBody(), true );
}
private function getCollectionPage( ActivityPubObject $collection,
Request $request,
$offset,
$pageSize )
{
$itemsKey = 'items';
$pageType = 'CollectionPage';
$isOrdered = $this->isOrdered( $collection );
if ( $isOrdered ) {
$itemsKey = 'orderedItems';
$pageType = 'OrderedCollectionPage';
}
if ( !$collection->hasField( $itemsKey ) ) {
throw new InvalidArgumentException(
"Collection does not have an \"$itemsKey\" key"
);
}
$collectionItems = $collection->getFieldValue( $itemsKey );
$pageItems = array();
$idx = $offset;
$count = 0;
while ( $count < $pageSize ) {
$item = $collectionItems->getFieldValue( $idx );
if ( !$item ) {
break;
}
if ( is_string( $item ) ) {
$pageItems[] = $item;
$count++;
} else if ( $this->authService->isAuthorized( $request, $item ) ) {
$pageItems[] = $item->asArray( 1 );
$count++;
}
$idx++;
}
if ( $count === 0 ) {
throw new NotFoundHttpException();
}
$page = array(
'@context' => $this->contextProvider->getContext(),
'id' => $collection['id'] . "?offset=$offset",
'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";
}
if ( $isOrdered ) {
$page['startIndex'] = $offset;
}
return $page;
}
private function hasNextItem( Request $request, ActivityPubObject $collectionItems, $idx )
{
$next = $collectionItems->getFieldValue( $idx );
while ( $next ) {
if ( is_string( $next ) ||
$this->authService->isAuthorized( $request, $next ) ) {
return $idx;
}
$idx++;
$next = $collectionItems->getFieldValue( $idx );
}
return false;
}
private function isOrdered( ActivityPubObject $collection )
{
if ( $collection->hasField( 'type' ) &&
$collection['type'] === 'OrderedCollection' ) {
return true;
} else if ( $collection->hasField( 'type' ) &&
$collection['type'] === 'Collection' ) {
return false;
} else {
throw new InvalidArgumentException( 'Not a collection' );
}
}
}

View File

@ -1,23 +1,19 @@
<?php
namespace ActivityPub\Objects;
class ContextProvider
{
private $ctx;
public function __construct( $ctx = null )
{
if ( ! $ctx ) {
if ( !$ctx ) {
$ctx = self::getDefaultContext();
}
$this->ctx = $ctx;
}
public function getContext()
{
return $this->ctx;
}
public static function getDefaultContext()
{
return array(
@ -25,5 +21,10 @@ class ContextProvider
'https://w3id.org/security/v1',
);
}
public function getContext()
{
return $this->ctx;
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Objects;
use ActivityPub\Utils\RandomProvider;
@ -51,7 +52,7 @@ class IdProvider
public function getId( Request $request, $path = "objects" )
{
$baseUri = $request->getSchemeAndHttpHost();
if ( ! empty( $path ) ) {
if ( !empty( $path ) ) {
$baseUri = $baseUri . "/{$this->pathPrefix}/$path";
}
$rnd = $this->randomProvider->randomString( self::ID_LENGTH );

View File

@ -14,28 +14,207 @@ use GuzzleHttp\Psr7\Request;
class ObjectsService
{
/**
* @var EntityManager
/**
* @var EntityManager
*/
protected $entityManager;
/**
* @var DateTimeProvider
/**
* @var DateTimeProvider
*/
protected $dateTimeProvider;
/**
* @var Client
/**
* @var Client
*/
protected $httpClient;
public function __construct( EntityManager $entityManager,
DateTimeProvider $dateTimeProvider,
Client $client)
Client $client )
{
$this->entityManager = $entityManager;
$this->dateTimeProvider = $dateTimeProvider;
$this->httpClient = $client;
}
/**
* Fully replaces the object referenced by $id by the new $fields
*
* @param string $id The id of the object to replace
* @param array $replacement The new fields to replace the object with
* @return ActivityPubObject|null The replaced object, or null
* if no object with $id exists
*/
public function replace( $id, $replacement )
{
$existing = $this->getObject( $id );
if ( !$existing ) {
return null;
}
foreach ( $existing->getFields() as $field ) {
if ( !array_key_exists( $field->getName(), $replacement ) ) {
$replacement[$field->getName()] = null;
}
}
return $this->update( $id, $replacement );
}/** @noinspection PhpDocMissingThrowsInspection */
/**
* Gets an object from the DB by its ActivityPub id
*
* For internal use only - external callers should use dereference()
*
* @param string $id The object's id
* @return ActivityPubObject|null The object or null
* if no object exists with that id
*/
protected function getObject( $id )
{
$results = $this->query( array( 'id' => $id ) );
if ( !empty( $results ) ) {
return $results[0];
}
return null;
}
/**
* Queries for an object with certain field values
*
* @param array $queryTerms An associative array where the keys are field
* names and the values are the values to query for. The value for a key
* can also be another associative array, which represents a field
* containing a target object that matches the given nested query.
* Finally, the value could be a sequential array, which represents a field
* containing all of the specified values (the field could also contain more
* values).
*
* @return ActivityPubObject[] The objects that match the query, if any,
* ordered by created timestamp from newest to oldest
*/
public function query( $queryTerms )
{
$qb = $this->getObjectQuery( $queryTerms );
$query = $qb->getQuery();
return $query->getResult();
}
/**
* Generates the Doctrine QueryBuilder that represents the query
*
* This function is recursive; it traverses the query tree to build up the
* final expression
*
* @param array $queryTerms The query terms from which to generate the expressions
* @param int $nonce A nonce value to differentiate field names
* @return QueryBuilder The expression
*/
protected function getObjectQuery( $queryTerms, $nonce = 0 )
{
$qb = $this->entityManager->createQueryBuilder();
$exprs = array();
foreach ( $queryTerms as $fieldName => $fieldValue ) {
if ( is_array( $fieldValue ) ) {
$subQuery = $this->getObjectQuery( $fieldValue, $nonce + 1 );
$exprs[] = $qb->expr()->andX(
$qb->expr()->like(
"field$nonce.name",
$qb->expr()->literal( (string)$fieldName )
),
$qb->expr()->in( "field$nonce.targetObject", $subQuery->getDql() )
);
} else {
$exprs[] = $qb->expr()->andX(
$qb->expr()->like(
"field$nonce.name",
$qb->expr()->literal( (string)$fieldName )
),
$qb->expr()->like(
"field$nonce.value",
$qb->expr()->literal( $fieldValue )
)
);
}
}
return $qb->select( "object$nonce" )
->from( 'ActivityPub\Entities\ActivityPubObject', "object$nonce" )
->join( "object{$nonce}.fields", "field$nonce" )
->where( call_user_func_array(
array( $qb->expr(), 'orX' ),
$exprs
) )
->groupBy( "object$nonce" )
->having( $qb->expr()->eq(
$qb->expr()->count( "field$nonce" ),
count( $queryTerms )
) );
}
/**
* Updates $object
*
* @param string $id The ActivityPub id of the object to update
* @param array $updatedFields An array where the key is a field name
* to update and the value is the field's new value. If the value is
* null, the field will be deleted.
*
* If the update results in an orphaned anonymous node (an ActivityPubObject
* with no 'id' field that no longer has any references to it), then the
* orphaned node will be deleted.
*
* @return ActivityPubObject|null The updated object,
* or null if an object with that id isn't in the DB
*/
public function update( $id, $updatedFields )
{
$object = $this->getObject( $id );
if ( !$object ) {
return null;
}
foreach ( $updatedFields as $fieldName => $newValue ) {
if ( $newValue === null && $object->hasField( $fieldName ) ) {
$field = $object->getField( $fieldName );
if ( $field->hasTargetObject() && !$field->getTargetObject()->hasField( 'id' ) ) {
$targetObject = $field->getTargetObject();
// Clear the target object by setting a dummy value
$field->setValue( '' );
$this->entityManager->remove( $targetObject );
}
$object->removeField( $field );
$this->entityManager->persist( $object );
$this->entityManager->remove( $field );
} else if ( $object->hasField( $fieldName ) ) {
$field = $object->getField( $fieldName );
$oldTargetObject = $field->getTargetObject();
if ( is_array( $newValue ) ) {
$newTargetObject = $this->persist( $newValue, 'objects-service.update' );
$field->setTargetObject(
$newTargetObject,
$this->dateTimeProvider->getTime( 'objects-service.update' )
);
} else {
$field->setValue(
$newValue, $this->dateTimeProvider->getTime( 'objects-service.update' )
);
}
if ( $oldTargetObject && !$oldTargetObject->hasField( 'id' ) ) {
$this->entityManager->remove( $oldTargetObject );
}
$this->entityManager->persist( $field );
} else {
if ( is_array( $newValue ) ) {
$newTargetObject = $this->persist( $newValue );
$field = Field::withObject( $object, $fieldName, $newTargetObject );
} else {
$field = Field::withValue( $object, $fieldName, $newValue );
}
$this->entityManager->persist( $field );
}
}
$object->setLastUpdated( $this->dateTimeProvider->getTime( 'objects-service.update' ) );
$this->entityManager->persist( $object );
$this->entityManager->flush();
return $object;
}
/**
* Persists a new object to the database with fields defined by $fields
*
@ -44,7 +223,7 @@ class ObjectsService
* The existing object will not have its fields modified.
*
* @param array $fields The fields that define the new object
* @param string $context The context to retrieve the current time in.
* @param string $context The context to retrieve the current time in.
* Used for fixing the time in tests.
*
* @return ActivityPubObject The created object
@ -67,7 +246,7 @@ class ObjectsService
/** @noinspection PhpUnhandledExceptionInspection */
$this->entityManager->flush();
return $object;
}/** @noinspection PhpDocMissingThrowsInspection */
}
/**
* Persists a field.
@ -89,7 +268,7 @@ class ObjectsService
$this->entityManager->persist( $fieldEntity );
} else {
if ( $fieldName !== 'id' &&
filter_var( $fieldValue, FILTER_VALIDATE_URL ) !== false ) {
filter_var( $fieldValue, FILTER_VALIDATE_URL ) !== false ) {
$dereferenced = $this->dereference( $fieldValue );
if ( $dereferenced ) {
$fieldEntity = Field::withObject(
@ -102,7 +281,7 @@ class ObjectsService
$fieldEntity = Field::withValue(
$object, $fieldName, $fieldValue, $this->dateTimeProvider->getTime( $context )
);
$this->entityManager->persist( $fieldEntity);
$this->entityManager->persist( $fieldEntity );
}
}
@ -135,189 +314,10 @@ class ObjectsService
return null;
}
$object = json_decode( $response->getBody(), true );
if ( ! $object ) {
if ( !$object ) {
return null;
}
return $this->persist( $object );
}
/**
* Queries for an object with certain field values
*
* @param array $queryTerms An associative array where the keys are field
* names and the values are the values to query for. The value for a key
* can also be another associative array, which represents a field
* containing a target object that matches the given nested query.
* Finally, the value could be a sequential array, which represents a field
* containing all of the specified values (the field could also contain more
* values).
*
* @return ActivityPubObject[] The objects that match the query, if any,
* ordered by created timestamp from newest to oldest
*/
public function query( $queryTerms )
{
$qb = $this->getObjectQuery( $queryTerms );
$query = $qb->getQuery();
return $query->getResult();
}
/**
* Generates the Doctrine QueryBuilder that represents the query
*
* This function is recursive; it traverses the query tree to build up the
* final expression
*
* @param array $queryTerms The query terms from which to generate the expressions
* @param int $nonce A nonce value to differentiate field names
* @return QueryBuilder The expression
*/
protected function getObjectQuery( $queryTerms, $nonce = 0 )
{
$qb = $this->entityManager->createQueryBuilder();
$exprs = array();
foreach( $queryTerms as $fieldName => $fieldValue ) {
if ( is_array( $fieldValue ) ) {
$subQuery = $this->getObjectQuery( $fieldValue, $nonce + 1 );
$exprs[] = $qb->expr()->andX(
$qb->expr()->like(
"field$nonce.name",
$qb->expr()->literal( (string) $fieldName )
),
$qb->expr()->in( "field$nonce.targetObject", $subQuery->getDql())
);
} else {
$exprs[] = $qb->expr()->andX(
$qb->expr()->like(
"field$nonce.name",
$qb->expr()->literal( (string) $fieldName )
),
$qb->expr()->like(
"field$nonce.value",
$qb->expr()->literal( $fieldValue )
)
);
}
}
return $qb->select( "object$nonce" )
->from( 'ActivityPub\Entities\ActivityPubObject', "object$nonce" )
->join( "object{$nonce}.fields", "field$nonce" )
->where( call_user_func_array(
array( $qb->expr(), 'orX' ),
$exprs
) )
->groupBy( "object$nonce" )
->having( $qb->expr()->eq(
$qb->expr()->count( "field$nonce" ),
count( $queryTerms )
) );
}
/**
* Gets an object from the DB by its ActivityPub id
*
* For internal use only - external callers should use dereference()
*
* @param string $id The object's id
* @return ActivityPubObject|null The object or null
* if no object exists with that id
*/
protected function getObject( $id )
{
$results = $this->query( array( 'id' => $id ) );
if ( ! empty( $results ) ) {
return $results[0];
}
return null;
}
/**
* Updates $object
*
* @param string $id The ActivityPub id of the object to update
* @param array $updatedFields An array where the key is a field name
* to update and the value is the field's new value. If the value is
* null, the field will be deleted.
*
* If the update results in an orphaned anonymous node (an ActivityPubObject
* with no 'id' field that no longer has any references to it), then the
* orphaned node will be deleted.
*
* @return ActivityPubObject|null The updated object,
* or null if an object with that id isn't in the DB
*/
public function update( $id, $updatedFields )
{
$object = $this->getObject( $id );
if ( ! $object ) {
return null;
}
foreach( $updatedFields as $fieldName => $newValue ) {
if ( $newValue === null && $object->hasField( $fieldName ) ) {
$field = $object->getField( $fieldName );
if ( $field->hasTargetObject() && ! $field->getTargetObject()->hasField( 'id' ) ) {
$targetObject = $field->getTargetObject();
// Clear the target object by setting a dummy value
$field->setValue( '' );
$this->entityManager->remove( $targetObject );
}
$object->removeField( $field );
$this->entityManager->persist( $object );
$this->entityManager->remove( $field );
} else if ( $object->hasField( $fieldName ) ) {
$field = $object->getField( $fieldName );
$oldTargetObject = $field->getTargetObject();
if ( is_array( $newValue ) ) {
$newTargetObject = $this->persist( $newValue, 'objects-service.update' );
$field->setTargetObject(
$newTargetObject,
$this->dateTimeProvider->getTime( 'objects-service.update' )
);
} else {
$field->setValue(
$newValue, $this->dateTimeProvider->getTime( 'objects-service.update' )
);
}
if ( $oldTargetObject && ! $oldTargetObject->hasField( 'id' ) ) {
$this->entityManager->remove( $oldTargetObject );
}
$this->entityManager->persist( $field );
} else {
if ( is_array( $newValue ) ) {
$newTargetObject = $this->persist( $newValue );
$field = Field::withObject( $object, $fieldName, $newTargetObject );
} else {
$field = Field::withValue( $object, $fieldName, $newValue );
}
$this->entityManager->persist( $field );
}
}
$object->setLastUpdated( $this->dateTimeProvider->getTime( 'objects-service.update' ) );
$this->entityManager->persist( $object );
$this->entityManager->flush();
return $object;
}
/**
* Fully replaces the object referenced by $id by the new $fields
*
* @param string $id The id of the object to replace
* @param array $replacement The new fields to replace the object with
* @return ActivityPubObject|null The replaced object, or null
* if no object with $id exists
*/
public function replace( $id, $replacement )
{
$existing = $this->getObject( $id );
if ( ! $existing ) {
return null;
}
foreach ( $existing->getFields() as $field ) {
if ( ! array_key_exists( $field->getName(), $replacement ) ) {
$replacement[$field->getName()] = null;
}
}
return $this->update( $id, $replacement );
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Utils;
use DateTime;

View File

@ -10,6 +10,7 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace ActivityPub\Utils;
/**
* HTTP header utility functions.
@ -20,12 +21,14 @@ class HeaderUtils
{
const DISPOSITION_ATTACHMENT = 'attachment';
const DISPOSITION_INLINE = 'inline';
/**
* This class should not be instantiated.
*/
private function __construct()
{
}
/**
* Splits an HTTP header by one or more separators.
*
@ -34,17 +37,17 @@ class HeaderUtils
* HeaderUtils::split("da, en-gb;q=0.8", ",;")
* // => ['da'], ['en-gb', 'q=0.8']]
*
* @param string $header HTTP header value
* @param string $header HTTP header value
* @param string $separators List of characters to split on, ordered by
* precedence, e.g. ",", ";=", or ",;="
*
* @return array Nested array with as many levels as there are characters in
* $separators
*/
public static function split($header, $separators)
public static function split( $header, $separators )
{
$quotedSeparators = preg_quote($separators, '/');
preg_match_all('
$quotedSeparators = preg_quote( $separators, '/' );
preg_match_all( '
/
(?!\s)
(?:
@ -52,16 +55,55 @@ class HeaderUtils
"(?:[^"\\\\]|\\\\.)*(?:"|\\\\|$)
|
# token
[^"'.$quotedSeparators.']+
[^"' . $quotedSeparators . ']+
)+
(?<!\s)
|
# separator
\s*
(?<separator>['.$quotedSeparators.'])
(?<separator>[' . $quotedSeparators . '])
\s*
/x', trim($header), $matches, PREG_SET_ORDER);
return self::groupParts($matches, $separators);
/x', trim( $header ), $matches, PREG_SET_ORDER );
return self::groupParts( $matches, $separators );
}
private static function groupParts( array $matches, $separators )
{
$separator = $separators[0];
$partSeparators = substr( $separators, 1 );
$i = 0;
$partMatches = [];
foreach ( $matches as $match ) {
if ( isset( $match['separator'] ) && $match['separator'] === $separator ) {
++$i;
} else {
$partMatches[$i][] = $match;
}
}
$parts = [];
if ( $partSeparators ) {
foreach ( $partMatches as $matches ) {
$parts[] = self::groupParts( $matches, $partSeparators );
}
} else {
foreach ( $partMatches as $matches ) {
$parts[] = self::unquote( $matches[0][0] );
}
}
return $parts;
}
/**
* Decodes a quoted string.
*
* If passed an unquoted string that matches the "token" construct (as
* defined in the HTTP specification), it is passed through verbatimly.
* @param $s
* @return string|string[]|null
*/
public static function unquote( $s )
{
return preg_replace( '/\\\\(.)|"/', '$1', $s );
}
/**
@ -79,12 +121,12 @@ class HeaderUtils
* @param array $parts
* @return array
*/
public static function combine(array $parts)
public static function combine( array $parts )
{
$assoc = [];
foreach ($parts as $part) {
$name = strtolower($part[0]);
if (isset($part[1])) {
foreach ( $parts as $part ) {
$name = strtolower( $part[0] );
if ( isset( $part[1] ) ) {
$value = $part[1];
} else {
$value = true;
@ -94,6 +136,48 @@ class HeaderUtils
return $assoc;
}
/**
* Generates a HTTP Content-Disposition field-value.
*
* @param string $disposition One of "inline" or "attachment"
* @param string $filename A unicode string
* @param string $filenameFallback A string containing only ASCII characters that
* is semantically equivalent to $filename. If the filename is already ASCII,
* it can be omitted, or just copied from $filename
*
* @return string A string suitable for use as a Content-Disposition field-value
*
* @throws \InvalidArgumentException
*
* @see RFC 6266
*/
public static function makeDisposition( $disposition, $filename, $filenameFallback = '' )
{
if ( !\in_array( $disposition, [ self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE ] ) ) {
throw new \InvalidArgumentException( sprintf( 'The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE ) );
}
if ( '' === $filenameFallback ) {
$filenameFallback = $filename;
}
// filenameFallback is not ASCII.
if ( !preg_match( '/^[\x20-\x7e]*$/', $filenameFallback ) ) {
throw new \InvalidArgumentException( 'The filename fallback must only contain ASCII characters.' );
}
// percent characters aren't safe in fallback.
if ( false !== strpos( $filenameFallback, '%' ) ) {
throw new \InvalidArgumentException( 'The filename fallback cannot contain the "%" character.' );
}
// path separators aren't allowed in either.
if ( false !== strpos( $filename, '/' ) || false !== strpos( $filename, '\\' ) || false !== strpos( $filenameFallback, '/' ) || false !== strpos( $filenameFallback, '\\' ) ) {
throw new \InvalidArgumentException( 'The filename and the fallback cannot contain the "/" and "\\" characters.' );
}
$params = [ 'filename' => $filenameFallback ];
if ( $filename !== $filenameFallback ) {
$params['filename*'] = "utf-8''" . rawurlencode( $filename );
}
return $disposition . '; ' . self::toString( $params, ';' );
}
/**
* Joins an associative array into a string for use in an HTTP header.
*
@ -109,17 +193,17 @@ class HeaderUtils
* @param $separator
* @return string
*/
public static function toString(array $assoc, $separator)
public static function toString( array $assoc, $separator )
{
$parts = [];
foreach ($assoc as $name => $value) {
if (true === $value) {
foreach ( $assoc as $name => $value ) {
if ( true === $value ) {
$parts[] = $name;
} else {
$parts[] = $name.'='.self::quote($value);
$parts[] = $name . '=' . self::quote( $value );
}
}
return implode($separator.' ', $parts);
return implode( $separator . ' ', $parts );
}
/**
@ -131,90 +215,11 @@ class HeaderUtils
* @param $s
* @return string
*/
public static function quote($s)
public static function quote( $s )
{
if (preg_match('/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s)) {
if ( preg_match( '/^[a-z0-9!#$%&\'*.^_`|~-]+$/i', $s ) ) {
return $s;
}
return '"'.addcslashes($s, '"\\"').'"';
}
/**
* Decodes a quoted string.
*
* If passed an unquoted string that matches the "token" construct (as
* defined in the HTTP specification), it is passed through verbatimly.
* @param $s
* @return string|string[]|null
*/
public static function unquote($s)
{
return preg_replace('/\\\\(.)|"/', '$1', $s);
}
/**
* Generates a HTTP Content-Disposition field-value.
*
* @param string $disposition One of "inline" or "attachment"
* @param string $filename A unicode string
* @param string $filenameFallback A string containing only ASCII characters that
* is semantically equivalent to $filename. If the filename is already ASCII,
* it can be omitted, or just copied from $filename
*
* @return string A string suitable for use as a Content-Disposition field-value
*
* @throws \InvalidArgumentException
*
* @see RFC 6266
*/
public static function makeDisposition($disposition, $filename, $filenameFallback = '')
{
if (!\in_array($disposition, [self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE])) {
throw new \InvalidArgumentException(sprintf('The disposition must be either "%s" or "%s".', self::DISPOSITION_ATTACHMENT, self::DISPOSITION_INLINE));
}
if ('' === $filenameFallback) {
$filenameFallback = $filename;
}
// filenameFallback is not ASCII.
if (!preg_match('/^[\x20-\x7e]*$/', $filenameFallback)) {
throw new \InvalidArgumentException('The filename fallback must only contain ASCII characters.');
}
// percent characters aren't safe in fallback.
if (false !== strpos($filenameFallback, '%')) {
throw new \InvalidArgumentException('The filename fallback cannot contain the "%" character.');
}
// path separators aren't allowed in either.
if (false !== strpos($filename, '/') || false !== strpos($filename, '\\') || false !== strpos($filenameFallback, '/') || false !== strpos($filenameFallback, '\\')) {
throw new \InvalidArgumentException('The filename and the fallback cannot contain the "/" and "\\" characters.');
}
$params = ['filename' => $filenameFallback];
if ($filename !== $filenameFallback) {
$params['filename*'] = "utf-8''".rawurlencode($filename);
}
return $disposition.'; '.self::toString($params, ';');
}
private static function groupParts(array $matches, $separators)
{
$separator = $separators[0];
$partSeparators = substr($separators, 1);
$i = 0;
$partMatches = [];
foreach ($matches as $match) {
if (isset($match['separator']) && $match['separator'] === $separator) {
++$i;
} else {
$partMatches[$i][] = $match;
}
}
$parts = [];
if ($partSeparators) {
foreach ($partMatches as $matches) {
$parts[] = self::groupParts($matches, $partSeparators);
}
} else {
foreach ($partMatches as $matches) {
$parts[] = self::unquote($matches[0][0]);
}
}
return $parts;
return '"' . addcslashes( $s, '"\\"' ) . '"';
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Utils;
class RandomProvider

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Utils;
class Util
@ -11,8 +12,8 @@ class Util
*/
public static function isAssoc( array $arr )
{
if (array() === $arr) return false;
return array_keys($arr) !== range(0, count($arr) - 1);
if ( array() === $arr ) return false;
return array_keys( $arr ) !== range( 0, count( $arr ) - 1 );
}
/**
@ -25,7 +26,7 @@ class Util
public static function arrayKeysExist( array $arr, array $keys )
{
foreach ( $keys as $key ) {
if ( ! array_key_exists( $key, $arr ) ) {
if ( !array_key_exists( $key, $arr ) ) {
return false;
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Test\Activities;
use ActivityPub\Test\TestConfig\APTestCase;

View File

@ -1,18 +1,19 @@
<?php
namespace ActivityPub\Test\Activities;
use ActivityPub\Auth\AuthService;
use ActivityPub\Activities\CreateHandler;
use ActivityPub\Activities\InboxActivityEvent;
use ActivityPub\Activities\OutboxActivityEvent;
use ActivityPub\Auth\AuthService;
use ActivityPub\Objects\CollectionsService;
use ActivityPub\Objects\ContextProvider;
use ActivityPub\Objects\IdProvider;
use ActivityPub\Objects\ObjectsService;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use ActivityPub\Utils\SimpleDateTimeProvider;
use GuzzleHttp\Client;
use ActivityPub\Test\TestConfig\APTestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
@ -30,7 +31,7 @@ class CreateHandlerTest extends APTestCase
$idProvider = $this->getMock( IdProvider::class );
// TODO provision mocks
$collectionsService = new CollectionsService(
4,
4,
$this->getMock( AuthService::class ),
new ContextProvider(),
$this->getMock( Client::class ),

View File

@ -1,36 +1,21 @@
<?php
namespace ActivityPub\Test\Activities;
use ActivityPub\Activities\DeleteHandler;
use ActivityPub\Activities\InboxActivityEvent;
use ActivityPub\Activities\OutboxActivityEvent;
use ActivityPub\Objects\ObjectsService;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use ActivityPub\Test\TestUtils\TestDateTimeProvider;
use DateTime;
use ActivityPub\Test\TestConfig\APTestCase;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
class DeleteHandlerTest extends APTestCase
{
private static function getObjects()
{
return array(
'https://elsewhere.com/objects/1' => 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(
@ -139,12 +124,12 @@ class DeleteHandlerTest extends APTestCase
),
) );
$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::getObjects()) ) {
->disableOriginalConstructor()
->setMethods( array( 'dereference', 'replace' ) )
->getMock();
$objectsService->method( 'dereference' )->will( $this->returnCallback(
function ( $id ) {
if ( array_key_exists( $id, self::getObjects() ) ) {
$objects = self::getObjects();
return TestActivityPubObject::fromArray( $objects[$id] );
}
@ -173,5 +158,21 @@ class DeleteHandlerTest extends APTestCase
$request->attributes->add( $attributes );
return $request;
}
private static function getObjects()
{
return array(
'https://elsewhere.com/objects/1' => 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',
)
);
}
}

View File

@ -1,12 +1,13 @@
<?php
namespace ActivityPub\Test\Activities;
use ActivityPub\Activities\FollowHandler;
use ActivityPub\Activities\InboxActivityEvent;
use ActivityPub\Activities\OutboxActivityEvent;
use ActivityPub\Objects\ContextProvider;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
@ -28,9 +29,8 @@ class FollowHandlerTest extends APTestCase
'type' => 'Follow',
'object' => 'https://example.com/actor/1',
);
$eventDispatcher->addListener( OutboxActivityEvent::NAME, function( $event, $name )
use ( &$outboxDispatched, $actor, $follow )
{
$eventDispatcher->addListener( OutboxActivityEvent::NAME, function ( $event, $name )
use ( &$outboxDispatched, $actor, $follow ) {
$this->assertEquals( OutboxActivityEvent::NAME, $name );
$outboxDispatched = true;
$accept = array(
@ -81,11 +81,10 @@ class FollowHandlerTest extends APTestCase
'type' => 'Follow',
'object' => 'https://example.com/actor/2',
);
$eventDispatcher->addListener( OutboxActivityEvent::NAME, function()
use ( &$outboxDispatched )
{
$outboxDispatched = true;
} );
$eventDispatcher->addListener( OutboxActivityEvent::NAME, function ()
use ( &$outboxDispatched ) {
$outboxDispatched = true;
} );
$eventDispatcher->dispatch( InboxActivityEvent::NAME, new InboxActivityEvent(
$follow,
$actor,

View File

@ -1,11 +1,12 @@
<?php
namespace ActivityPub\Test\Activities;
use ActivityPub\Activities\OutboxActivityEvent;
use ActivityPub\Activities\NonActivityHandler;
use ActivityPub\Activities\OutboxActivityEvent;
use ActivityPub\Objects\ContextProvider;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use Symfony\Component\HttpFoundation\Request;
class NonActivityHandlerTest extends APTestCase

View File

@ -1,39 +1,23 @@
<?php
namespace ActivityPub\Test\Activities;
use ActivityPub\Activities\InboxActivityEvent;
use ActivityPub\Activities\OutboxActivityEvent;
use ActivityPub\Activities\UpdateHandler;
use ActivityPub\Objects\ObjectsService;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
class UpdateHandlerTest extends APTestCase
{
private static function getObjects()
{
return array(
'https://elsewhere.com/objects/1' => array(
'id' => 'https://elsewhere.com/objects/1',
'attributedTo' => 'https://elsewhere.com/actors/1',
),
'https://example.com/objects/1' => array(
'id' => 'https://example.com/objects/1',
'attributedTo' => 'https://example.com/actors/1',
'type' => 'Note',
'content' => 'This is a note',
),
);
}
/**
* @var EventDispatcher
*/
private $eventDispatcher;
/**
* @var array
*/
@ -44,7 +28,7 @@ class UpdateHandlerTest extends APTestCase
$this->objects = self::getObjects();
$objectsService = $this->getMock( ObjectsService::class );
$objectsService->method( 'dereference' )->will( $this->returnCallback(
function( $id ) {
function ( $id ) {
if ( array_key_exists( $id, $this->objects ) ) {
return TestActivityPubObject::fromArray( $this->objects[$id] );
}
@ -52,7 +36,7 @@ class UpdateHandlerTest extends APTestCase
}
) );
$objectsService->method( 'update' )->will( $this->returnCallback(
function( $id, $updateFields ) {
function ( $id, $updateFields ) {
if ( array_key_exists( $id, $this->objects ) ) {
$existing = $this->objects[$id];
foreach ( $updateFields as $field => $newValue ) {
@ -72,6 +56,22 @@ class UpdateHandlerTest extends APTestCase
$this->eventDispatcher->addSubscriber( $updateHandler );
}
private static function getObjects()
{
return array(
'https://elsewhere.com/objects/1' => array(
'id' => 'https://elsewhere.com/objects/1',
'attributedTo' => 'https://elsewhere.com/actors/1',
),
'https://example.com/objects/1' => array(
'id' => 'https://example.com/objects/1',
'attributedTo' => 'https://example.com/actors/1',
'type' => 'Note',
'content' => 'This is a note',
),
);
}
public function testUpdateHandler()
{
$testCases = array(

View File

@ -1,11 +1,12 @@
<?php
namespace ActivityPub\Test\Activities;
use ActivityPub\Activities\InboxActivityEvent;
use ActivityPub\Activities\OutboxActivityEvent;
use ActivityPub\Activities\ValidationHandler;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
@ -16,13 +17,14 @@ class ValidationHandlerTest extends APTestCase
* @var EventDispatcher
*/
private $eventDispatcher;
public function setUp()
{
$this->eventDispatcher = new EventDispatcher();
$validationHandler = new ValidationHandler();
$this->eventDispatcher->addSubscriber( $validationHandler );
}
public function testValidationHandler()
{
$testCases = array(
@ -162,7 +164,7 @@ class ValidationHandlerTest extends APTestCase
$event = $testCase['event'];
if ( array_key_exists( 'expectedException', $testCase ) ) {
$expectedExceptionMessage = '';
if ( array_key_exists( 'expectedExceptionMessage', $testCase )) {
if ( array_key_exists( 'expectedExceptionMessage', $testCase ) ) {
$expectedExceptionMessage = $testCase['expectedExceptionMessage'];
}
$this->setExpectedException(

View File

@ -1,39 +1,43 @@
<?php /** @noinspection PhpUnhandledExceptionInspection */
namespace ActivityPub\Test;
use ActivityPub\ActivityPub;
use ActivityPub\Config\ActivityPubConfig;
use ActivityPub\Test\TestConfig\SQLiteTestCase;
use ActivityPub\Test\TestConfig\ArrayDataSet;
use ActivityPub\Test\TestConfig\SQLiteTestCase;
class ActivityPubTest extends SQLiteTestCase
{
public function getDataSet() {
public function getDataSet()
{
return new ArrayDataSet( array() );
}
public function testItCreatesSchema() {
public function testItCreatesSchema()
{
$this->assertTrue( file_exists( $this->getDbPath() ) );
}
protected function getDbPath()
{
return dirname( __FILE__ ) . '/db.sqlite';
}
/**
* @depends testItCreatesSchema
*/
public function testItUpdatesSchema() {
public function testItUpdatesSchema()
{
$config = ActivityPubConfig::createBuilder()
->setDbConnectionParams( array(
'driver' => 'pdo_sqlite',
'path' => $this->getDbPath(),
) )
->build();
->setDbConnectionParams( array(
'driver' => 'pdo_sqlite',
'path' => $this->getDbPath(),
) )
->build();
$activityPub = new ActivityPub( $config );
$activityPub->updateSchema();
$this->assertTrue( file_exists( $this->getDbPath() ) );
}
protected function getDbPath() {
return dirname( __FILE__ ) . '/db.sqlite';
}
}

View File

@ -3,13 +3,13 @@
namespace ActivityPub\Test\Auth;
use ActivityPub\Auth\AuthListener;
use ActivityPub\Objects\ObjectsService;
use ActivityPub\Entities\ActivityPubObject;
use ActivityPub\Objects\ObjectsService;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use ActivityPub\Test\TestConfig\APTestCase;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class AuthListenerTest extends APTestCase
{
@ -28,21 +28,12 @@ class AuthListenerTest extends APTestCase
) ) );
}
public function getEvent()
{
$kernel = $this->getMock( HttpKernelInterface::class );
$request = Request::create( 'https://example.com/foo', Request::METHOD_GET );
return new GetResponseEvent(
$kernel, $request, HttpKernelInterface::MASTER_REQUEST
);
}
public function testAuthListener()
{
$testCases = array(
array(
'id' => 'basicTest',
'authFunction' => function() {
'authFunction' => function () {
return 'https://example.com/actor/1';
},
'expectedAttributes' => array(
@ -53,7 +44,7 @@ class AuthListenerTest extends APTestCase
),
array(
'id' => 'existingActorTest',
'authFunction' => function() {
'authFunction' => function () {
return 'https://example.com/actor/1';
},
'requestAttributes' => array(
@ -69,7 +60,7 @@ class AuthListenerTest extends APTestCase
),
array(
'id' => 'defaultAuthTest',
'authFunction' => function() {
'authFunction' => function () {
return false;
},
'expectedAttributes' => array(),
@ -108,5 +99,14 @@ class AuthListenerTest extends APTestCase
}
}
}
public function getEvent()
{
$kernel = $this->getMock( HttpKernelInterface::class );
$request = Request::create( 'https://example.com/foo', Request::METHOD_GET );
return new GetResponseEvent(
$kernel, $request, HttpKernelInterface::MASTER_REQUEST
);
}
}

View File

@ -1,9 +1,10 @@
<?php
namespace ActivityPub\Test\Auth;
use ActivityPub\Auth\AuthService;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use Symfony\Component\HttpFoundation\Request;
class AuthServiceTest extends APTestCase

View File

@ -1,16 +1,17 @@
<?php
namespace ActivityPub\Test\Auth;
use DateTime;
use ActivityPub\Auth\SignatureListener;
use ActivityPub\Crypto\HttpSignatureService;
use ActivityPub\Objects\ObjectsService;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use ActivityPub\Test\TestUtils\TestDateTimeProvider;
use DateTime;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\HttpKernelInterface;
use Symfony\Component\HttpKernel\Event\GetResponseEvent;
use ActivityPub\Test\TestConfig\APTestCase;
use Symfony\Component\HttpKernel\HttpKernelInterface;
class SignatureListenerTest extends APTestCase
{
@ -28,20 +29,6 @@ oYi+1hqp1fIekaxsyQIDAQAB
*/
private $signatureListener;
private static function getActor()
{
return array( 'id' => self::ACTOR_ID );
}
private static function getKey()
{
return array(
'id' => self::KEY_ID,
'owner' => 'https://example.com/actor/1',
'publicKeyPem' => self::PUBLIC_KEY,
);
}
public function setUp()
{
$dateTimeProvider = new TestDateTimeProvider( array(
@ -53,35 +40,26 @@ oYi+1hqp1fIekaxsyQIDAQAB
$objectsService = $this->getMock( ObjectsService::class );
$objectsService->method( 'dereference' )
->will( $this->returnValueMap( array(
array( self::KEY_ID, TestActivityPubObject::fromArray( self::getKey()) ),
array( self::ACTOR_ID, TestActivityPubObject::fromArray( self::getActor()) ),
array( self::KEY_ID, TestActivityPubObject::fromArray( self::getKey() ) ),
array( self::ACTOR_ID, TestActivityPubObject::fromArray( self::getActor() ) ),
) ) );
$this->signatureListener = new SignatureListener(
$httpSignatureService, $objectsService
);
}
private function getEvent()
private static function getKey()
{
$kernel = $this->getMock( HttpKernelInterface::class );
$request = Request::create(
'https://example.com/foo?param=value&pet=dog',
Request::METHOD_POST,
array(),
array(),
array(),
array(),
'{"hello": "world"}'
return array(
'id' => self::KEY_ID,
'owner' => 'https://example.com/actor/1',
'publicKeyPem' => self::PUBLIC_KEY,
);
$request->headers->set( 'host', 'example.com' );
$request->headers->set( 'content-type', 'application/json' );
$request->headers->set(
'digest', 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE='
);
$request->headers->set( 'content-length', 18 );
$request->headers->set( 'date', 'Sun, 05 Jan 2014 21:31:40 GMT' );
$event = new GetResponseEvent( $kernel, $request, HttpKernelInterface::MASTER_REQUEST );
return $event;
}
private static function getActor()
{
return array( 'id' => self::ACTOR_ID );
}
public function testSignatureListener()
@ -136,12 +114,12 @@ oYi+1hqp1fIekaxsyQIDAQAB
foreach ( $testCases as $testCase ) {
$event = $this->getEvent();
if ( array_key_exists( 'headers', $testCase ) ) {
foreach( $testCase['headers'] as $header => $value ) {
foreach ( $testCase['headers'] as $header => $value ) {
$event->getRequest()->headers->set( $header, $value );
}
}
if ( array_key_exists( 'requestAttributes', $testCase ) ) {
foreach( $testCase['requestAttributes'] as $attribute => $value ) {
foreach ( $testCase['requestAttributes'] as $attribute => $value ) {
$event->getRequest()->attributes->set( $attribute, $value );
}
}
@ -153,5 +131,28 @@ oYi+1hqp1fIekaxsyQIDAQAB
);
}
}
private function getEvent()
{
$kernel = $this->getMock( HttpKernelInterface::class );
$request = Request::create(
'https://example.com/foo?param=value&pet=dog',
Request::METHOD_POST,
array(),
array(),
array(),
array(),
'{"hello": "world"}'
);
$request->headers->set( 'host', 'example.com' );
$request->headers->set( 'content-type', 'application/json' );
$request->headers->set(
'digest', 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE='
);
$request->headers->set( 'content-length', 18 );
$request->headers->set( 'date', 'Sun, 05 Jan 2014 21:31:40 GMT' );
$event = new GetResponseEvent( $kernel, $request, HttpKernelInterface::MASTER_REQUEST );
return $event;
}
}

View File

@ -5,8 +5,8 @@ namespace ActivityPub\Test\Config;
use ActivityPub\Config\ActivityPubConfig;
use ActivityPub\Config\ActivityPubModule;
use ActivityPub\Http\Router;
use Doctrine\ORM\EntityManager;
use ActivityPub\Test\TestConfig\APTestCase;
use Doctrine\ORM\EntityManager;
class ActivityPubModuleTest extends APTestCase
{
@ -18,14 +18,14 @@ class ActivityPubModuleTest extends APTestCase
public function setUp()
{
$config = ActivityPubConfig::createBuilder()
->setDbConnectionParams( array(
'driver' => 'pdo_sqlite',
'path' => ':memory:',
) )
->build();
->setDbConnectionParams( array(
'driver' => 'pdo_sqlite',
'path' => ':memory:',
) )
->build();
$this->module = new ActivityPubModule( $config );
}
public function testItInjects()
{
$entityManager = $this->module->get( EntityManager::class );

View File

@ -1,21 +1,54 @@
<?php
namespace ActivityPub\Test\Controllers;
use ActivityPub\Auth\AuthService;
use ActivityPub\Controllers\GetController;
use ActivityPub\Objects\ContextProvider;
use ActivityPub\Objects\CollectionsService;
use ActivityPub\Objects\ContextProvider;
use ActivityPub\Objects\ObjectsService;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use ActivityPub\Utils\SimpleDateTimeProvider;
use GuzzleHttp\Client;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use ActivityPub\Test\TestConfig\APTestCase;
use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
class GetControllerTest extends APTestCase
{
/**
* @var GetController
*/
private $getController;
/**
* @var array
*/
private $objects;
public function setUp()
{
$this->objects = self::getObjects();
$objectsService = $this->getMock( ObjectsService::class );
$objectsService->method( 'dereference' )->will(
$this->returnCallback( function ( $uri ) {
if ( array_key_exists( $uri, $this->objects ) ) {
return TestActivityPubObject::fromArray( $this->objects[$uri] );
}
return null;
} )
);
$authService = new AuthService();
$contextProvider = new ContextProvider();
$httpClient = $this->getMock( Client::class );
$collectionsService = new CollectionsService(
4, $authService, $contextProvider, $httpClient, new SimpleDateTimeProvider()
);
$this->getController = new GetController(
$objectsService, $collectionsService, $authService
);
}
private static function getObjects()
{
return array(
@ -58,39 +91,6 @@ class GetControllerTest extends APTestCase
);
}
/**
* @var GetController
*/
private $getController;
/**
* @var array
*/
private $objects;
public function setUp()
{
$this->objects = self::getObjects();
$objectsService = $this->getMock( ObjectsService::class );
$objectsService->method( 'dereference' )->will(
$this->returnCallback( function( $uri ) {
if ( array_key_exists( $uri, $this->objects) ) {
return TestActivityPubObject::fromArray( $this->objects[$uri] );
}
return null;
})
);
$authService = new AuthService();
$contextProvider = new ContextProvider();
$httpClient = $this->getMock( Client::class );
$collectionsService = new CollectionsService(
4, $authService, $contextProvider, $httpClient, new SimpleDateTimeProvider()
);
$this->getController = new GetController(
$objectsService, $collectionsService, $authService
);
}
public function testItRendersPersistedObject()
{
$request = Request::create( 'https://example.com/objects/1' );

View File

@ -1,12 +1,13 @@
<?php
namespace ActivityPub\Test\Controllers;
use ActivityPub\Activities\InboxActivityEvent;
use ActivityPub\Activities\OutboxActivityEvent;
use ActivityPub\Controllers\PostController;
use ActivityPub\Objects\ObjectsService;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use Symfony\Component\EventDispatcher\EventDispatcher;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
@ -15,48 +16,10 @@ use Symfony\Component\HttpKernel\Exception\UnauthorizedHttpException;
class PostControllerTest extends APTestCase
{
private static function getObjects()
{
return array(
'https://example.com/actor/1/inbox' => array(
'id' => 'https://example.com/actor/1/inbox',
),
'https://example.com/actor/1/outbox' => array(
'id' => 'https://example.com/actor/1/outbox',
),
'https://example.com/actor/1' => array(
'id' => 'https://example.com/actor/1',
'inbox' => array(
'id' => 'https://example.com/actor/1/inbox',
),
'outbox' => array(
'id' => 'https://example.com/actor/1/outbox',
),
),
'https://elsewhere.com/actor/1' => array(
'id' => 'https://elsewhere.com/actor/1',
),
);
}
private static function getRefs()
{
return array(
'https://example.com/actor/1/inbox' => array(
'field' => 'inbox',
'referencingObject' => 'https://example.com/actor/1',
),
'https://example.com/actor/1/outbox' => array(
'field' => 'outbox',
'referencingObject' => 'https://example.com/actor/1',
),
);
}
/**
* @var array
*/
private $objects;
/**
* @var array
*/
@ -68,9 +31,9 @@ class PostControllerTest extends APTestCase
$this->refs = self::getRefs();
$objectsService = $this->getMock( ObjectsService::class );
$objectsService->method( 'query' )->will(
$this->returnCallback( function( $query ) {
$this->returnCallback( function ( $query ) {
if ( array_key_exists( 'id', $query ) &&
array_key_exists( $query['id'], $this->objects ) ) {
array_key_exists( $query['id'], $this->objects ) ) {
$object = TestActivityPubObject::fromArray(
$this->objects[$query['id']]
);
@ -89,7 +52,7 @@ class PostControllerTest extends APTestCase
} )
);
$objectsService->method( 'dereference' )->will(
$this->returnCallback( function( $id ) {
$this->returnCallback( function ( $id ) {
if ( array_key_exists( $id, $this->objects ) ) {
return TestActivityPubObject::fromArray( $this->objects[$id] );
} else {
@ -235,14 +198,14 @@ class PostControllerTest extends APTestCase
);
foreach ( $testCases as $testCase ) {
$eventDispatcher = $this->getMockBuilder( EventDispatcher::class )
->setMethods( array( 'dispatch' ) )
->getMock();
->setMethods( array( 'dispatch' ) )
->getMock();
if ( array_key_exists( 'expectedEvent', $testCase ) ) {
$eventDispatcher->expects( $this->once() )
->method( 'dispatch' )
->with(
$this->equalTo($testCase['expectedEventName']),
$this->equalTo($testCase['expectedEvent'])
$this->equalTo( $testCase['expectedEventName'] ),
$this->equalTo( $testCase['expectedEvent'] )
);
}
$postController = new PostController( $eventDispatcher, $objectsService );
@ -254,6 +217,44 @@ class PostControllerTest extends APTestCase
}
}
private static function getObjects()
{
return array(
'https://example.com/actor/1/inbox' => array(
'id' => 'https://example.com/actor/1/inbox',
),
'https://example.com/actor/1/outbox' => array(
'id' => 'https://example.com/actor/1/outbox',
),
'https://example.com/actor/1' => array(
'id' => 'https://example.com/actor/1',
'inbox' => array(
'id' => 'https://example.com/actor/1/inbox',
),
'outbox' => array(
'id' => 'https://example.com/actor/1/outbox',
),
),
'https://elsewhere.com/actor/1' => array(
'id' => 'https://elsewhere.com/actor/1',
),
);
}
private static function getRefs()
{
return array(
'https://example.com/actor/1/inbox' => array(
'field' => 'inbox',
'referencingObject' => 'https://example.com/actor/1',
),
'https://example.com/actor/1/outbox' => array(
'field' => 'outbox',
'referencingObject' => 'https://example.com/actor/1',
),
);
}
private function makeRequest( $uri, $method, $body, $attributes )
{
$request = Request::create(

View File

@ -1,11 +1,12 @@
<?php
namespace ActivityPub\Test\Crypto;
use DateTime;
use ActivityPub\Crypto\HttpSignatureService;
use ActivityPub\Test\TestUtils\TestDateTimeProvider;
use GuzzleHttp\Psr7\Request as PsrRequest;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Test\TestUtils\TestDateTimeProvider;
use DateTime;
use GuzzleHttp\Psr7\Request as PsrRequest;
use Symfony\Component\HttpFoundation\Request;
class HttpSignatureServiceTest extends APTestCase
@ -48,42 +49,6 @@ G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
$this->httpSignatureService = new HttpSignatureService( $dateTimeProvider );
}
private static function getSymfonyRequest()
{
$request = Request::create(
'https://example.com/foo?param=value&pet=dog',
Request::METHOD_POST,
array(),
array(),
array(),
array(),
'{"hello": "world"}'
);
$request->headers->set( 'host', 'example.com' );
$request->headers->set( 'content-type', 'application/json' );
$request->headers->set(
'digest', 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE='
);
$request->headers->set( 'content-length', 18 );
$request->headers->set( 'date', 'Sun, 05 Jan 2014 21:31:40 GMT' );
return $request;
}
private static function getPsrRequest()
{
$headers = array(
'Host' => 'example.com',
'Content-Type' => 'application/json',
'Digest' => 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=',
'Content-Length' => 18,
'Date' => 'Sun, 05 Jan 2014 21:31:40 GMT'
);
$body = '{"hello": "world"}';
return new PsrRequest(
'POST', 'https://example.com/foo?param=value&pet=dog', $headers, $body
);
}
public function testItVerifies()
{
$testCases = array(
@ -138,7 +103,7 @@ G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
'id' => 'headerMissing',
'headers' => array(
'Authorization' => 'Signature keyId="Test",algorithm="rsa-sha256",headers="(request-target) host date content-type digest content-length x-foo-header",signature="vSdrb+dS3EceC9bcwHSo4MlyKS59iFIrhgYkz8+oVLEEzmYZZvRs8rgOp+63LEM3v+MFHB32NfpB2bEKBIvB1q52LaEUHFv120V01IL+TAD48XaERZFukWgHoBTLMhYS2Gb51gWxpeIq8knRmPnYePbF5MOkR0Zkly4zKH7s1dE="',
),
),
'expectedResult' => false,
),
array(
@ -194,6 +159,27 @@ G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
}
}
private static function getSymfonyRequest()
{
$request = Request::create(
'https://example.com/foo?param=value&pet=dog',
Request::METHOD_POST,
array(),
array(),
array(),
array(),
'{"hello": "world"}'
);
$request->headers->set( 'host', 'example.com' );
$request->headers->set( 'content-type', 'application/json' );
$request->headers->set(
'digest', 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE='
);
$request->headers->set( 'content-length', 18 );
$request->headers->set( 'date', 'Sun, 05 Jan 2014 21:31:40 GMT' );
return $request;
}
public function testItSigns()
{
$testCases = array(
@ -223,7 +209,7 @@ G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
$request, self::PRIVATE_KEY, $testCase['keyId'], $testCase['headers']
);
} else {
$actual= $this->httpSignatureService->sign(
$actual = $this->httpSignatureService->sign(
$request, self::PRIVATE_KEY, $testCase['keyId']
);
}
@ -232,5 +218,20 @@ G6aFKaqQfOXKCyWoUiVknQJAXrlgySFci/2ueKlIE1QqIiLSZ8V8OlpFLRnb1pzI
);
}
}
private static function getPsrRequest()
{
$headers = array(
'Host' => 'example.com',
'Content-Type' => 'application/json',
'Digest' => 'SHA-256=X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=',
'Content-Length' => 18,
'Date' => 'Sun, 05 Jan 2014 21:31:40 GMT'
);
$body = '{"hello": "world"}';
return new PsrRequest(
'POST', 'https://example.com/foo?param=value&pet=dog', $headers, $body
);
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Test\Crypto;
use ActivityPub\Crypto\RsaKeypair;

View File

@ -2,13 +2,13 @@
namespace ActivityPub\Test\Entities;
use ActivityPub\Utils\DateTimeProvider;
use DateTime;
use ActivityPub\Entities\ActivityPubObject;
use ActivityPub\Database\PrefixNamingStrategy;
use ActivityPub\Entities\ActivityPubObject;
use ActivityPub\Test\TestConfig\ArrayDataSet;
use ActivityPub\Test\TestConfig\SQLiteTestCase;
use ActivityPub\Test\TestUtils\TestDateTimeProvider;
use ActivityPub\Utils\DateTimeProvider;
use DateTime;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Setup;
@ -23,40 +23,6 @@ class EntityTest extends SQLiteTestCase
*/
protected $dateTimeProvider;
protected function getDataSet()
{
return new ArrayDataSet( array(
'objects' => array(),
'fields' => array(),
'keys' => array(),
) );
}
protected function setUp()
{
parent::setUp();
$dbConfig = Setup::createAnnotationMetadataConfiguration(
array( __DIR__ . '/../../src/Entities' ), true
);
$namingStrategy = new PrefixNamingStrategy( '' );
$dbConfig->setNamingStrategy( $namingStrategy );
$dbParams = array(
'driver' => 'pdo_sqlite',
'path' => $this->getDbPath(),
);
$this->entityManager = EntityManager::create( $dbParams, $dbConfig );
$this->dateTimeProvider = new TestDateTimeProvider( array(
'objects-service.create' => new DateTime( "12:00" ),
'objects-service.update' => new DateTime( "12:01" ),
) );
}
private function getTime( $context ) {
return $this->dateTimeProvider
->getTime( $context )
->format( "Y-m-d H:i:s" );
}
public function testItCreatesAnObjectWithAPrivateKey()
{
$object = new ActivityPubObject( $this->dateTimeProvider->getTime( 'objects-service.create' ) );
@ -85,6 +51,13 @@ class EntityTest extends SQLiteTestCase
$this->assertTablesEqual( $expectedKeysTable, $keysQueryTable );
}
private function getTime( $context )
{
return $this->dateTimeProvider
->getTime( $context )
->format( "Y-m-d H:i:s" );
}
public function itUpdatesAPrivateKey()
{
$object = new ActivityPubObject( $this->dateTimeProvider->getTime( 'objects-service.create' ) );
@ -116,5 +89,33 @@ class EntityTest extends SQLiteTestCase
$this->assertTablesEqual( $expectedObjectsTable, $objectsQueryTable );
$this->assertTablesEqual( $expectedKeysTable, $keysQueryTable );
}
protected function getDataSet()
{
return new ArrayDataSet( array(
'objects' => array(),
'fields' => array(),
'keys' => array(),
) );
}
protected function setUp()
{
parent::setUp();
$dbConfig = Setup::createAnnotationMetadataConfiguration(
array( __DIR__ . '/../../src/Entities' ), true
);
$namingStrategy = new PrefixNamingStrategy( '' );
$dbConfig->setNamingStrategy( $namingStrategy );
$dbParams = array(
'driver' => 'pdo_sqlite',
'path' => $this->getDbPath(),
);
$this->entityManager = EntityManager::create( $dbParams, $dbConfig );
$this->dateTimeProvider = new TestDateTimeProvider( array(
'objects-service.create' => new DateTime( "12:00" ),
'objects-service.update' => new DateTime( "12:01" ),
) );
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Test\Http;
use ActivityPub\Controllers\GetController;
@ -29,7 +30,7 @@ class RouterTest extends APTestCase
$this->router = new Router( $this->getController, $this->postController );
$this->kernel = $this->getMock( HttpKernel::class );
}
public function testRouter()
{
$testCases = array(
@ -49,7 +50,7 @@ class RouterTest extends APTestCase
'expectedException' => MethodNotAllowedHttpException::class,
),
);
foreach( $testCases as $testCase ) {
foreach ( $testCases as $testCase ) {
$request = $testCase['request'];
$event = new GetResponseEvent(
$this->kernel, $request, HttpKernelInterface::MASTER_REQUEST

View File

@ -1,14 +1,15 @@
<?php
namespace ActivityPub\Test\Objects;
use ActivityPub\Utils\SimpleDateTimeProvider;
use ActivityPub\Auth\AuthService;
use ActivityPub\Objects\ContextProvider;
use ActivityPub\Objects\CollectionsService;
use ActivityPub\Objects\ContextProvider;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Test\TestUtils\TestActivityPubObject;
use ActivityPub\Utils\SimpleDateTimeProvider;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response as Psr7Response;
use ActivityPub\Test\TestConfig\APTestCase;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;

View File

@ -1,10 +1,11 @@
<?php
namespace ActivityPub\Test\Objects;
use ActivityPub\Objects\IdProvider;
use ActivityPub\Objects\ObjectsService;
use ActivityPub\Utils\RandomProvider;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Utils\RandomProvider;
use Symfony\Component\HttpFoundation\Request;
class IdProviderTest extends APTestCase
@ -17,7 +18,7 @@ class IdProviderTest extends APTestCase
{
$this->objectsService = $this->getMock( ObjectsService::class );
$this->objectsService->method( 'query' )
->will( $this->returnCallback( function( $query) {
->will( $this->returnCallback( function ( $query ) {
$existsId = sprintf(
'https://example.com/ap/objects/%s', self::EXISTING_ID_STR
);

View File

@ -2,16 +2,16 @@
namespace ActivityPub\Test\Objects;
use ActivityPub\Utils\DateTimeProvider;
use DateTime;
use BadMethodCallException;
use ActivityPub\Test\TestConfig\SQLiteTestCase;
use ActivityPub\Test\TestConfig\ArrayDataSet;
use ActivityPub\Entities\ActivityPubObject;
use Doctrine\ORM\EntityManager;
use ActivityPub\Objects\ObjectsService;
use ActivityPub\Database\PrefixNamingStrategy;
use ActivityPub\Entities\ActivityPubObject;
use ActivityPub\Objects\ObjectsService;
use ActivityPub\Test\TestConfig\ArrayDataSet;
use ActivityPub\Test\TestConfig\SQLiteTestCase;
use ActivityPub\Test\TestUtils\TestDateTimeProvider;
use ActivityPub\Utils\DateTimeProvider;
use BadMethodCallException;
use DateTime;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Setup;
use GuzzleHttp\Client;
use GuzzleHttp\Psr7\Response;
@ -35,42 +35,6 @@ class ObjectsServiceTest extends SQLiteTestCase
*/
protected $httpClient;
protected function getDataSet()
{
return new ArrayDataSet( array( 'objects' => array(), 'fields' => array() ) );
}
protected function setUp()
{
parent::setUp();
$dbConfig = Setup::createAnnotationMetadataConfiguration(
array( __DIR__ . '/../../src/Entities' ), true
);
$namingStrategy = new PrefixNamingStrategy( '' );
$dbConfig->setNamingStrategy( $namingStrategy );
$dbParams = array(
'driver' => 'pdo_sqlite',
'path' => $this->getDbPath(),
);
$this->entityManager = EntityManager::create( $dbParams, $dbConfig );
$this->dateTimeProvider = new TestDateTimeProvider( array(
'objects-service.create' => new DateTime( "12:00" ),
'objects-service.update' => new DateTime( "12:01" ),
) );
$this->httpClient = $this->getMock( Client::class );
$this->httpClient->method( 'send' )
->willReturn( new Response( 404 ) );
$this->objectsService = new ObjectsService(
$this->entityManager, $this->dateTimeProvider, $this->httpClient
);
}
private function getTime( $context ) {
return $this->dateTimeProvider
->getTime( $context )
->format( "Y-m-d H:i:s" );
}
public function testItCreatesObject()
{
$fields = array(
@ -114,8 +78,8 @@ class ObjectsServiceTest extends SQLiteTestCase
),
),
) );
$expectedObjectsTable = $expected->getTable('objects');
$expectedFieldsTable = $expected->getTable('fields');
$expectedObjectsTable = $expected->getTable( 'objects' );
$expectedFieldsTable = $expected->getTable( 'fields' );
$objectsQueryTable = $this->getConnection()->createQueryTable(
'objects', 'SELECT * FROM objects'
);
@ -126,6 +90,13 @@ class ObjectsServiceTest extends SQLiteTestCase
$this->assertTablesEqual( $expectedFieldsTable, $fieldsQueryTable );
}
private function getTime( $context )
{
return $this->dateTimeProvider
->getTime( $context )
->format( "Y-m-d H:i:s" );
}
public function testObjectFieldsSet()
{
$fields = array(
@ -241,8 +212,8 @@ class ObjectsServiceTest extends SQLiteTestCase
),
),
) );
$expectedObjectsTable = $expected->getTable('objects');
$expectedFieldsTable = $expected->getTable('fields');
$expectedObjectsTable = $expected->getTable( 'objects' );
$expectedFieldsTable = $expected->getTable( 'fields' );
$objectsQueryTable = $this->getConnection()->createQueryTable(
'objects', 'SELECT * FROM objects'
);
@ -333,8 +304,8 @@ class ObjectsServiceTest extends SQLiteTestCase
),
),
) );
$expectedObjectsTable = $expected->getTable('objects');
$expectedFieldsTable = $expected->getTable('fields');
$expectedObjectsTable = $expected->getTable( 'objects' );
$expectedFieldsTable = $expected->getTable( 'fields' );
$objectsQueryTable = $this->getConnection()->createQueryTable(
'objects', 'SELECT * FROM objects'
);
@ -380,11 +351,11 @@ class ObjectsServiceTest extends SQLiteTestCase
$now = $this->getTime( 'objects-service.create' );
$this->objectsService->persist( $fields );
$expected = new ArrayDataSet( array(
'objects' => array(
array( 'id' => 1, 'created' => $now, 'lastUpdated' => $now ),
array( 'id' => 2, 'created' => $now, 'lastUpdated' => $now ),
array( 'id' => 3, 'created' => $now, 'lastUpdated' => $now ),
array( 'id' => 4, 'created' => $now, 'lastUpdated' => $now ),
'objects' => array(
array( 'id' => 1, 'created' => $now, 'lastUpdated' => $now ),
array( 'id' => 2, 'created' => $now, 'lastUpdated' => $now ),
array( 'id' => 3, 'created' => $now, 'lastUpdated' => $now ),
array( 'id' => 4, 'created' => $now, 'lastUpdated' => $now ),
),
'fields' => array(
array(
@ -488,8 +459,8 @@ class ObjectsServiceTest extends SQLiteTestCase
),
),
) );
$expectedObjectsTable = $expected->getTable('objects');
$expectedFieldsTable = $expected->getTable('fields');
$expectedObjectsTable = $expected->getTable( 'objects' );
$expectedFieldsTable = $expected->getTable( 'fields' );
$objectsQueryTable = $this->getConnection()->createQueryTable(
'objects', 'SELECT * FROM objects'
);
@ -714,12 +685,12 @@ class ObjectsServiceTest extends SQLiteTestCase
public function testItDoesNotStoreObjectsWithTheSameId()
{
$fieldsOne = array(
$fieldsOne = array(
'id' => 'https://example.com/notes/1',
'type' => 'Note',
'content' => 'This is a note',
);
$fieldsTwo = array(
);
$fieldsTwo = array(
'id' => 'https://example.com/notes/1',
'type' => 'Note',
'content' => 'This is another note',
@ -762,8 +733,8 @@ class ObjectsServiceTest extends SQLiteTestCase
),
),
) );
$expectedObjectsTable = $expected->getTable('objects');
$expectedFieldsTable = $expected->getTable('fields');
$expectedObjectsTable = $expected->getTable( 'objects' );
$expectedFieldsTable = $expected->getTable( 'fields' );
$objectsQueryTable = $this->getConnection()->createQueryTable(
'objects', 'SELECT * FROM objects'
);
@ -849,8 +820,8 @@ class ObjectsServiceTest extends SQLiteTestCase
),
),
) );
$expectedObjectsTable = $expected->getTable('objects');
$expectedFieldsTable = $expected->getTable('fields');
$expectedObjectsTable = $expected->getTable( 'objects' );
$expectedFieldsTable = $expected->getTable( 'fields' );
$objectsQueryTable = $this->getConnection()->createQueryTable(
'objects', 'SELECT * FROM objects'
);
@ -953,8 +924,8 @@ class ObjectsServiceTest extends SQLiteTestCase
),
),
) );
$expectedObjectsTable = $expected->getTable('objects');
$expectedFieldsTable = $expected->getTable('fields');
$expectedObjectsTable = $expected->getTable( 'objects' );
$expectedFieldsTable = $expected->getTable( 'fields' );
$objectsQueryTable = $this->getConnection()->createQueryTable(
'objects', 'SELECT * FROM objects'
);
@ -1054,8 +1025,8 @@ class ObjectsServiceTest extends SQLiteTestCase
),
),
) );
$expectedObjectsTable = $expected->getTable('objects');
$expectedFieldsTable = $expected->getTable('fields');
$expectedObjectsTable = $expected->getTable( 'objects' );
$expectedFieldsTable = $expected->getTable( 'fields' );
$objectsQueryTable = $this->getConnection()->createQueryTable(
'objects', 'SELECT * FROM objects'
);
@ -1107,8 +1078,8 @@ class ObjectsServiceTest extends SQLiteTestCase
),
),
) );
$expectedObjectsTable = $expected->getTable('objects');
$expectedFieldsTable = $expected->getTable('fields');
$expectedObjectsTable = $expected->getTable( 'objects' );
$expectedFieldsTable = $expected->getTable( 'fields' );
$objectsQueryTable = $this->getConnection()->createQueryTable(
'objects', 'SELECT * FROM objects'
);
@ -1279,14 +1250,46 @@ class ObjectsServiceTest extends SQLiteTestCase
if ( array_key_exists( 'expectedQueryResults', $testCase ) ) {
foreach ( $testCase['expectedQueryResults'] as $expectedQueryResult ) {
$result = array_map(
function( ActivityPubObject $obj ) { return $obj->asArray(); },
function ( ActivityPubObject $obj ) {
return $obj->asArray();
},
$this->objectsService->query( $expectedQueryResult['query'] )
);
$this->assertEquals( $expectedQueryResult['expectedResult'], $result );
}
}
}
}
}
protected function setUp()
{
parent::setUp();
$dbConfig = Setup::createAnnotationMetadataConfiguration(
array( __DIR__ . '/../../src/Entities' ), true
);
$namingStrategy = new PrefixNamingStrategy( '' );
$dbConfig->setNamingStrategy( $namingStrategy );
$dbParams = array(
'driver' => 'pdo_sqlite',
'path' => $this->getDbPath(),
);
$this->entityManager = EntityManager::create( $dbParams, $dbConfig );
$this->dateTimeProvider = new TestDateTimeProvider( array(
'objects-service.create' => new DateTime( "12:00" ),
'objects-service.update' => new DateTime( "12:01" ),
) );
$this->httpClient = $this->getMock( Client::class );
$this->httpClient->method( 'send' )
->willReturn( new Response( 404 ) );
$this->objectsService = new ObjectsService(
$this->entityManager, $this->dateTimeProvider, $this->httpClient
);
}
protected function getDataSet()
{
return new ArrayDataSet( array( 'objects' => array(), 'fields' => array() ) );
}
}

View File

@ -1,12 +1,13 @@
<?php
namespace ActivityPub\Test\TestConfig;
use PHPUnit\Framework\TestCase;
abstract class APTestCase extends TestCase
{
function getMock($originalClassName, $methods = array(), array $arguments = array(), $mockClassName = '', $callOriginalConstructor = false, $callOriginalClone = false, $callAutoload = true, $cloneArguments = false, $callOriginalMethods = false, $proxyTarget = null)
function getMock( $originalClassName, $methods = array(), array $arguments = array(), $mockClassName = '', $callOriginalConstructor = false, $callOriginalClone = false, $callAutoload = true, $cloneArguments = false, $callOriginalMethods = false, $proxyTarget = null )
{
return parent::getMock($originalClassName, $methods, $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods, $proxyTarget); // TODO: Change the autogenerated stub
return parent::getMock( $originalClassName, $methods, $arguments, $mockClassName, $callOriginalConstructor, $callOriginalClone, $callAutoload, $cloneArguments, $callOriginalMethods, $proxyTarget ); // TODO: Change the autogenerated stub
}
}

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Test\TestConfig;
use InvalidArgumentException;
@ -13,35 +14,35 @@ class ArrayDataSet extends \PHPUnit_Extensions_Database_DataSet_AbstractDataSet
/**
* @param array $data
*/
public function __construct(array $data)
public function __construct( array $data )
{
foreach ($data as $tableName => $rows) {
foreach ( $data as $tableName => $rows ) {
$columns = [];
if (isset($rows[0])) {
$columns = array_keys($rows[0]);
if ( isset( $rows[0] ) ) {
$columns = array_keys( $rows[0] );
}
$metaData = new \PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData($tableName, $columns);
$table = new \PHPUnit_Extensions_Database_DataSet_DefaultTable($metaData);
$metaData = new \PHPUnit_Extensions_Database_DataSet_DefaultTableMetaData( $tableName, $columns );
$table = new \PHPUnit_Extensions_Database_DataSet_DefaultTable( $metaData );
foreach ($rows as $row) {
$table->addRow($row);
foreach ( $rows as $row ) {
$table->addRow( $row );
}
$this->tables[$tableName] = $table;
}
}
protected function createIterator($reverse = false)
public function getTable( $tableName )
{
return new \PHPUnit_Extensions_Database_DataSet_DefaultTableIterator($this->tables, $reverse);
}
public function getTable($tableName)
{
if (!isset($this->tables[$tableName])) {
throw new InvalidArgumentException("$tableName is not a table in the current database.");
if ( !isset( $this->tables[$tableName] ) ) {
throw new InvalidArgumentException( "$tableName is not a table in the current database." );
}
return $this->tables[$tableName];
}
protected function createIterator( $reverse = false )
{
return new \PHPUnit_Extensions_Database_DataSet_DefaultTableIterator( $this->tables, $reverse );
}
}

View File

@ -16,36 +16,6 @@ abstract class SQLiteTestCase extends APTestCase
private $conn = null;
private $dbPath = '';
protected function setUp()
{
parent::setUp();
$dbPath = $this->getDbPath();
if ( file_exists( $dbPath ) ) {
unlink( $dbPath );
}
$config = ActivityPubConfig::createBuilder()
->setDbConnectionParams( array(
'driver' => 'pdo_sqlite',
'path' => $dbPath,
) )
->build();
$activityPub = new ActivityPub( $config );
$activityPub->updateSchema();
}
protected function tearDown()
{
parent::tearDown();
unlink( $this->getDbPath() );
unset( $this->conn );
unset( $this->pdo );
}
protected function getDbPath()
{
return dirname( __FILE__ ) . '/db.sqlite';
}
final public function getConnection()
{
if ( $this->conn === null ) {
@ -57,5 +27,35 @@ abstract class SQLiteTestCase extends APTestCase
}
return $this->conn;
}
protected function getDbPath()
{
return dirname( __FILE__ ) . '/db.sqlite';
}
protected function setUp()
{
parent::setUp();
$dbPath = $this->getDbPath();
if ( file_exists( $dbPath ) ) {
unlink( $dbPath );
}
$config = ActivityPubConfig::createBuilder()
->setDbConnectionParams( array(
'driver' => 'pdo_sqlite',
'path' => $dbPath,
) )
->build();
$activityPub = new ActivityPub( $config );
$activityPub->updateSchema();
}
protected function tearDown()
{
parent::tearDown();
unlink( $this->getDbPath() );
unset( $this->conn );
unset( $this->pdo );
}
}

View File

@ -11,21 +11,39 @@ use DateTime;
*/
class TestActivityPubObject extends ActivityPubObject
{
public static function getDefaultTime() {
private $fixedTime;
public function __construct( DateTime $time = null )
{
if ( !$time ) {
$time = self::getDefaultTime();
}
$this->fixedTime = $time;
parent::__construct( $time );
}
public static function getDefaultTime()
{
return DateTime::createFromFormat(
DateTime::RFC2822, 'Sun, 05 Jan 2014 21:31:40 GMT'
);
}
private $fixedTime;
public function __construct( DateTime $time = null )
public static function fromArray( array $arr, DateTime $time = null )
{
if ( ! $time ) {
if ( !$time ) {
$time = self::getDefaultTime();
}
$this->fixedTime = $time;
parent::__construct( $time );
$object = new TestActivityPubObject( $time );
foreach ( $arr as $name => $value ) {
if ( is_array( $value ) ) {
$child = self::fromArray( $value, $time );
TestField::withObject( $object, $name, $child, $time );
} else {
TestField::withValue( $object, $name, $value, $time );
}
}
return $object;
}
public function addField( Field $field, DateTime $time = null )
@ -44,22 +62,5 @@ class TestActivityPubObject extends ActivityPubObject
{
// do not change lastUpdated
}
public static function fromArray( array $arr, DateTime $time = null )
{
if ( ! $time ) {
$time = self::getDefaultTime();
}
$object = new TestActivityPubObject( $time );
foreach ( $arr as $name => $value ) {
if ( is_array( $value ) ) {
$child = self::fromArray( $value, $time );
TestField::withObject( $object, $name, $child, $time );
} else {
TestField::withValue( $object, $name, $value, $time );
}
}
return $object;
}
}

View File

@ -2,8 +2,8 @@
namespace ActivityPub\Test\TestUtils;
use DateTime;
use ActivityPub\Utils\DateTimeProvider;
use DateTime;
/**
* A DateTimeProvider that returns fixed values for create and update times
@ -19,10 +19,10 @@ class TestDateTimeProvider implements DateTimeProvider
{
$this->context = $context;
}
public function getTime( $context = '' )
{
if ( array_key_exists( $context, $this->context )) {
if ( array_key_exists( $context, $this->context ) ) {
return $this->context[$context];
} else {
return new DateTime( 'now' );

View File

@ -1,4 +1,5 @@
<?php
namespace ActivityPub\Test\TestUtils;
use ActivityPub\Entities\ActivityPubObject;
@ -14,7 +15,7 @@ class TestField extends Field
protected function __construct( $time = null )
{
if ( ! $time ) {
if ( !$time ) {
$time = TestActivityPubObject::getDefaultTime();
}
parent::__construct( $time );

View File

@ -1,8 +1,9 @@
<?php
namespace ActivityPub\Test\Utils;
use ActivityPub\Utils\Util;
use ActivityPub\Test\TestConfig\APTestCase;
use ActivityPub\Utils\Util;
class UtilTest extends APTestCase
{