diff --git a/composer.json b/composer.json index 87933b6..32b1b32 100644 --- a/composer.json +++ b/composer.json @@ -16,6 +16,7 @@ "docs": "phpdoc -d ./src -t ./docs" }, "require": { + "php": "^5.5 || ^7.0", "ext-json": "*", "cache/apc-adapter": "0.3.2", "cache/apcu-adapter": "0.2.2", @@ -31,6 +32,7 @@ "monolog/monolog": "^1.0", "phpseclib/phpseclib": "^2.0", "psr/http-message": "^1.0", + "ramsey/uuid": "3.8.0", "symfony/dependency-injection": "^3.4", "symfony/event-dispatcher": "^3.4", "symfony/http-foundation": "^3.4", diff --git a/src/JsonLd/JsonLdGraph.php b/src/JsonLd/JsonLdGraph.php index 16b6953..fcd622b 100644 --- a/src/JsonLd/JsonLdGraph.php +++ b/src/JsonLd/JsonLdGraph.php @@ -2,7 +2,9 @@ namespace ActivityPub\JsonLd; +use ActivityPub\Utils\UuidProvider; use InvalidArgumentException; +use Ramsey\Uuid\Uuid; /** * A view of a JSON-LD graph. Maps ids to JsonLdNode instances. @@ -11,27 +13,27 @@ use InvalidArgumentException; */ class JsonLdGraph { - /** - * @var int - */ - private $nextBlankId; - /** * @var array */ private $graph; - public function __construct() + /** + * @var UuidProvider + */ + private $uuidProvider; + + public function __construct( UuidProvider $uuidProvider ) { - $this->nextBlankId = 0; $this->graph = array(); + $this->uuidProvider = $uuidProvider; } public function addNode( JsonLdNode $node ) { $id = $node->getId(); if ( is_null( $id ) ) { - $id = $this->getNextBlankId(); + $id = $this->uuidProvider->uuid(); $node->setId( $id ); } $this->graph[$id] = $node; @@ -54,11 +56,4 @@ class JsonLdGraph $this->graph[$newNodeName] = $this->graph[$blankNodeName]; unset( $this->graph[$blankNodeName] ); } - - private function getNextBlankId() - { - $nextId = $this->nextBlankId; - $this->nextBlankId += 1; - return "_:b$nextId"; - } } \ No newline at end of file diff --git a/src/JsonLd/JsonLdNode.php b/src/JsonLd/JsonLdNode.php index ca2b5c8..88b7ca8 100644 --- a/src/JsonLd/JsonLdNode.php +++ b/src/JsonLd/JsonLdNode.php @@ -5,10 +5,14 @@ namespace ActivityPub\JsonLd; use ActivityPub\JsonLd\Dereferencer\DereferencerInterface; use ActivityPub\JsonLd\Exceptions\NodeNotFoundException; use ActivityPub\JsonLd\Exceptions\PropertyNotDefinedException; +use ActivityPub\JsonLd\TripleStore\TypedRdfTriple; use ArrayAccess; use BadMethodCallException; use InvalidArgumentException; use ML\JsonLD\JsonLD; +use ML\JsonLD\TypedValue; +use ML\JsonLD\Value; +use Psr\Log\LoggerInterface; use stdClass; class JsonLdNode implements ArrayAccess @@ -42,6 +46,11 @@ class JsonLdNode implements ArrayAccess */ private $dereferencer; + /** + * @var LoggerInterface + */ + private $logger; + /** * This node's view of the JSON-LD graph. * @var JsonLdGraph @@ -63,17 +72,21 @@ class JsonLdNode implements ArrayAccess * @param string|array|stdClass $context The JSON-LD context. * @param JsonLdNodeFactory $factory The factory used to construct this instance. * @param DereferencerInterface $dereferencer + * @param LoggerInterface $logger * @param JsonLdGraph $graph The JSON-LD graph this node is a part of. + * @param array $backreferences */ public function __construct( $jsonLd, $context, JsonLdNodeFactory $factory, DereferencerInterface $dereferencer, + LoggerInterface $logger, JsonLdGraph $graph, $backreferences = array() ) { $this->factory = $factory; $this->dereferencer = $dereferencer; + $this->logger = $logger; if ( $jsonLd == new stdClass() ) { $this->expanded = new stdClass(); } else { @@ -90,8 +103,8 @@ class JsonLdNode implements ArrayAccess } /** - * Gets this node's id, if it has one. Could be null or a temporary id if this is a blank node. - * @return string|null + * Gets this node's id. Could be a temporary id if this is a blank node. + * @return string */ public function getId() { @@ -338,6 +351,52 @@ class JsonLdNode implements ArrayAccess $this->backreferences[$expandedName][] = $referencingNode; } + /** + * Returns the node expressed as an array of RdfTriples. + * @return TypedRdfTriple[] + */ + public function toRdfTriples() + { + $cloned = clone $this->expanded; + // First serialize this node + $quads = JsonLD::toRdf( $cloned ); + $triples = array(); + foreach ( $quads as $quad ) { + if ( (string)$quad->getSubject() === $this->getId() ) { + $objectType = null; + if ( $quad->getObject() instanceof Value ) { + $object = $quad->getObject()->getValue(); + if ( $quad->getObject() instanceof TypedValue ) { + $objectType = $quad->getObject()->getType(); + } + } else { + $objectIri = $quad->getObject(); + if ( $objectIri->getScheme() === '_' ) { + // TODO resolve the associated value to force the generation of a UUID, then set $object to the uuid + } else { + $object = (string)$quad->getObject(); + } + $objectType = '@id'; + } + $triples[] = TypedRdfTriple::create( + (string)$quad->getSubject(), (string)$quad->getProperty(), $object, $objectType + ); + } + } + // Then serialize any sub-nodes + foreach ( $this->expanded as $name => $values ) { + if ( $name === '@id' ) { + continue; + } + foreach ( $this->getMany( $name ) as $subNode ) { + if ( $subNode instanceof JsonLdNode ) { + $triples = array_merge( $triples, $subNode->toRdfTriples() ); + } + } + } + return $triples; + } + /** * Whether a offset exists * @link https://php.net/manual/en/arrayaccess.offsetexists.php @@ -412,9 +471,11 @@ class JsonLdNode implements ArrayAccess // TODO memoize this function $dummyObj = (object) array( '@context' => $this->context, + '_dummyKey' => '_dummyValue', // Set a dummy key to ensure that @id gets properly expanded $name => '_dummyValue', ); $expanded = (array) JsonLD::expand( $dummyObj )[0]; + unset( $expanded['_:_dummyKey'] ); return array_keys( $expanded )[0]; } } \ No newline at end of file diff --git a/src/JsonLd/JsonLdNodeFactory.php b/src/JsonLd/JsonLdNodeFactory.php index c47e3b8..3f6269b 100644 --- a/src/JsonLd/JsonLdNodeFactory.php +++ b/src/JsonLd/JsonLdNodeFactory.php @@ -3,6 +3,11 @@ namespace ActivityPub\JsonLd; use ActivityPub\JsonLd\Dereferencer\DereferencerInterface; +use ActivityPub\JsonLd\TripleStore\TypedRdfTriple; +use ActivityPub\Utils\UuidProvider; +use InvalidArgumentException; +use Psr\Log\LoggerInterface; +use stdClass; /** * A factory class for constructing JsonLdNode instances @@ -23,10 +28,25 @@ class JsonLdNodeFactory */ private $dereferencer; - public function __construct( $context, DereferencerInterface $dereferencer ) + /** + * @var LoggerInterface + */ + private $logger; + + /** + * @var UuidProvider + */ + private $uuidProvider; + + public function __construct( $context, + DereferencerInterface $dereferencer, + LoggerInterface $logger, + UuidProvider $uuidProvider ) { $this->context = $context; $this->dereferencer = $dereferencer; + $this->logger = $logger; + $this->uuidProvider = $uuidProvider; } /** @@ -39,8 +59,63 @@ class JsonLdNodeFactory public function newNode( $jsonLd, $graph = null, $backreferences = array() ) { if ( is_null( $graph ) ) { - $graph = new JsonLdGraph(); + $graph = new JsonLdGraph( $this->uuidProvider ); } - return new JsonLdNode( $jsonLd, $this->context, $this, $this->dereferencer, $graph, $backreferences ); + return new JsonLdNode( + $jsonLd, $this->context, $this, $this->dereferencer, $this->logger, $graph, $backreferences + ); + } + + /** + * Constructs a JsonLdNode from a collection of RdfTriples, properly setting up the graph traversals based + * on relationships between the passed-in triples. + * + * @param TypedRdfTriple[] $triples The triples. + * @param string $rootNodeId The ID of the root node - that is, the node returned from this function. This is + * necessary because the RDF triples array can contain triples from multiple nodes. + * @param JsonLdGraph|null $graph An existing JsonLdGraph to add this node to. + */ + public function nodeFromRdf( $triples, $rootNodeId, $graph = null ) + { + if ( is_null( $graph ) ) { + $graph = new JsonLdGraph( $this->uuidProvider ); + } + $buckets = array(); + $backreferences = array(); + foreach ( $triples as $triple ) { + $buckets[$triple->getSubject()][] = $triple; + } + if ( ! array_key_exists( $rootNodeId, $buckets ) ) { + throw new InvalidArgumentException("No triple with subject $rootNodeId was found"); + } + $nodes = array(); + foreach ( $buckets as $id => $triples ) { + $obj = new stdClass(); + foreach( $triples as $triple ) { + if ( $triple->getObjectType() && $triple->getObjectType() === '@id' ) { + $obj->$triple->getPredicate()[] = (object) array( '@id' => $triple->getObject() ); + $backreferences[$triple->getObject()][] = (object) array( + 'predicate' => $triple->getPredicate(), + 'referer' => $triple->getSubject(), + ); + } else if ( $triple->getObjectType() ) { + $obj->$triple->getPredicate()[] = (object) array( + '@type' => $triple->getObjectType(), + '@value' => $triple->getObject(), + ); + } else { + $obj->$triple->getPredicate()[] = (object) array( '@value' => $triple->getObject() ); + } + } + $node = $this->newNode( $obj, $graph ); + $nodes[$node->getId()] = $node; + } + foreach ( $backreferences as $referencedId => $references ) { + $referencedNode = $nodes[$referencedId]; + foreach ( $references as $reference ) { + $referencedNode->addBackReference( $reference->predicate, $nodes[$reference->referer] ); + } + } + return $nodes[$rootNodeId]; } } \ No newline at end of file diff --git a/src/JsonLd/TripleStore/Doctrine/DoctrineTriplestore.php b/src/JsonLd/TripleStore/Doctrine/DoctrineTriplestore.php new file mode 100644 index 0000000..8e5b276 --- /dev/null +++ b/src/JsonLd/TripleStore/Doctrine/DoctrineTriplestore.php @@ -0,0 +1,8 @@ +subject = $subject; + $this->predicate = $predicate; + $this->object = $object; + $this->objectType = $objectType; + } + + public static function create( $subject = null, $predicate = null, $object = null, $objectType = null ) + { + return new TypedRdfTriple( $subject, $predicate, $object, $objectType ); + } + + /** + * @return string|null + */ + public function getSubject() + { + return $this->subject; + } + + /** + * @param string $subject + * @return TypedRdfTriple + */ + public function setSubject( $subject ) + { + $this->subject = $subject; + return $this; + } + + /** + * @return string|null + */ + public function getPredicate() + { + return $this->predicate; + } + + /** + * @param string $predicate + * @return TypedRdfTriple + */ + public function setPredicate( $predicate ) + { + $this->predicate = $predicate; + return $this; + } + + /** + * @return string|null + */ + public function getObject() + { + return $this->object; + } + + /** + * @param string $object + * @return TypedRdfTriple + */ + public function setObject( $object ) + { + $this->object = $object; + return $this; + } + + /** + * @return string|null + */ + public function getObjectType() + { + return $this->objectType; + } + + /** + * @param string $objectType + * @return TypedRdfTriple + */ + public function setObjectType( $objectType ) + { + $this->objectType = $objectType; + return $this; + } + + /** + * True if this triple has a subject, a predicate, and an object. + * @return bool + */ + public function isFullySpecified() + { + return $this->getSubject() && $this->getPredicate() && $this->getObject(); + } +} \ No newline at end of file diff --git a/src/Utils/UuidProvider.php b/src/Utils/UuidProvider.php new file mode 100644 index 0000000..891fdd6 --- /dev/null +++ b/src/Utils/UuidProvider.php @@ -0,0 +1,17 @@ + array( 'https://www.w3.org/ns/activitystreams' ), + 'id' => 'https://example.org/collections/1', + 'type' => 'Collection', + 'name' => 'MyCollection', + 'published' => '2019-05-01', + 'items' => array( + 'https://example.org/collections/1/items/1', + 'https://example.org/collections/1/items/2', + ) + ), + $this->asContext, + array( + TypedRdfTriple::create( + 'https://example.org/collections/1', + 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', + 'https://www.w3.org/ns/activitystreams#Collection', + '@id' + ), + TypedRdfTriple::create( + 'https://example.org/collections/1', + 'https://www.w3.org/ns/activitystreams#items', + 'https://example.org/collections/1/items/1', + '@id' + ), + TypedRdfTriple::create( + 'https://example.org/collections/1', + 'https://www.w3.org/ns/activitystreams#items', + 'https://example.org/collections/1/items/2', + '@id' + ), + TypedRdfTriple::create( + 'https://example.org/collections/1', + 'https://www.w3.org/ns/activitystreams#name', + 'MyCollection', + 'http://www.w3.org/2001/XMLSchema#string' + ), + TypedRdfTriple::create( + 'https://example.org/collections/1', + 'https://www.w3.org/ns/activitystreams#published', + '2019-05-01', + 'http://www.w3.org/2001/XMLSchema#dateTime' + ), + TypedRdfTriple::create( + 'https://example.org/collections/1/items/1', + 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', + 'https://www.w3.org/ns/activitystreams#Note', + '@id' + ), + TypedRdfTriple::create( + 'https://example.org/collections/1/items/2', + 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', + 'https://www.w3.org/ns/activitystreams#Note', + '@id' + ), + ), + array( + 'https://example.org/collections/1/items/1' => (object) array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'id' => 'https://example.org/collections/1/items/1', + 'type' => 'Note', + ), + 'https://example.org/collections/1/items/2' => (object) array( + '@context' => array( 'https://www.w3.org/ns/activitystreams' ), + 'id' => 'https://example.org/collections/1/items/2', + 'type' => 'Note', + ), + ), + ), + array( + (object) array( + '@context' => array( + 'https://www.w3.org/ns/activitystreams', + 'https://w3id.org/security/v1', + ), + 'id' => 'https://example.org/sally', + 'type' => 'Actor', + 'publicKey' => (object) array( + 'publicKeyPem' => 'the_public_key', + ) + ), + $this->asContext, + array( + TypedRdfTriple::create( + 'https://example.org/sally', + 'http://www.w3.org/1999/02/22-rdf-syntax-ns#type', + 'https://www.w3.org/ns/activitystreams#Actor', + '@id' + ), + TypedRdfTriple::create( + 'https://example.org/sally', + 'https://w3id.org/security/v1#publicKey', + $this->uuids[0] + ), + TypedRdfTriple::create( + $this->uuids[0], + 'https://w3id.org/security/v1#publicKeyPem', + 'the_public_key', + '@id' + ), + ), + ), + ); + } + + /** + * @dataProvider provideToRdfTriple + */ + public function testToRdfTriple( $inputObj, $context, $expectedTriples, $nodeGraph = array() ) + { + $node = $this->makeJsonLdNode( $inputObj, $context, $nodeGraph ); + $triples = $node->toRdfTriples(); + $this->assertEquals( $expectedTriples, $triples ); + } + private function makeJsonLdNode( $inputObj, $context, $nodeGraph = array() ) { - $factory = new JsonLdNodeFactory( $context, new TestDereferencer( $nodeGraph ) ); + $factory = new JsonLdNodeFactory( + $context, new TestDereferencer( $nodeGraph ), new Logger(), new TestUuidProvider( $this->uuids ) + ); return $factory->newNode( $inputObj ); } } \ No newline at end of file diff --git a/test/TestUtils/TestUuidProvider.php b/test/TestUtils/TestUuidProvider.php new file mode 100644 index 0000000..f0af318 --- /dev/null +++ b/test/TestUtils/TestUuidProvider.php @@ -0,0 +1,35 @@ +uuids = $uuids; + $this->uuidIdx = 0; + } + + public function uuid() + { + $uuid = $this->uuids[$this->uuidIdx]; + $this->uuidIdx = ( $this->uuidIdx + 1 ) % count( $this->uuids ); + return $uuid; + } +} \ No newline at end of file