Skip to content

Commit

Permalink
Merge pull request #2648 from bolt/feature/post-api
Browse files Browse the repository at this point in the history
Role-based API support for `POST`, `PUT` and `DELETE` operations
  • Loading branch information
bobdenotter authored Nov 29, 2021
2 parents 4e7878c + 3dfd45a commit 6016c91
Show file tree
Hide file tree
Showing 12 changed files with 468 additions and 170 deletions.
297 changes: 150 additions & 147 deletions config/bolt/permissions.yaml

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion config/packages/security.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ security:
ROLE_EDITOR: [ROLE_USER]
# ROLE_USER is assigned to Bolt Entity Users if no roles have been set
ROLE_USER: []
ROLE_WEBSERVICE: []

encoders:
Bolt\Entity\User: auto
Expand All @@ -20,7 +21,11 @@ security:
dev:
pattern: ^/(_(profiler|wdt)|css|images|js)/
security: false


api:
pattern: ^/api
http_basic: ~

main:
pattern: ^/
anonymous: true
Expand Down
4 changes: 4 additions & 0 deletions config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,7 @@ services:
class: Jasny\Twig\ArrayExtension
tags:
- { name: twig.extension }

Bolt\Api\ContentDataPersister:
decorates: 'api_platform.doctrine.orm.data_persister'

58 changes: 58 additions & 0 deletions src/Api/ContentDataPersister.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

namespace Bolt\Api;

use ApiPlatform\Core\DataPersister\ContextAwareDataPersisterInterface;
use Bolt\Configuration\Config;
use Bolt\Configuration\Content\FieldType;
use Bolt\Entity\Content;
use Bolt\Repository\FieldRepository;

class ContentDataPersister implements ContextAwareDataPersisterInterface
{
/** @var ContextAwareDataPersisterInterface */
private $decorated;

/** @var Config */
private $config;

public function __construct(ContextAwareDataPersisterInterface $decorated, Config $config)
{
$this->decorated = $decorated;
$this->config = $config;
}

public function supports($data, array $context = []): bool
{
return $this->decorated->supports($data, $context);
}

public function persist($data, array $context = [])
{
if ($data instanceof Content) {
$contentTypes = $this->config->get('contenttypes');

$data->setDefinitionFromContentTypesConfig($contentTypes);

foreach ($data->getFields() as $field) {
$fieldDefinition = FieldType::factory($field->getName(), $data->getDefinition());
$newField = FieldRepository::factory($fieldDefinition);

// todo: This works for standalone fields only.
// See CollectionField.php and SetField.php
$newField->setName($field->getName());
$newField->setValue($field->getValue());

$data->removeField($field);
$data->addField($newField);
}
}

$this->decorated->persist($data, $context);
}

public function remove($data, array $context = [])
{
$this->decorated->persist($data, $context);
}
}
45 changes: 33 additions & 12 deletions src/Entity/Content.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,22 @@
/**
* @ApiResource(
* normalizationContext={"groups"={"get_content","get_definition"}},
* collectionOperations={"get"},
* itemOperations={"get"},
* graphql={"item_query", "collection_query"}
* denormalizationContext={"groups"={"api_write"},"enable_max_depth"=true},
* collectionOperations={
* "get"={"security"="is_granted('api:get')"},
* "post"={"security"="is_granted('api:post')"}
* },
* itemOperations={
* "get"={"security"="is_granted('api:get')"},
* "put"={"security"="is_granted('api:post')"},
* "delete"={"security"="is_granted('api:delete')"}
* },
* graphql={
* "item_query"={"security"="is_granted('api:get')"},
* "collection_query"={"security"="is_granted('api:get')"},
* "create"={"security"="is_granted('api:post')"},
* "delete"={"security"="is_granted('api:delete')"}
* }
* )
* @ApiFilter(SearchFilter::class)
* @ORM\Entity(repositoryClass="Bolt\Repository\ContentRepository")
Expand All @@ -47,15 +60,15 @@ class Content
* @ORM\Id()
* @ORM\GeneratedValue()
* @ORM\Column(type="integer")
* @Groups("get_content")
* @Groups({"get_content", "api_write"})
*/
private $id;

/**
* @var string
*
* @ORM\Column(type="string", length=191)
* @Groups("get_content")
* @Groups({"get_content","api_write"})
*/
private $contentType;

Expand All @@ -71,39 +84,39 @@ class Content
* @var string
*
* @ORM\Column(type="string", length=191)
* @Groups("get_content")
* @Groups({"get_content","api_write"})
*/
private $status;

/**
* @var \DateTime
*
* @ORM\Column(type="datetime")
* @Groups("get_content")
* @Groups({"get_content","api_write"})
*/
private $createdAt;

/**
* @var \DateTime|null
*
* @ORM\Column(type="datetime", nullable=true)
* @Groups("get_content")
* @Groups({"get_content","api_write"})
*/
private $modifiedAt = null;

/**
* @var \DateTime|null
*
* @ORM\Column(type="datetime", nullable=true)
* @Groups("get_content")
* @Groups({"get_content","api_write"})
*/
private $publishedAt = null;

/**
* @var \DateTime|null
*
* @ORM\Column(type="datetime", nullable=true)
* @Groups("get_content")
* @Groups({"get_content","api_write"})
*/
private $depublishedAt = null;

Expand All @@ -122,6 +135,7 @@ class Content
* cascade={"persist"}
* )
* @ORM\OrderBy({"sortorder": "ASC"})
* @Groups("api_write")
*/
private $fields;

Expand Down Expand Up @@ -726,10 +740,17 @@ private function convertToUTCFromLocal(?\DateTime $dateTime): ?\DateTime
*/
private function standaloneFieldsFilter(): Collection
{
$keys = $this->getDefinition()->get('fields')->keys()->all();
$definition = $this->getDefinition();

$keys = $definition ?
$this->getDefinition()->get('fields')->keys()->all()
: [];
// If the definition is missing, we cannot filter out keys. ¯\_(ツ)_/¯


return $this->fields->filter(function (Field $field) use ($keys) {
return ! $field->hasParent() && in_array($field->getName(), $keys, true);
return ! $field->hasParent() &&
(in_array($field->getName(), $keys, true) || empty($keys));
});
}

Expand Down
32 changes: 25 additions & 7 deletions src/Entity/Field.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,29 @@

/**
* @ApiResource(
* denormalizationContext={"groups"={"api_write"},"enable_max_depth"=true},
* normalizationContext={"groups"={"get_field"}},
*
* subresourceOperations={
* "api_contents_fields_get_subresource"={
* "method"="GET",
* "normalization_context"={"groups"={"get_field"}}
* "method"="GET"
* },
* },
* collectionOperations={"get"},
* itemOperations={"get"},
* graphql={"item_query", "collection_query"}
* collectionOperations={
* "get"={"security"="is_granted('api:get')"},
* "post"={"security"="is_granted('api:post')"}
* },
* itemOperations={
* "get"={"security"="is_granted('api:get')"},
* "put"={"security"="is_granted('api:post')"},
* "delete"={"security"="is_granted('api:delete')"}
* },
* graphql={
* "item_query"={"security"="is_granted('api:get')"},
* "collection_query"={"security"="is_granted('api:get')"},
* "create"={"security"="is_granted('api:post')"},
* "delete"={"security"="is_granted('api:delete')"}
* }
* )
* @ApiFilter(SearchFilter::class)
* @ORM\Entity(repositoryClass="Bolt\Repository\FieldRepository")
Expand All @@ -53,7 +67,7 @@ class Field implements FieldInterface, TranslatableInterface

/**
* @ORM\Column(type="string", length=191)
* @Groups("get_field")
* @Groups({"get_field","api_write"})
*/
public $name;

Expand All @@ -64,8 +78,9 @@ class Field implements FieldInterface, TranslatableInterface
private $version;

/**
* @ORM\ManyToOne(targetEntity="Bolt\Entity\Content", inversedBy="fields", cascade={"persist"})
* @ORM\ManyToOne(targetEntity="Bolt\Entity\Content", inversedBy="fields", fetch="EAGER")
* @ORM\JoinColumn(nullable=false)
* @Groups("api_write")
*/
private $content;

Expand Down Expand Up @@ -279,6 +294,9 @@ public function set(string $key, $value): self
return $this;
}

/**
* @Groups("api_write")
*/
public function setValue($value): self
{
$this->translate($this->getLocale(), ! $this->isTranslatable())->setValue($value);
Expand Down
2 changes: 2 additions & 0 deletions src/Entity/Field/CollectionField.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,8 @@ public function setValue($fields): Field
{
/** @var Field $field */
foreach ($fields as $field) {
// todo: This should be able to handle an array of fields
// in key-value format, not just Field.php types.
$field->setParent($this);
}

Expand Down
2 changes: 2 additions & 0 deletions src/Entity/Field/SetField.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ public function setValue($fields): Field

/** @var Field $field */
foreach ($fields as $field) {
// todo: This should be able to handle an array of fields
// in key-value format, not just Field.php types.
$field->setParent($this);
$value[$field->getName()] = $field;
}
Expand Down
18 changes: 15 additions & 3 deletions src/Entity/Relation.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,21 @@
/**
* @ApiResource(
* normalizationContext={"groups"={"get_relation"}},
* collectionOperations={"get"},
* itemOperations={"get"},
* graphql={"item_query", "collection_query"}
* collectionOperations={
* "get"={"security"="is_granted('api:get')"},
* "post"={"security"="is_granted(‘api:post’)"}
* },
* itemOperations={
* "get"={"security"="is_granted('api:get')"},
* "put"={"security"="is_granted('api:post')"},
* "delete"={"security"="is_granted('api:delete')"}
* },
* graphql={
* "item_query"={"security"="is_granted('api:get')"},
* "collection_query"={"security"="is_granted('api:get')"},
* "create"={"security"="is_granted('api:post')"},
* "delete"={"security"="is_granted('api:delete')"}
* }
* )
* @ORM\Entity(repositoryClass="Bolt\Repository\RelationRepository")
* @ORM\Table(indexes={
Expand Down
Loading

0 comments on commit 6016c91

Please sign in to comment.