diff --git a/application/Module.php b/application/Module.php index 178265f8d..dbdf45171 100644 --- a/application/Module.php +++ b/application/Module.php @@ -290,6 +290,8 @@ public function addTermDefinitionsToContext(LaminasEvent $event) } $context['o-cnt'] = 'http://www.w3.org/2011/content#'; $context['o-time'] = 'http://www.w3.org/2006/time#'; + $context['oa'] = 'http://www.w3.org/ns/oa#'; + $context['rdf'] = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#'; $event->setParam('context', $context); } diff --git a/application/src/Api/Adapter/ValueAnnotationAdapter.php b/application/src/Api/Adapter/ValueAnnotationAdapter.php index 1b8b4d04e..6c2f2ea91 100644 --- a/application/src/Api/Adapter/ValueAnnotationAdapter.php +++ b/application/src/Api/Adapter/ValueAnnotationAdapter.php @@ -1,12 +1,21 @@ 'id', + 'is_public' => 'isPublic', + 'created' => 'created', + 'modified' => 'modified', + ]; + public function getResourceName() { return 'value_annotations'; @@ -22,14 +31,70 @@ public function getEntityClass() return ValueAnnotation::class; } - public function search(Request $request) + public function buildQuery(QueryBuilder $qb, array $query) { - return AbstractAdapter::search($request); + parent::buildQuery($qb, $query); + + // The visibility filter only checks the annotation's own public flag, not + // the parent resource's. Join through Value to check both. + $services = $this->getServiceLocator(); + $acl = $services->get('Omeka\Acl'); + if ($acl->userIsAllowed('Omeka\Entity\Resource', 'view-all')) { + return; + } + + $identity = $services->get('Omeka\AuthenticationService')->getIdentity(); + $valueAlias = $qb->createAlias(); + $parentResourceAlias = $qb->createAlias(); + + $qb->join( + 'Omeka\Entity\Value', + $valueAlias, + 'WITH', + $qb->expr()->eq("$valueAlias.valueAnnotation", 'omeka_root') + ); + $qb->join("$valueAlias.resource", $parentResourceAlias); + + $parentVisibilityExpr = $qb->expr()->eq( + "$parentResourceAlias.isPublic", + $qb->createNamedParameter(true) + ); + if ($identity) { + $parentVisibilityExpr = $qb->expr()->orX( + $parentVisibilityExpr, + $qb->expr()->eq( + "$parentResourceAlias.owner", + $qb->createNamedParameter($identity) + ) + ); + } + $qb->andWhere($parentVisibilityExpr); } public function read(Request $request) { - return AbstractAdapter::read($request); + // The visibility filter doesn't catch a public annotation on a private + // resource, so verify the parent explicitly. Return 404 rather than 403 + // to avoid revealing that the resource exists. + $response = parent::read($request); + + $acl = $this->getServiceLocator()->get('Omeka\Acl'); + if ($acl->userIsAllowed('Omeka\Entity\Resource', 'view-all')) { + return $response; + } + + // The ResourceVisibilityFilter applies to lazy-loaded associations, so + // if the parent resource is not visible to this user, getResource() returns null. + $entity = $response->getContent(); + if (!$entity->getValue()->getResource()) { + throw new Exception\NotFoundException(sprintf( + $this->getTranslator()->translate('%1$s entity with criteria %2$s not found'), + $this->getEntityClass(), + json_encode(['id' => $entity->getId()]) + )); + } + + return $response; } public function create(Request $request) diff --git a/application/src/Api/Representation/ValueAnnotationRepresentation.php b/application/src/Api/Representation/ValueAnnotationRepresentation.php index 2c2eaa52b..bf895418c 100644 --- a/application/src/Api/Representation/ValueAnnotationRepresentation.php +++ b/application/src/Api/Representation/ValueAnnotationRepresentation.php @@ -5,12 +5,33 @@ class ValueAnnotationRepresentation extends AbstractResourceEntityRepresentation { public function getResourceJsonLdType() { - return 'o:ValueAnnotation'; + return ['o:ValueAnnotation', 'oa:Annotation']; } public function getResourceJsonLd() { - return []; + $value = $this->annotatedValue(); + $valueJson = $value->jsonSerialize(); + // Unset @annotation to avoid a circular reference back to this object. + unset($valueJson['@annotation']); + return [ + 'oa:hasTarget' => [ + '@type' => 'rdf:Statement', + 'rdf:subject' => $this->resource()->getReference(), + 'rdf:predicate' => ['@id' => $value->property()->uri()], + 'rdf:object' => $valueJson, + ], + ]; + } + + public function annotatedValue() + { + return new ValueRepresentation($this->resource->getValue(), $this->getServiceLocator()); + } + + public function resource() + { + return $this->getAdapter('resources')->getRepresentation($this->resource->getValue()->getResource()); } public function displayValues(array $options = []) diff --git a/application/src/Entity/ValueAnnotation.php b/application/src/Entity/ValueAnnotation.php index 224e0cf51..a1fe0a00d 100644 --- a/application/src/Entity/ValueAnnotation.php +++ b/application/src/Entity/ValueAnnotation.php @@ -12,6 +12,11 @@ class ValueAnnotation extends Resource */ protected $id; + /** + * @OneToOne(targetEntity="Value", mappedBy="valueAnnotation") + */ + protected $value; + public function getResourceName() { return 'value_annotations'; @@ -21,4 +26,9 @@ public function getId() { return $this->id; } + + public function getValue() + { + return $this->value; + } } diff --git a/application/src/Service/AclFactory.php b/application/src/Service/AclFactory.php index ae3266c3a..7ab66163a 100644 --- a/application/src/Service/AclFactory.php +++ b/application/src/Service/AclFactory.php @@ -282,6 +282,7 @@ protected function addRulesForAllRoles(Acl $acl) 'Omeka\Api\Adapter\AssetAdapter', 'Omeka\Api\Adapter\ApiResourceAdapter', 'Omeka\Api\Adapter\DataTypeAdapter', + 'Omeka\Api\Adapter\ValueAnnotationAdapter', ], [ 'search', @@ -299,6 +300,7 @@ protected function addRulesForAllRoles(Acl $acl) 'Omeka\Entity\Property', 'Omeka\Entity\ResourceTemplate', 'Omeka\Entity\Asset', + 'Omeka\Entity\ValueAnnotation', ], [ 'read',