[WIP] Begin implementing actor creation

This commit is contained in:
Jeremy Dormitzer 2018-12-21 09:51:58 -05:00
parent dacd01a47b
commit 48f7c6205c
11 changed files with 486 additions and 4 deletions

View File

@ -0,0 +1,14 @@
<?php
namespace ActivityPub\Actors;
/**
* Represents an ActivityPub actor object
*
* This class is the main entrypoint for the ActivityPub API, via the
* post_to_inbox() and post_to_outbox() methods.
*/
class ActivityPubActor
{
// TODO
}
?>

View File

@ -0,0 +1,80 @@
<?php
namespace ActivityPub\Actors;
use ActivityPub\Crypto\RsaKeypair;
use ActivityPub\Objects\ObjectService;
use ActivityPub\Utils\Util;
use Doctrine\ORM\EntityManager;
class ActorService
{
/**
* @var ObjectService
*/
private $objectService;
/**
* @var EntityManager
*/
private $entityManager;
public function __construct( ObjectService $objectService, EntityManager entityManager )
{
$this->objectService = $objectService;
$this->entityManager = $entityManager;
}
/**
* Creates a new Actor object
*
* Also creates the actor's collections - inbox, outbox, following,
* followers, and liked - and the actor's public/private keypair.
* The collections will have ids generated by appending /<collection name>
* to the end of the actor's id, e.g. /inbox, /outbox, etc. The public key
* will have its id generated by appending #main-key to the end of the actor's
* id. The private key will be persisted and associated with the actor object.
*
* @param array $fields The actor's fields. The id and type fields are required
* @return ActivityPubActor The created Actor object
*/
public function createActor( $fields )
{
$requiredFields = array( 'id', 'type' );
if ( ! Util::arrayKeysExist( $fields, $requiredFields ) ) {
throw new InvalidArgumentException( 'Actors require id and type fields' );
}
$actorId = rtrim( $fields['id'], '/' );
$keypair = RsaKeypair::generate();
$publicKeyField = array(
'id' => "${actorId}#main-key",
'owner' => $actorId,
'publicKeyPem' => $keypair->getPublicKey(),
);
$fields['publicKey'] = $publicKeyField;
}
/**
* Returns the actor identified by $id
*
* @param string $id The actor's id
* @return ActivityPubActor The actor
*/
public function getActor( $id )
{
}
/**
* Deletes the actor identified by $id by replacing it with a Tombstone object
*
* Also deletes the actor's collections - inbox, outbox, following,
* followers, and liked - and the actor's public/private keypair.
* @param string $id The actor's id
* @return ActivityPubObject The Tombstone that the actor was replaced with
*/
public function deleteActor( $id )
{
}
}
?>

View File

@ -0,0 +1,56 @@
<?php
namespace ActivityPub\Collections;
use ActivityPub\Entities\ActivityPubObject;
use ActivityPub\Objects\ObjectsService;
class CollectionsService
{
/**
* Creates a new collection - ordered/unordered and paged/unpaged
*
* @return ActivityPubObject The created Collection object
*/
public function createCollection()
{
}
/**
* Adds the object to the collection
*
* This method handles ordered/unordered and paged/unpaged collections.
* @param string $collectionId The id of collection
* @param ActivityPubObject $object The object to add to the collection
*/
public function addToCollection( string $collectionId, ActivityPubObject $object )
{
}
/**
* Removes the object from the collection
*
* This method handles ordered/unordered and paged/unpaged collections.
* @param string $collectionId The id of the collection
* @param string $objectId The id of the object to remove
*/
public function removeFromCollection( string $collectionId, string $objectId )
{
}
/**
* Deletes the collection by replacing it with a Tombstone object
*
* None of the items in the collection will be deleted; however, if it is
* a PagedCollection then all the collection page objects will be deleted.
* @param string $collectionId The id of the collection
* @return ActivityPubObject The Tombstone that the collection was replaced with
*/
public function deleteCollection( string $collectionId )
{
}
}
?>

View File

@ -38,6 +38,16 @@ class RsaKeypair
return $this->publicKey;
}
/**
* Returns the private key as a string
*
* @return string The private key
*/
public function getPrivateKey()
{
return $this->privateKey;
}
/**
* Generates a signature for $data
*
@ -63,7 +73,7 @@ class RsaKeypair
*
* @param string $data The data
* @param string $signature The signature
* @return boolean
* @return bool
*/
public function verify( $data, $signature )
{

View File

@ -49,6 +49,13 @@ class ActivityPubObject implements ArrayAccess
*/
protected $lastUpdated;
/**
* The private key associated with this object, if any
* @OneToOne(targetEntity="PrivateKey", mappedBy="object", cascade={"all"})
* @var PrivateKey
*/
protected $privateKey;
public function __construct( DateTime $time = null ) {
if ( ! $time ) {
$time = new DateTime( "now" );
@ -254,6 +261,30 @@ class ActivityPubObject implements ArrayAccess
$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
*
* @param string $key The new private key value
*/
public function setPrivateKey( string $key )
{
if ( $this->hasPrivateKey() ) {
$this->privateKey->setKey( $key );
} else {
$this->privateKey = new PrivateKey( $key, $this );
}
}
public function offsetExists( $offset )
{
return $this->hasField( $offset );

View File

@ -0,0 +1,61 @@
<?php
namespace ActivityPub\Entities;
use ActivityPub\Entities\ActivityPubObject;
/**
* The keys table holds the private keys associated with ActivityPub actors
*
* @Entity @Table(name="keys")
*/
class PrivateKey
{
/**
* @var int
* @Id
* @Column(type="integer")
* @GeneratedValue
*/
protected $id;
/**
* The private key
*
* @var string
* @Column(type="string")
*/
protected $key;
/**
* The object associated with this private key
*
* @var ActivityPubObject
* @OneToOne(targetEntity="ActivityPubObject", inversedBy="privateKey")
*/
protected $object;
/**
* Creates a new PrivateKey
*
* Don't call this directly - instead, use ActivityPubObject->setPrivateKey()
* @param string $key The private key as a string
* @param ActivityPubObject $object The object associated with this key
*/
public function __construct( string $key, ActivityPubObject $object )
{
$this->key = $key;
$this->object = $object;
}
/**
* Sets the private key string
*
* Don't call this directly - instead, use ActivityPubObject->setPrivateKey()
* @param string $key The private key as a string
*/
public function setKey( string $key )
{
$this->key = $key;
}
}
?>

View File

@ -3,10 +3,33 @@ namespace ActivityPub\Utils;
class Util
{
public static function isAssoc(array $arr)
/**
* Returns true if the input array is associative
*
* @param array $arr The array to test
* @return bool True if the array is associative, false otherwise
*/
public static function isAssoc( array $arr )
{
if (array() === $arr) return false;
return array_keys($arr) !== range(0, count($arr) - 1);
}
/**
* Returns true if all of the specified keys exist in the array
*
* @param array $arr The array to check
* @param array $keys The keys to check the existence of
* @return bool True if all of the keys are in the array, false otherwise
*/
public static function arrayKeysExist( array $arr, array $keys )
{
foreach ( $keys as $key ) {
if ( ! array_key_exists( $key, $arr ) ) {
return false;
}
}
return true;
}
}
?>

112
test/EntityTest.php Normal file
View File

@ -0,0 +1,112 @@
<?php
namespace ActivityPub\Test;
use DateTime;
use ActivityPub\Crypto\RsaKeypair;
use ActivityPub\Entities\ActivityPubObject;
use ActivityPub\Database\PrefixNamingStrategy;
use ActivityPub\Test\Config\ArrayDataSet;
use ActivityPub\Test\Config\SQLiteTestCase;
use ActivityPub\Test\TestUtils\TestDateTimeProvider;
use Doctrine\ORM\EntityManager;
use Doctrine\ORM\Tools\Setup;
class EntityTest extends SQLiteTestCase
{
protected $entityManager;
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' => __DIR__ . '/db.sqlite',
);
$this->entityManager = EntityManager::create( $dbParams, $dbConfig );
$this->dateTimeProvider = new TestDateTimeProvider(
new DateTime( "12:00" ), 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( 'create' ) );
$privateKey = 'a private key';
$object->setPrivateKey( $privateKey );
$this->entityManager->persist( $object );
$this->entityManager->flush();
$now = $this->getTime( 'create' );
$expected = new ArrayDataSet( array(
'objects' => array(
array( 'id' => 1, 'created' => $now, 'lastUpdated' => $now ),
),
'keys' => array(
array( 'id' => 1, 'object_id' => 1, 'key' => $privateKey )
),
) );
$expectedObjectsTable = $expected->getTable( 'objects' );
$expectedKeysTable = $expected->getTable( 'keys' );
$objectsQueryTable = $this->getConnection()->createQueryTable(
'objects', 'SELECT * FROM objects'
);
$keysQueryTable = $this->getConnection()->createQueryTable(
'keys', 'SELECT * FROM keys'
);
$this->assertTablesEqual( $expectedObjectsTable, $objectsQueryTable );
$this->assertTablesEqual( $expectedKeysTable, $keysQueryTable );
}
public function itUpdatesAPrivateKey()
{
$object = new ActivityPubObject( $this->dateTimeProvider->getTime( 'create' ) );
$privateKey = 'a private key';
$object->setPrivateKey( $privateKey );
$this->entityManager->persist( $object );
$this->entityManager->flush();
$newPrivateKey = 'a new private key';
$object->setPrivateKey( $newPrivateKey );
$this->entityManager->persiste( $object );
$this->entityManager->flush();
$now = $this->getTime( 'create' );
$expected = new ArrayDataSet( array(
'objects' => array(
array( 'id' => 1, 'created' => $now, 'lastUpdated' => $now ),
),
'keys' => array(
array( 'id' => 1, 'object_id' => 1, 'key' => $newPrivateKey )
),
) );
$expectedObjectsTable = $expected->getTable( 'objects' );
$expectedKeysTable = $expected->getTable( 'keys' );
$objectsQueryTable = $this->getConnection()->createQueryTable(
'objects', 'SELECT * FROM objects'
);
$keysQueryTable = $this->getConnection()->createQueryTable(
'keys', 'SELECT * FROM keys'
);
$this->assertTablesEqual( $expectedObjectsTable, $objectsQueryTable );
$this->assertTablesEqual( $expectedKeysTable, $keysQueryTable );
}
}
?>

View File

@ -38,8 +38,12 @@ class ObjectsServiceTest extends SQLiteTestCase
'path' => __DIR__ . '/db.sqlite',
);
$this->entityManager = EntityManager::create( $dbParams, $dbConfig );
$this->dateTimeProvider = new TestDateTimeProvider( new DateTime( "12:00" ), new DateTime( "12:01" ) );
$this->objectsService = new ObjectsService( $this->entityManager, $this->dateTimeProvider );
$this->dateTimeProvider = new TestDateTimeProvider(
new DateTime( "12:00" ), new DateTime( "12:01" )
);
$this->objectsService = new ObjectsService(
$this->entityManager, $this->dateTimeProvider
);
}
private function getTime( $context ) {

View File

@ -13,6 +13,12 @@ class RsaKeypairTest extends TestCase
$keypair = RsaKeypair::generate();
$this->assertStringStartsWith( '-----BEGIN PUBLIC KEY-----', $keypair->getPublicKey() );
$this->assertStringEndsWith( '-----END PUBLIC KEY-----', $keypair->getPublicKey() );
$this->assertStringStartsWith(
'-----BEGIN RSA PRIVATE KEY-----', $keypair->getPrivateKey()
);
$this->assertStringEndsWith(
'-----END RSA PRIVATE KEY-----', $keypair->getPrivateKey()
);
}
public function testItSignsAndValidatesSignatures()

85
test/UtilTest.php Normal file
View File

@ -0,0 +1,85 @@
<?php
namespace ActivityPub\Test;
use ActivityPub\Utils\Util;
use PHPUnit\Framework\TestCase;
class UtilTest extends TestCase
{
public function testItFindsAssocArray()
{
$arr = array( 'foo' => 'bar' );
$isAssoc = Util::isAssoc( $arr );
$this->assertTrue( $isAssoc );
}
public function testItReturnsFalseForNonAssoc()
{
$arr = array( 'foo', 'bar' );
$isAssoc = Util::isAssoc( $arr );
$this->assertFalse( $isAssoc );
}
public function testItHandlesMixedArray()
{
$arr = array( 'foo' => 'bar', 'baz' );
$isAssoc = Util::isAssoc( $arr );
$this->assertTrue( $isAssoc );
}
public function testItChecksEmptyArrayIsAssoc()
{
$arr = array();
$isAssoc = Util::isAssoc( $arr );
$this->assertFalse( $isAssoc );
}
public function testArrayKeysExist()
{
$arr = array( 'foo' => 'bar', 'baz' => 'qux' );
$keys = array( 'foo', 'baz' );
$keysExist = Util::arrayKeysExist( $arr, $keys );
$this->assertTrue( $keysExist );
}
public function testItChecksForAllKeys()
{
$arr = array( 'foo' => 'bar' );
$keys = array( 'foo', 'baz' );
$keysExist = Util::arrayKeysExist( $arr, $keys );
$this->assertFalse( $keysExist );
}
public function testItAllowsExtraKeys()
{
$arr = array( 'foo' => 'bar', 'baz' => 'qux' );
$keys = array( 'foo' );
$keysExist = Util::arrayKeysExist( $arr, $keys );
$this->assertTrue( $keysExist );
}
public function testItHandlesEmptyArray()
{
$arr = array();
$keys = array( 'foo' );
$keysExist = Util::arrayKeysExist( $arr, $keys );
$this->assertFalse( $keysExist );
}
public function testItHandlesEmptyKeys()
{
$arr = array( 'foo' => 'bar', 'baz' => 'qux' );
$keys = array();
$keysExist = Util::arrayKeysExist( $arr, $keys );
$this->assertTrue( $keysExist );
}
public function testItHandlesBothEmpty()
{
$arr = array();
$keys = array();
$keysExist = Util::arrayKeysExist( $arr, $keys );
$this->assertTrue( $keysExist );
}
}
?>