From f8003af8d9642453daf95fe10f188a4aee0a8866 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Mon, 21 Jun 2021 15:43:11 +0200 Subject: [PATCH 01/30] Role-based API support for `POST`, `PUT` and `DELETE` operations --- config/bolt/permissions.yaml | 294 +++++++++++++++++----------------- config/packages/security.yaml | 1 + src/Entity/Content.php | 18 ++- src/Entity/Field.php | 18 ++- src/Entity/Relation.php | 18 ++- 5 files changed, 193 insertions(+), 156 deletions(-) diff --git a/config/bolt/permissions.yaml b/config/bolt/permissions.yaml index bf976685b..c36abe08f 100644 --- a/config/bolt/permissions.yaml +++ b/config/bolt/permissions.yaml @@ -1,147 +1,147 @@ -# This file defines role-based access control for your Bolt site. -# Before making any modifications to this file, make sure you've thoroughly -# read the documentation at https://docs.bolt.cm/configuration/permissions -# and understand the consequences of making uninformed changes to the roles and -# permissions. - -# List of roles that are presented in the list of options when editing a user. -# Roles that are not in this list are left 'as is' when editing users. -# Note: ROLE_USER is assigned to Bolt Entity Users if no roles have been set. -assignable_roles: [ROLE_DEVELOPER, ROLE_ADMIN, ROLE_CHIEF_EDITOR, ROLE_EDITOR, ROLE_USER] - -# These permissions are the 'global' permissions; these are not tied -# to any content types, but rather apply to global, non-content activity in -# Bolt's backend. Most of these permissions map directly to backend routes; -# keep in mind, however, that routes do not always correspond to URL paths 1:1. -# The default set defined here is appropriate for most sites, so most likely, -# you will not have to change it. -# Also note that the 'editcontent' and 'overview' routes are special-cased -# inside the code, so they don't appear here. -global: - about: [ ROLE_EDITOR ] # view the 'About Bolt' page - clearcache: [ ROLE_CHIEF_EDITOR ] - dashboard: [ IS_AUTHENTICATED_REMEMBERED ] - extensions: [ ROLE_ADMIN ] - # these control /bolt/file-edit and /bolt/filemanager -> combined create/read/update/delete permission - # the part after the files: is the 'location' where the files are part of - managefiles:config: [ ROLE_ADMIN ] # all configuration yml files /bolt/filemanager/config and /bolt/file-edit/config?file=/bolt/menu.yaml - managefiles:files: [ ROLE_EDITOR ] - managefiles:themes: [ ROLE_ADMIN ] - editprofile: [ IS_AUTHENTICATED_FULLY ] # edit own profile - translation: [ ROLE_ADMIN ] - user:list: [ ROLE_ADMIN ] # overview listing of users and a list of active sessions - user:add: [ ROLE_ADMIN ] # add user - allows editing user _before_ saving, can set roles, status on create, after saving 'useredit' is needed. - user:status: [ ROLE_ADMIN ] # user enable/disable - only used for changing status outside of add/edit context - user:delete: [ ROLE_ADMIN ] # user delete - user:edit: [ ROLE_ADMIN ] # user edit all fields, includes user:status permissions - maintenance-mode: [ ROLE_EDITOR ] # view the frontend when in maintenance mode - systemlog: [ ROLE_ADMIN ] - api_admin: [ ROLE_ADMIN ] # WARNING: this only shows/hides api in the bolt admin, it doesn't protect the /api route(s) - bulk_operations: [ ROLE_CHIEF_EDITOR ] - kitchensink: [ ROLE_ADMIN ] - upload: [ ROLE_EDITOR ] - extensionmenus: [ IS_AUTHENTICATED_REMEMBERED ] # allows you to see menu items added by extensions - media_edit: [ ROLE_CHIEF_EDITOR ] # edit metadata for images etc. - fetch_embed_data: [ ROLE_EDITOR ] # get embed (meta)data for urls via the back-end (needed to embed youtube etc.) - list_files:config: [ ROLE_ADMIN ] # should probably not be used? - list_files:files: [ ROLE_EDITOR ] # get list of files (images?) available for use as site-content - list_files:themes: [ ROLE_ADMIN ] # should probably not be used? - -# For content type related actions, permissions can be set individually for -# each content type. For this, we define three groups of permission sets. -# The 'contenttype-base' permission sets *overrides*; any roles specified here -# will grant a permission for all content types, regardless of the rest of this -# section. -# The 'contenttype-default' contains rules that are used when the desired -# content type does not define a rule for this permission itself. -# The 'contenttypes' section specifies permissions for individual content -# types. -# -# To understand how this works, it may be best to follow the permission checker -# through its decision-making process. -# -# First it checks whether any of the current user's roles match any of the -# roles in contenttype-base/{permission}. If so, the search is over, and the -# permission can be granted. -# -# The next step is to find contenttypes/{contenttype}/{permission}. If it is -# found, then the permission can be granted if and only if any of the user's -# roles match any role in contenttypes/{contenttype}/{permission}. -# -# If either contenttypes/{contenttype} or -# contenttypes/{contenttype}/{permission} is absent, the permission checker -# uses contenttype-default/{permission} instead. If any role exists in both the -# user's roles and contenttype-default/{permission}, the permission can be -# granted. -# -# Note especially that an *empty* set of roles in the contenttype section means -# something else than the *absence* of the permission. If the permission is -# defined with an empty role list, it overrides the role list in -# contenttype-default; but if the permission is not mentioned, the -# corresponding entry in contenttype-default applies. -# -# The following permissions are available on a per-contenttype basis: -# -# - edit: allows updating existing records -# - create: allows creating new records -# - change-status: allows changing the published status of a record -# - delete: allows (hard) deletion of records -# - change-ownership: allows changing a record's owner. Note that ownership may -# grant additional permissions on a record, so this -# permission can indirectly enable users more permissions -# in ways that may not be immediately obvious. -# - view: allows viewing records in the backend (listings, content/fields) -# -# Note that all permissions imply 'view' permission, so you don't have to give 'view' along with 'edit' -# Note change-ownership is available as a setting but is not implemented in the bolt admin at the moment - -# these permissions will be set for all contenttypes, config below can add additional roles to these, but they can not be overridden -contenttype-base: - edit: [ ROLE_CHIEF_EDITOR ] - create: [ ROLE_CHIEF_EDITOR ] - change-status: [ ROLE_CHIEF_EDITOR ] # Note: You can have 'change-status' permission without 'edit' but you cannot use that at the moment as there is no screen that only handles status changes - delete: [ ROLE_CHIEF_EDITOR ] - change-ownership: [ ROLE_CHIEF_EDITOR ] - view: [ ROLE_CHIEF_EDITOR ] # = show in menu, show listings, open 'edit' view without actually being able to edit, any of the other permissions always imply 'view' - -# these permissions are used as a default for contenttypes, they are added to the base permissions -# you can override these settings per contenttype by adding it to the `contenttypes:` array -contenttype-default: - edit: [ ROLE_EDITOR, CONTENT_OWNER ] - create: [ ROLE_EDITOR ] - change-ownership: [ CONTENT_OWNER ] # <-- NOT IMPLEMENTED YET (and: how to handle chance-ownership permission without 'edit'?) - view: [ ROLE_EDITOR ] - - -contenttypes: - -# This is an example of how to define Contenttype specific permissions -# -# contenttypes: -# # Keys in this dictionary map to keys in the contenttypes.yml specification. -# showcases: -# # Rules defined here *override* rules defined in contenttype-default, -# # but *add* to rules in contenttype-base. This means that permissions -# # granted through contenttype-base cannot be revoked here, merely -# # amended. -# -# # Only the Admin and Chief Editor are allowed to edit records -# edit: [ ROLE_CHIEF_EDITOR ] -# create: [ ROLE_CHIEF_EDITOR ] -# change-status: [ ROLE_CHIEF_EDITOR ] -# delete: [ ROLE_CHIEF_EDITOR ] -# pages: -# edit: [ ROLE_EDITOR, CONTENT_OWNER ] -# create: [ ROLE_EDITOR ] -# change-ownership: [ CONTENT_OWNER ] -# view: [ ROLE_USER ] -# entries: -# edit: [ ROLE_EDITOR ] -# edit: [ ROLE_EDITOR, CONTENT_OWNER ] -# create: [ ROLE_EDITOR ] -# change-ownership: [ CONTENT_OWNER ] -# view: [ ROLE_EDITOR ] -# homepage: # singleton -# view: [ ROLE_EDITOR ] -# edit: [ ROLE_EDITOR ] -# create: [ ROLE_EDITOR ] +# This file defines role-based access control for your Bolt site. +# Before making any modifications to this file, make sure you've thoroughly +# read the documentation at https://docs.bolt.cm/configuration/permissions +# and understand the consequences of making uninformed changes to the roles and +# permissions. + +# List of roles that are presented in the list of options when editing a user. +# Roles that are not in this list are left 'as is' when editing users. +# Note: ROLE_USER is assigned to Bolt Entity Users if no roles have been set. +assignable_roles: [ROLE_DEVELOPER, ROLE_ADMIN, ROLE_CHIEF_EDITOR, ROLE_EDITOR, ROLE_USER] + +# These permissions are the 'global' permissions; these are not tied +# to any content types, but rather apply to global, non-content activity in +# Bolt's backend. Most of these permissions map directly to backend routes; +# keep in mind, however, that routes do not always correspond to URL paths 1:1. +# The default set defined here is appropriate for most sites, so most likely, +# you will not have to change it. +# Also note that the 'editcontent' and 'overview' routes are special-cased +# inside the code, so they don't appear here. +global: + about: [ ROLE_EDITOR ] # view the 'About Bolt' page + clearcache: [ ROLE_CHIEF_EDITOR ] + dashboard: [ IS_AUTHENTICATED_REMEMBERED ] + extensions: [ ROLE_ADMIN ] + # these control /bolt/file-edit and /bolt/filemanager -> combined create/read/update/delete permission + # the part after the files: is the 'location' where the files are part of + managefiles:config: [ ROLE_ADMIN ] # all configuration yml files /bolt/filemanager/config and /bolt/file-edit/config?file=/bolt/menu.yaml + managefiles:files: [ ROLE_EDITOR ] + managefiles:themes: [ ROLE_ADMIN ] + editprofile: [ IS_AUTHENTICATED_FULLY ] # edit own profile + translation: [ ROLE_ADMIN ] + user:list: [ ROLE_ADMIN ] # overview listing of users and a list of active sessions + user:add: [ ROLE_ADMIN ] # add user - allows editing user _before_ saving, can set roles, status on create, after saving 'useredit' is needed. + user:status: [ ROLE_ADMIN ] # user enable/disable - only used for changing status outside of add/edit context + user:delete: [ ROLE_ADMIN ] # user delete + user:edit: [ ROLE_ADMIN ] # user edit all fields, includes user:status permissions + maintenance-mode: [ ROLE_EDITOR ] # view the frontend when in maintenance mode + systemlog: [ ROLE_ADMIN ] + api_admin: [ ROLE_ADMIN ] # WARNING: this only shows/hides api in the bolt admin, it doesn't protect the /api route(s) + bulk_operations: [ ROLE_EDITOR ] + kitchensink: [ ROLE_ADMIN ] + upload: [ ROLE_EDITOR ] # TODO PERMISSIONS upload media/files ? Or should this be handled by managefiles:files + extensionmenus: [ IS_AUTHENTICATED_REMEMBERED ] # allows you to see menu items added by extensions + media_edit: [ ROLE_CHIEF_EDITOR ] # edit metadata for images etc. + fetch_embed_data: [ ROLE_EDITOR ] # get embed (meta)data for urls via the back-end (needed to embed youtube etc.) + list_files:config: [ ROLE_ADMIN ] # should probably not be used? + list_files:files: [ ROLE_EDITOR ] # get list of files (images?) available for use as site-content + list_files:themes: [ ROLE_ADMIN ] # should probably not be used? + +# For content type related actions, permissions can be set individually for +# each content type. For this, we define three groups of permission sets. +# The 'contenttype-base' permission sets *overrides*; any roles specified here +# will grant a permission for all content types, regardless of the rest of this +# section. +# The 'contenttype-default' contains rules that are used when the desired +# content type does not define a rule for this permission itself. +# The 'contenttypes' section specifies permissions for individual content +# types. +# +# To understand how this works, it may be best to follow the permission checker +# through its decision-making process. +# +# First it checks whether any of the current user's roles match any of the +# roles in contenttype-base/{permission}. If so, the search is over, and the +# permission can be granted. +# +# The next step is to find contenttypes/{contenttype}/{permission}. If it is +# found, then the permission can be granted if and only if any of the user's +# roles match any role in contenttypes/{contenttype}/{permission}. +# +# If either contenttypes/{contenttype} or +# contenttypes/{contenttype}/{permission} is absent, the permission checker +# uses contenttype-default/{permission} instead. If any role exists in both the +# user's roles and contenttype-default/{permission}, the permission can be +# granted. +# +# Note especially that an *empty* set of roles in the contenttype section means +# something else than the *absence* of the permission. If the permission is +# defined with an empty role list, it overrides the role list in +# contenttype-default; but if the permission is not mentioned, the +# corresponding entry in contenttype-default applies. +# +# The following permissions are available on a per-contenttype basis: +# +# - edit: allows updating existing records +# - create: allows creating new records +# - change-status: allows changing the published status of a record +# - delete: allows (hard) deletion of records +# - change-ownership: allows changing a record's owner. Note that ownership may +# grant additional permissions on a record, so this +# permission can indirectly enable users more permissions +# in ways that may not be immediately obvious. +# - view: allows viewing records in the backend (listings, content/fields) +# +# Note that all permissions imply 'view' permission, so you don't have to give 'view' along with 'edit' +# Note change-ownership is available as a setting but is not implemented in the bolt admin at the moment + +# these permissions will be set for all contenttypes, config below can add additional roles to these, but they can not be overridden +contenttype-base: + edit: [ ROLE_CHIEF_EDITOR ] + create: [ ROLE_CHIEF_EDITOR ] + change-status: [ ROLE_CHIEF_EDITOR ] # Note: You can have 'change-status' permission without 'edit' but you cannot use that at the moment as there is no screen that only handles status changes + delete: [ ROLE_CHIEF_EDITOR ] + change-ownership: [ ROLE_CHIEF_EDITOR ] + view: [ ROLE_CHIEF_EDITOR ] # = show in menu, show listings, open 'edit' view without actually being able to edit, any of the other permissions always imply 'view' + +# these permissions are used as a default for contenttypes, they are added to the base permissions +# you can override these settings per contenttype by adding it to the `contenttypes:` array +contenttype-default: + edit: [ ROLE_EDITOR, CONTENT_OWNER ] + create: [ ROLE_EDITOR ] + change-ownership: [ CONTENT_OWNER ] # <-- NOT IMPLEMENTED YET (and: how to handle chance-ownership permission without 'edit'?) + view: [ ROLE_EDITOR ] + + +contenttypes: + +# This is an example of how to define Contenttype specific permissions +# +# contenttypes: +# # Keys in this dictionary map to keys in the contenttypes.yml specification. +# showcases: +# # Rules defined here *override* rules defined in contenttype-default, +# # but *add* to rules in contenttype-base. This means that permissions +# # granted through contenttype-base cannot be revoked here, merely +# # amended. +# +# # Only the Admin and Chief Editor are allowed to edit records +# edit: [ ROLE_CHIEF_EDITOR ] +# create: [ ROLE_CHIEF_EDITOR ] +# change-status: [ ROLE_CHIEF_EDITOR ] +# delete: [ ROLE_CHIEF_EDITOR ] +# pages: +# edit: [ ROLE_EDITOR, CONTENT_OWNER ] +# create: [ ROLE_EDITOR ] +# change-ownership: [ CONTENT_OWNER ] +# view: [ ROLE_USER ] +# entries: +# edit: [ ROLE_EDITOR ] +# edit: [ ROLE_EDITOR, CONTENT_OWNER ] +# create: [ ROLE_EDITOR ] +# change-ownership: [ CONTENT_OWNER ] +# view: [ ROLE_EDITOR ] +# homepage: # singleton +# view: [ ROLE_EDITOR ] +# edit: [ ROLE_EDITOR ] +# create: [ ROLE_EDITOR ] diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 220f35f6f..d1358d003 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -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 diff --git a/src/Entity/Content.php b/src/Entity/Content.php index 5ed36877d..a4221470a 100644 --- a/src/Entity/Content.php +++ b/src/Entity/Content.php @@ -24,9 +24,21 @@ /** * @ApiResource( * normalizationContext={"groups"={"get_content","get_definition"}}, - * 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\ContentRepository") diff --git a/src/Entity/Field.php b/src/Entity/Field.php index c5680665c..1503a98d0 100644 --- a/src/Entity/Field.php +++ b/src/Entity/Field.php @@ -28,9 +28,21 @@ * "normalization_context"={"groups"={"get_field"}} * }, * }, - * 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") diff --git a/src/Entity/Relation.php b/src/Entity/Relation.php index 37972790e..e4d278322 100644 --- a/src/Entity/Relation.php +++ b/src/Entity/Relation.php @@ -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={ From 8d7e6c3358fbc4f5d7963896c5594faa0374e544 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Tue, 16 Nov 2021 16:45:00 +0100 Subject: [PATCH 02/30] Creating content --- config/packages/security.yaml | 6 +++++- src/Entity/Content.php | 31 ++++++++++++++++++++----------- src/Entity/Field.php | 16 +++++++++++----- 3 files changed, 36 insertions(+), 17 deletions(-) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index d1358d003..fa9eb01ee 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -21,7 +21,11 @@ security: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false - + + api: + pattern: ^/api + http_basic: ~ + main: pattern: ^/ anonymous: true diff --git a/src/Entity/Content.php b/src/Entity/Content.php index a4221470a..7beed2555 100644 --- a/src/Entity/Content.php +++ b/src/Entity/Content.php @@ -24,9 +24,10 @@ /** * @ApiResource( * normalizationContext={"groups"={"get_content","get_definition"}}, + * denormalizationContext={"groups"={"api_write"},"enable_max_depth"=true}, * collectionOperations={ * "get"={"security"="is_granted('api:get')"}, - * "post"={"security"="is_granted(‘api:post’)"} + * "post"={"security"="is_granted('api:post')"} * }, * itemOperations={ * "get"={"security"="is_granted('api:get')"}, @@ -59,7 +60,7 @@ class Content * @ORM\Id() * @ORM\GeneratedValue() * @ORM\Column(type="integer") - * @Groups("get_content") + * @Groups({"get_content", "api_write"}) */ private $id; @@ -67,7 +68,7 @@ class Content * @var string * * @ORM\Column(type="string", length=191) - * @Groups("get_content") + * @Groups({"get_content","api_write"}) */ private $contentType; @@ -83,7 +84,7 @@ class Content * @var string * * @ORM\Column(type="string", length=191) - * @Groups("get_content") + * @Groups({"get_content","api_write"}) */ private $status; @@ -91,7 +92,7 @@ class Content * @var \DateTime * * @ORM\Column(type="datetime") - * @Groups("get_content") + * @Groups({"get_content","api_write"}) */ private $createdAt; @@ -99,7 +100,7 @@ class Content * @var \DateTime|null * * @ORM\Column(type="datetime", nullable=true) - * @Groups("get_content") + * @Groups({"get_content","api_write"}) */ private $modifiedAt = null; @@ -107,7 +108,7 @@ class Content * @var \DateTime|null * * @ORM\Column(type="datetime", nullable=true) - * @Groups("get_content") + * @Groups({"get_content","api_write"}) */ private $publishedAt = null; @@ -115,7 +116,7 @@ class Content * @var \DateTime|null * * @ORM\Column(type="datetime", nullable=true) - * @Groups("get_content") + * @Groups({"get_content","api_write"}) */ private $depublishedAt = null; @@ -134,6 +135,7 @@ class Content * cascade={"persist"} * ) * @ORM\OrderBy({"sortorder": "ASC"}) + * @Groups("api_write") */ private $fields; @@ -720,7 +722,7 @@ private function convertToLocalFromDatabase(?\DateTime $dateTime): ?\DateTime /** * All date/timestamps are created in the current local timezone by default. - * Dates/timestamps must be stored in UTC in the database. This method converts + * Dates/timestamps must be stored in UTC in the dat abase. This method converts * the local date to UTC. */ private function convertToUTCFromLocal(?\DateTime $dateTime): ?\DateTime @@ -739,10 +741,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)); }); } diff --git a/src/Entity/Field.php b/src/Entity/Field.php index 1503a98d0..f52989421 100644 --- a/src/Entity/Field.php +++ b/src/Entity/Field.php @@ -22,15 +22,17 @@ /** * @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"={"security"="is_granted('api:get')"}, - * "post"={"security"="is_granted(‘api:post’)"} + * "post"={"security"="is_granted('api:post')"} * }, * itemOperations={ * "get"={"security"="is_granted('api:get')"}, @@ -65,7 +67,7 @@ class Field implements FieldInterface, TranslatableInterface /** * @ORM\Column(type="string", length=191) - * @Groups("get_field") + * @Groups({"get_field","api_write"}) */ public $name; @@ -76,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", cascade={"persist"}, fetch="EAGER") * @ORM\JoinColumn(nullable=false) + * @Groups("api_write") */ private $content; @@ -291,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); From 370c732d11a52eba5c74ef36bed48b777430e0c5 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Tue, 16 Nov 2021 17:14:03 +0100 Subject: [PATCH 03/30] Add WIP for persistence --- config/services.yaml | 4 +++ src/Api/ContentDataPersister.php | 36 ++++++++++++++++++++++++ src/Event/Listener/FieldFillListener.php | 5 ++++ 3 files changed, 45 insertions(+) create mode 100644 src/Api/ContentDataPersister.php diff --git a/config/services.yaml b/config/services.yaml index 12eeeab5f..266899f38 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -145,3 +145,7 @@ services: class: Jasny\Twig\ArrayExtension tags: - { name: twig.extension } + + Bolt\Api\ContentDataPersister: + decorates: 'api_platform.doctrine.orm.data_persister' + diff --git a/src/Api/ContentDataPersister.php b/src/Api/ContentDataPersister.php new file mode 100644 index 000000000..c5e1bf98b --- /dev/null +++ b/src/Api/ContentDataPersister.php @@ -0,0 +1,36 @@ +decorated = $decorated; + } + + public function supports($data, array $context = []): bool + { + return $this->decorated->supports($data, $context); + } + + public function persist($data, array $context = []) + { + // Here we need to make some adjustments. + // Like setting the proper author, making the fields + // the right type, etc. + dd("PERSISTING"); + + $this->decorated->persist($data, $context); + } + + public function remove($data, array $context = []) + { + $this->decorated->persist($data, $context); + } +} diff --git a/src/Event/Listener/FieldFillListener.php b/src/Event/Listener/FieldFillListener.php index 1cfb783ae..e1b85e3da 100644 --- a/src/Event/Listener/FieldFillListener.php +++ b/src/Event/Listener/FieldFillListener.php @@ -54,6 +54,11 @@ public function preUpdate(LifecycleEventArgs $args): void $value = $this->clean($field->getParsedValue()); $field->setValue($value); } + + if ($field->getType() === 'generic') { + dump($field->getDefinition()->get('type')); + die; + } } } From 9ac0dbd48bd64b2dc8a7bfa1e3cf3c59c9bdf397 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Thu, 18 Nov 2021 16:37:52 +0100 Subject: [PATCH 04/30] Make the correct field types --- src/Api/ContentDataPersister.php | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/Api/ContentDataPersister.php b/src/Api/ContentDataPersister.php index c5e1bf98b..55ecf3f41 100644 --- a/src/Api/ContentDataPersister.php +++ b/src/Api/ContentDataPersister.php @@ -3,15 +3,23 @@ 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; - public function __construct(ContextAwareDataPersisterInterface $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 @@ -21,10 +29,23 @@ public function supports($data, array $context = []): bool public function persist($data, array $context = []) { - // Here we need to make some adjustments. - // Like setting the proper author, making the fields - // the right type, etc. - dd("PERSISTING"); + 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); + + $newField->setName($field->getName()); + $newField->setValue($field->getValue()); + + $data->removeField($field); + $data->addField($newField); + } + + } $this->decorated->persist($data, $context); } From 35d4a37d92b4368207951314cc183c10d97c535e Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Thu, 18 Nov 2021 16:40:44 +0100 Subject: [PATCH 05/30] Update Content.php --- src/Entity/Content.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Content.php b/src/Entity/Content.php index 7beed2555..7d3a1dd94 100644 --- a/src/Entity/Content.php +++ b/src/Entity/Content.php @@ -722,7 +722,7 @@ private function convertToLocalFromDatabase(?\DateTime $dateTime): ?\DateTime /** * All date/timestamps are created in the current local timezone by default. - * Dates/timestamps must be stored in UTC in the dat abase. This method converts + * Dates/timestamps must be stored in UTC in the database. This method converts * the local date to UTC. */ private function convertToUTCFromLocal(?\DateTime $dateTime): ?\DateTime From d6d999c9b727e081242ce098afd262082c8df0e9 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Thu, 18 Nov 2021 16:41:33 +0100 Subject: [PATCH 06/30] Update FieldFillListener.php --- src/Event/Listener/FieldFillListener.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Event/Listener/FieldFillListener.php b/src/Event/Listener/FieldFillListener.php index e1b85e3da..1cfb783ae 100644 --- a/src/Event/Listener/FieldFillListener.php +++ b/src/Event/Listener/FieldFillListener.php @@ -54,11 +54,6 @@ public function preUpdate(LifecycleEventArgs $args): void $value = $this->clean($field->getParsedValue()); $field->setValue($value); } - - if ($field->getType() === 'generic') { - dump($field->getDefinition()->get('type')); - die; - } } } From bf0b5ce834ea2f30810a7a77fb9968c08927d310 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Fri, 19 Nov 2021 10:36:43 +0100 Subject: [PATCH 07/30] Update permissions.yaml --- config/bolt/permissions.yaml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/config/bolt/permissions.yaml b/config/bolt/permissions.yaml index c36abe08f..d6d03d487 100644 --- a/config/bolt/permissions.yaml +++ b/config/bolt/permissions.yaml @@ -46,6 +46,10 @@ global: list_files:config: [ ROLE_ADMIN ] # should probably not be used? list_files:files: [ ROLE_EDITOR ] # get list of files (images?) available for use as site-content list_files:themes: [ ROLE_ADMIN ] # should probably not be used? + api:get: [ ROLE_WEBSERVICE ] # allow read access to Bolt's RESTful and GraphQL API + api:post: [ ROLE_WEBSERVICE ] # allow write access to Bolt's RESTful and GraphQL API + api:delete: [ ROLE_WEBSERVICE ] # allow delete access to Bolt's RESTful and GraphQL API + # For content type related actions, permissions can be set individually for # each content type. For this, we define three groups of permission sets. From bfb0bceab4a6a1cb77824611b1a439983610315b Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Mon, 21 Jun 2021 15:43:11 +0200 Subject: [PATCH 08/30] Role-based API support for `POST`, `PUT` and `DELETE` operations --- src/Entity/Field.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Field.php b/src/Entity/Field.php index f52989421..734d62eaa 100644 --- a/src/Entity/Field.php +++ b/src/Entity/Field.php @@ -32,7 +32,7 @@ * }, * collectionOperations={ * "get"={"security"="is_granted('api:get')"}, - * "post"={"security"="is_granted('api:post')"} + * "post"={"security"="is_granted(‘api:post’)"} * }, * itemOperations={ * "get"={"security"="is_granted('api:get')"}, From 9aed4a209f17cea2f1a630696807fbe36ecac49b Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Tue, 16 Nov 2021 16:45:00 +0100 Subject: [PATCH 09/30] Creating content --- src/Entity/Content.php | 2 +- src/Entity/Field.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Entity/Content.php b/src/Entity/Content.php index 7d3a1dd94..7beed2555 100644 --- a/src/Entity/Content.php +++ b/src/Entity/Content.php @@ -722,7 +722,7 @@ private function convertToLocalFromDatabase(?\DateTime $dateTime): ?\DateTime /** * All date/timestamps are created in the current local timezone by default. - * Dates/timestamps must be stored in UTC in the database. This method converts + * Dates/timestamps must be stored in UTC in the dat abase. This method converts * the local date to UTC. */ private function convertToUTCFromLocal(?\DateTime $dateTime): ?\DateTime diff --git a/src/Entity/Field.php b/src/Entity/Field.php index 734d62eaa..f52989421 100644 --- a/src/Entity/Field.php +++ b/src/Entity/Field.php @@ -32,7 +32,7 @@ * }, * collectionOperations={ * "get"={"security"="is_granted('api:get')"}, - * "post"={"security"="is_granted(‘api:post’)"} + * "post"={"security"="is_granted('api:post')"} * }, * itemOperations={ * "get"={"security"="is_granted('api:get')"}, From 2cb89aa8597a4184770b0671fc3d90cd5d38d552 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Tue, 16 Nov 2021 17:14:03 +0100 Subject: [PATCH 10/30] Add WIP for persistence --- src/Event/Listener/FieldFillListener.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Event/Listener/FieldFillListener.php b/src/Event/Listener/FieldFillListener.php index 1cfb783ae..e1b85e3da 100644 --- a/src/Event/Listener/FieldFillListener.php +++ b/src/Event/Listener/FieldFillListener.php @@ -54,6 +54,11 @@ public function preUpdate(LifecycleEventArgs $args): void $value = $this->clean($field->getParsedValue()); $field->setValue($value); } + + if ($field->getType() === 'generic') { + dump($field->getDefinition()->get('type')); + die; + } } } From fc892234a4fe8e413067355d6a606a8d21d732ca Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Thu, 18 Nov 2021 16:40:44 +0100 Subject: [PATCH 11/30] Update Content.php --- src/Entity/Content.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Content.php b/src/Entity/Content.php index 7beed2555..7d3a1dd94 100644 --- a/src/Entity/Content.php +++ b/src/Entity/Content.php @@ -722,7 +722,7 @@ private function convertToLocalFromDatabase(?\DateTime $dateTime): ?\DateTime /** * All date/timestamps are created in the current local timezone by default. - * Dates/timestamps must be stored in UTC in the dat abase. This method converts + * Dates/timestamps must be stored in UTC in the database. This method converts * the local date to UTC. */ private function convertToUTCFromLocal(?\DateTime $dateTime): ?\DateTime From eda9c99bc170531a5eff3e4b2bc1dd15b6401556 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Thu, 18 Nov 2021 16:41:33 +0100 Subject: [PATCH 12/30] Update FieldFillListener.php --- src/Event/Listener/FieldFillListener.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Event/Listener/FieldFillListener.php b/src/Event/Listener/FieldFillListener.php index e1b85e3da..1cfb783ae 100644 --- a/src/Event/Listener/FieldFillListener.php +++ b/src/Event/Listener/FieldFillListener.php @@ -54,11 +54,6 @@ public function preUpdate(LifecycleEventArgs $args): void $value = $this->clean($field->getParsedValue()); $field->setValue($value); } - - if ($field->getType() === 'generic') { - dump($field->getDefinition()->get('type')); - die; - } } } From a9388257084cc3dd82564074d9488a4cf554be63 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Fri, 19 Nov 2021 10:46:52 +0100 Subject: [PATCH 13/30] csfix --- src/Api/ContentDataPersister.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Api/ContentDataPersister.php b/src/Api/ContentDataPersister.php index 55ecf3f41..73691ff9f 100644 --- a/src/Api/ContentDataPersister.php +++ b/src/Api/ContentDataPersister.php @@ -34,7 +34,7 @@ public function persist($data, array $context = []) $data->setDefinitionFromContentTypesConfig($contentTypes); - foreach($data->getFields() as $field) { + foreach ($data->getFields() as $field) { $fieldDefinition = FieldType::factory($field->getName(), $data->getDefinition()); $newField = FieldRepository::factory($fieldDefinition); @@ -44,7 +44,6 @@ public function persist($data, array $context = []) $data->removeField($field); $data->addField($newField); } - } $this->decorated->persist($data, $context); From bb68e2d716db58bf4b98bebdf27e2679ad165278 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Fri, 19 Nov 2021 10:51:23 +0100 Subject: [PATCH 14/30] Update security.yaml --- config/packages/security.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index fa9eb01ee..427503166 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,7 +1,7 @@ security: role_hierarchy: ROLE_DEVELOPER: [ROLE_ADMIN, ROLE_CAN_SWITCH_USER] - ROLE_ADMIN: [ROLE_CHIEF_EDITOR] + ROLE_ADMIN: [ROLE_CHIEF_EDITOR, ROLE_WEBSERVICE] ROLE_CHIEF_EDITOR: [ROLE_EDITOR] ROLE_EDITOR: [ROLE_USER] # ROLE_USER is assigned to Bolt Entity Users if no roles have been set From ef16dff70e63276c61f83118bf98935e22411d6f Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Mon, 21 Jun 2021 15:43:11 +0200 Subject: [PATCH 15/30] Role-based API support for `POST`, `PUT` and `DELETE` operations --- config/bolt/permissions.yaml | 5 ++--- src/Entity/Content.php | 3 +-- src/Entity/Field.php | 2 +- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/config/bolt/permissions.yaml b/config/bolt/permissions.yaml index d6d03d487..ae0b5d79e 100644 --- a/config/bolt/permissions.yaml +++ b/config/bolt/permissions.yaml @@ -50,7 +50,6 @@ global: api:post: [ ROLE_WEBSERVICE ] # allow write access to Bolt's RESTful and GraphQL API api:delete: [ ROLE_WEBSERVICE ] # allow delete access to Bolt's RESTful and GraphQL API - # For content type related actions, permissions can be set individually for # each content type. For this, we define three groups of permission sets. # The 'contenttype-base' permission sets *overrides*; any roles specified here @@ -108,7 +107,7 @@ contenttype-base: change-ownership: [ ROLE_CHIEF_EDITOR ] view: [ ROLE_CHIEF_EDITOR ] # = show in menu, show listings, open 'edit' view without actually being able to edit, any of the other permissions always imply 'view' -# these permissions are used as a default for contenttypes, they are added to the base permissions +# these permissions are used as a default for contenttypes, they are added to the base permissions # you can override these settings per contenttype by adding it to the `contenttypes:` array contenttype-default: edit: [ ROLE_EDITOR, CONTENT_OWNER ] @@ -144,7 +143,7 @@ contenttypes: # edit: [ ROLE_EDITOR, CONTENT_OWNER ] # create: [ ROLE_EDITOR ] # change-ownership: [ CONTENT_OWNER ] -# view: [ ROLE_EDITOR ] +# view: [ ROLE_EDITOR ] # homepage: # singleton # view: [ ROLE_EDITOR ] # edit: [ ROLE_EDITOR ] diff --git a/src/Entity/Content.php b/src/Entity/Content.php index 7d3a1dd94..3d0593310 100644 --- a/src/Entity/Content.php +++ b/src/Entity/Content.php @@ -24,10 +24,9 @@ /** * @ApiResource( * normalizationContext={"groups"={"get_content","get_definition"}}, - * denormalizationContext={"groups"={"api_write"},"enable_max_depth"=true}, * collectionOperations={ * "get"={"security"="is_granted('api:get')"}, - * "post"={"security"="is_granted('api:post')"} + * "post"={"security"="is_granted(‘api:post’)"} * }, * itemOperations={ * "get"={"security"="is_granted('api:get')"}, diff --git a/src/Entity/Field.php b/src/Entity/Field.php index f52989421..734d62eaa 100644 --- a/src/Entity/Field.php +++ b/src/Entity/Field.php @@ -32,7 +32,7 @@ * }, * collectionOperations={ * "get"={"security"="is_granted('api:get')"}, - * "post"={"security"="is_granted('api:post')"} + * "post"={"security"="is_granted(‘api:post’)"} * }, * itemOperations={ * "get"={"security"="is_granted('api:get')"}, From 77f27dd670a93dd00adc6a699e3361f6aa7e527f Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Tue, 16 Nov 2021 16:45:00 +0100 Subject: [PATCH 16/30] Creating content --- src/Entity/Content.php | 5 +++-- src/Entity/Field.php | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Entity/Content.php b/src/Entity/Content.php index 3d0593310..7beed2555 100644 --- a/src/Entity/Content.php +++ b/src/Entity/Content.php @@ -24,9 +24,10 @@ /** * @ApiResource( * normalizationContext={"groups"={"get_content","get_definition"}}, + * denormalizationContext={"groups"={"api_write"},"enable_max_depth"=true}, * collectionOperations={ * "get"={"security"="is_granted('api:get')"}, - * "post"={"security"="is_granted(‘api:post’)"} + * "post"={"security"="is_granted('api:post')"} * }, * itemOperations={ * "get"={"security"="is_granted('api:get')"}, @@ -721,7 +722,7 @@ private function convertToLocalFromDatabase(?\DateTime $dateTime): ?\DateTime /** * All date/timestamps are created in the current local timezone by default. - * Dates/timestamps must be stored in UTC in the database. This method converts + * Dates/timestamps must be stored in UTC in the dat abase. This method converts * the local date to UTC. */ private function convertToUTCFromLocal(?\DateTime $dateTime): ?\DateTime diff --git a/src/Entity/Field.php b/src/Entity/Field.php index 734d62eaa..f52989421 100644 --- a/src/Entity/Field.php +++ b/src/Entity/Field.php @@ -32,7 +32,7 @@ * }, * collectionOperations={ * "get"={"security"="is_granted('api:get')"}, - * "post"={"security"="is_granted(‘api:post’)"} + * "post"={"security"="is_granted('api:post')"} * }, * itemOperations={ * "get"={"security"="is_granted('api:get')"}, From abcb536a9c06ad394d4784e74a97c11ce918cd19 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Tue, 16 Nov 2021 17:14:03 +0100 Subject: [PATCH 17/30] Add WIP for persistence --- src/Api/ContentDataPersister.php | 30 ++++-------------------- src/Event/Listener/FieldFillListener.php | 5 ++++ 2 files changed, 10 insertions(+), 25 deletions(-) diff --git a/src/Api/ContentDataPersister.php b/src/Api/ContentDataPersister.php index 73691ff9f..c5e1bf98b 100644 --- a/src/Api/ContentDataPersister.php +++ b/src/Api/ContentDataPersister.php @@ -3,23 +3,15 @@ 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) + public function __construct(ContextAwareDataPersisterInterface $decorated) { $this->decorated = $decorated; - $this->config = $config; } public function supports($data, array $context = []): bool @@ -29,22 +21,10 @@ public function supports($data, array $context = []): bool 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); - - $newField->setName($field->getName()); - $newField->setValue($field->getValue()); - - $data->removeField($field); - $data->addField($newField); - } - } + // Here we need to make some adjustments. + // Like setting the proper author, making the fields + // the right type, etc. + dd("PERSISTING"); $this->decorated->persist($data, $context); } diff --git a/src/Event/Listener/FieldFillListener.php b/src/Event/Listener/FieldFillListener.php index 1cfb783ae..e1b85e3da 100644 --- a/src/Event/Listener/FieldFillListener.php +++ b/src/Event/Listener/FieldFillListener.php @@ -54,6 +54,11 @@ public function preUpdate(LifecycleEventArgs $args): void $value = $this->clean($field->getParsedValue()); $field->setValue($value); } + + if ($field->getType() === 'generic') { + dump($field->getDefinition()->get('type')); + die; + } } } From 23ae2e5bbb9515a153b36108b9f93744797ab47e Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Thu, 18 Nov 2021 16:37:52 +0100 Subject: [PATCH 18/30] Make the correct field types --- src/Api/ContentDataPersister.php | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/Api/ContentDataPersister.php b/src/Api/ContentDataPersister.php index c5e1bf98b..55ecf3f41 100644 --- a/src/Api/ContentDataPersister.php +++ b/src/Api/ContentDataPersister.php @@ -3,15 +3,23 @@ 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; - public function __construct(ContextAwareDataPersisterInterface $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 @@ -21,10 +29,23 @@ public function supports($data, array $context = []): bool public function persist($data, array $context = []) { - // Here we need to make some adjustments. - // Like setting the proper author, making the fields - // the right type, etc. - dd("PERSISTING"); + 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); + + $newField->setName($field->getName()); + $newField->setValue($field->getValue()); + + $data->removeField($field); + $data->addField($newField); + } + + } $this->decorated->persist($data, $context); } From 5dce70b73f27bd3deaec507eda603658136ea0de Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Thu, 18 Nov 2021 16:40:44 +0100 Subject: [PATCH 19/30] Update Content.php --- src/Entity/Content.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Content.php b/src/Entity/Content.php index 7beed2555..7d3a1dd94 100644 --- a/src/Entity/Content.php +++ b/src/Entity/Content.php @@ -722,7 +722,7 @@ private function convertToLocalFromDatabase(?\DateTime $dateTime): ?\DateTime /** * All date/timestamps are created in the current local timezone by default. - * Dates/timestamps must be stored in UTC in the dat abase. This method converts + * Dates/timestamps must be stored in UTC in the database. This method converts * the local date to UTC. */ private function convertToUTCFromLocal(?\DateTime $dateTime): ?\DateTime From 0e1c42867010e2944a68b6b528e58d46938c5508 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Thu, 18 Nov 2021 16:41:33 +0100 Subject: [PATCH 20/30] Update FieldFillListener.php --- src/Event/Listener/FieldFillListener.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Event/Listener/FieldFillListener.php b/src/Event/Listener/FieldFillListener.php index e1b85e3da..1cfb783ae 100644 --- a/src/Event/Listener/FieldFillListener.php +++ b/src/Event/Listener/FieldFillListener.php @@ -54,11 +54,6 @@ public function preUpdate(LifecycleEventArgs $args): void $value = $this->clean($field->getParsedValue()); $field->setValue($value); } - - if ($field->getType() === 'generic') { - dump($field->getDefinition()->get('type')); - die; - } } } From d932f8ab3d299d2d7361e5e3c7a7eb8cb01e4785 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Tue, 16 Nov 2021 16:45:00 +0100 Subject: [PATCH 21/30] Creating content --- src/Entity/Content.php | 2 +- src/Entity/Field.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Entity/Content.php b/src/Entity/Content.php index 7d3a1dd94..7beed2555 100644 --- a/src/Entity/Content.php +++ b/src/Entity/Content.php @@ -722,7 +722,7 @@ private function convertToLocalFromDatabase(?\DateTime $dateTime): ?\DateTime /** * All date/timestamps are created in the current local timezone by default. - * Dates/timestamps must be stored in UTC in the database. This method converts + * Dates/timestamps must be stored in UTC in the dat abase. This method converts * the local date to UTC. */ private function convertToUTCFromLocal(?\DateTime $dateTime): ?\DateTime diff --git a/src/Entity/Field.php b/src/Entity/Field.php index f52989421..d1413cd2e 100644 --- a/src/Entity/Field.php +++ b/src/Entity/Field.php @@ -78,7 +78,7 @@ class Field implements FieldInterface, TranslatableInterface private $version; /** - * @ORM\ManyToOne(targetEntity="Bolt\Entity\Content", inversedBy="fields", cascade={"persist"}, fetch="EAGER") + * @ORM\ManyToOne(targetEntity="Bolt\Entity\Content", inversedBy="fields", fetch="EAGER") * @ORM\JoinColumn(nullable=false) * @Groups("api_write") */ From 36587fb48af108db836df7775858514edd0b62a5 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Tue, 16 Nov 2021 17:14:03 +0100 Subject: [PATCH 22/30] Add WIP for persistence --- src/Api/ContentDataPersister.php | 31 ++++-------------------- src/Event/Listener/FieldFillListener.php | 5 ++++ 2 files changed, 10 insertions(+), 26 deletions(-) diff --git a/src/Api/ContentDataPersister.php b/src/Api/ContentDataPersister.php index 55ecf3f41..c5e1bf98b 100644 --- a/src/Api/ContentDataPersister.php +++ b/src/Api/ContentDataPersister.php @@ -3,23 +3,15 @@ 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) + public function __construct(ContextAwareDataPersisterInterface $decorated) { $this->decorated = $decorated; - $this->config = $config; } public function supports($data, array $context = []): bool @@ -29,23 +21,10 @@ public function supports($data, array $context = []): bool 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); - - $newField->setName($field->getName()); - $newField->setValue($field->getValue()); - - $data->removeField($field); - $data->addField($newField); - } - - } + // Here we need to make some adjustments. + // Like setting the proper author, making the fields + // the right type, etc. + dd("PERSISTING"); $this->decorated->persist($data, $context); } diff --git a/src/Event/Listener/FieldFillListener.php b/src/Event/Listener/FieldFillListener.php index 1cfb783ae..e1b85e3da 100644 --- a/src/Event/Listener/FieldFillListener.php +++ b/src/Event/Listener/FieldFillListener.php @@ -54,6 +54,11 @@ public function preUpdate(LifecycleEventArgs $args): void $value = $this->clean($field->getParsedValue()); $field->setValue($value); } + + if ($field->getType() === 'generic') { + dump($field->getDefinition()->get('type')); + die; + } } } From 78b669d911bd4030497310eed01e4c2e03745885 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Thu, 18 Nov 2021 16:37:52 +0100 Subject: [PATCH 23/30] Make the correct field types --- src/Api/ContentDataPersister.php | 31 ++++++++++++++++++++++++++----- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/src/Api/ContentDataPersister.php b/src/Api/ContentDataPersister.php index c5e1bf98b..55ecf3f41 100644 --- a/src/Api/ContentDataPersister.php +++ b/src/Api/ContentDataPersister.php @@ -3,15 +3,23 @@ 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; - public function __construct(ContextAwareDataPersisterInterface $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 @@ -21,10 +29,23 @@ public function supports($data, array $context = []): bool public function persist($data, array $context = []) { - // Here we need to make some adjustments. - // Like setting the proper author, making the fields - // the right type, etc. - dd("PERSISTING"); + 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); + + $newField->setName($field->getName()); + $newField->setValue($field->getValue()); + + $data->removeField($field); + $data->addField($newField); + } + + } $this->decorated->persist($data, $context); } From f9722af5113093463faa39fb968867d88a790a0c Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Thu, 18 Nov 2021 16:40:44 +0100 Subject: [PATCH 24/30] Update Content.php --- src/Entity/Content.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Entity/Content.php b/src/Entity/Content.php index 7beed2555..7d3a1dd94 100644 --- a/src/Entity/Content.php +++ b/src/Entity/Content.php @@ -722,7 +722,7 @@ private function convertToLocalFromDatabase(?\DateTime $dateTime): ?\DateTime /** * All date/timestamps are created in the current local timezone by default. - * Dates/timestamps must be stored in UTC in the dat abase. This method converts + * Dates/timestamps must be stored in UTC in the database. This method converts * the local date to UTC. */ private function convertToUTCFromLocal(?\DateTime $dateTime): ?\DateTime From b3e4415c81f7be11f9fad845ab21ee1e3f4e0080 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Thu, 18 Nov 2021 16:41:33 +0100 Subject: [PATCH 25/30] Update FieldFillListener.php --- src/Event/Listener/FieldFillListener.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/Event/Listener/FieldFillListener.php b/src/Event/Listener/FieldFillListener.php index e1b85e3da..1cfb783ae 100644 --- a/src/Event/Listener/FieldFillListener.php +++ b/src/Event/Listener/FieldFillListener.php @@ -54,11 +54,6 @@ public function preUpdate(LifecycleEventArgs $args): void $value = $this->clean($field->getParsedValue()); $field->setValue($value); } - - if ($field->getType() === 'generic') { - dump($field->getDefinition()->get('type')); - die; - } } } From 9caa2a951612d4436694ec1265b97b80574b91a8 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Fri, 19 Nov 2021 10:46:52 +0100 Subject: [PATCH 26/30] csfix --- src/Api/ContentDataPersister.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Api/ContentDataPersister.php b/src/Api/ContentDataPersister.php index 55ecf3f41..73691ff9f 100644 --- a/src/Api/ContentDataPersister.php +++ b/src/Api/ContentDataPersister.php @@ -34,7 +34,7 @@ public function persist($data, array $context = []) $data->setDefinitionFromContentTypesConfig($contentTypes); - foreach($data->getFields() as $field) { + foreach ($data->getFields() as $field) { $fieldDefinition = FieldType::factory($field->getName(), $data->getDefinition()); $newField = FieldRepository::factory($fieldDefinition); @@ -44,7 +44,6 @@ public function persist($data, array $context = []) $data->removeField($field); $data->addField($newField); } - } $this->decorated->persist($data, $context); From 17552548e7632bfa199bf265d16f172dfed6ceb4 Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Tue, 23 Nov 2021 09:59:21 +0100 Subject: [PATCH 27/30] Cleanup and documenting --- config/packages/security.yaml | 2 +- src/Api/ContentDataPersister.php | 2 ++ src/Entity/Field/CollectionField.php | 2 ++ src/Entity/Field/SetField.php | 2 ++ 4 files changed, 7 insertions(+), 1 deletion(-) diff --git a/config/packages/security.yaml b/config/packages/security.yaml index 427503166..fa9eb01ee 100644 --- a/config/packages/security.yaml +++ b/config/packages/security.yaml @@ -1,7 +1,7 @@ security: role_hierarchy: ROLE_DEVELOPER: [ROLE_ADMIN, ROLE_CAN_SWITCH_USER] - ROLE_ADMIN: [ROLE_CHIEF_EDITOR, ROLE_WEBSERVICE] + ROLE_ADMIN: [ROLE_CHIEF_EDITOR] ROLE_CHIEF_EDITOR: [ROLE_EDITOR] ROLE_EDITOR: [ROLE_USER] # ROLE_USER is assigned to Bolt Entity Users if no roles have been set diff --git a/src/Api/ContentDataPersister.php b/src/Api/ContentDataPersister.php index 73691ff9f..25ac9d3a2 100644 --- a/src/Api/ContentDataPersister.php +++ b/src/Api/ContentDataPersister.php @@ -38,6 +38,8 @@ public function persist($data, array $context = []) $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()); diff --git a/src/Entity/Field/CollectionField.php b/src/Entity/Field/CollectionField.php index 7c0ff9e56..02d1ab818 100644 --- a/src/Entity/Field/CollectionField.php +++ b/src/Entity/Field/CollectionField.php @@ -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); } diff --git a/src/Entity/Field/SetField.php b/src/Entity/Field/SetField.php index fdda6b441..f9c8fb3f4 100644 --- a/src/Entity/Field/SetField.php +++ b/src/Entity/Field/SetField.php @@ -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; } From a6ddceff78e87945e7455f465b157dec8bb8892b Mon Sep 17 00:00:00 2001 From: Ivo Valchev Date: Tue, 23 Nov 2021 10:12:07 +0100 Subject: [PATCH 28/30] Add yaml migrations --- yaml-migrations/m_2021-11-23-security.yaml | 14 ++++++++++++++ yaml-migrations/m_2021-11-23-services.yaml | 9 +++++++++ 2 files changed, 23 insertions(+) create mode 100644 yaml-migrations/m_2021-11-23-security.yaml create mode 100644 yaml-migrations/m_2021-11-23-services.yaml diff --git a/yaml-migrations/m_2021-11-23-security.yaml b/yaml-migrations/m_2021-11-23-security.yaml new file mode 100644 index 000000000..2afd52491 --- /dev/null +++ b/yaml-migrations/m_2021-11-23-security.yaml @@ -0,0 +1,14 @@ +# See: https://github.com/bolt/core/pull/2648 + +file: packages/security.yaml +since: 5.1.0 + +add: + security: + firewalls: + api: + pattern: ^/api + http_basic: ~ + + role_hierarchy: + ROLE_WEBSERVICE: [] diff --git a/yaml-migrations/m_2021-11-23-services.yaml b/yaml-migrations/m_2021-11-23-services.yaml new file mode 100644 index 000000000..71106f756 --- /dev/null +++ b/yaml-migrations/m_2021-11-23-services.yaml @@ -0,0 +1,9 @@ +# See: https://github.com/bolt/core/pull/2648 + +file: services.yaml +since: 5.1.0 + +add: + services: + Bolt\Api\ContentDataPersister: + decorates: 'api_platform.doctrine.orm.data_persister' From 922d2b2378e0439f0b8e5f7fc67c381077625247 Mon Sep 17 00:00:00 2001 From: Joossensei Date: Tue, 23 Nov 2021 13:20:07 +0100 Subject: [PATCH 29/30] Add new tests and fix old ones This should work :D --- .../integration/api_getcontent.spec.js | 150 ++++++++++++++++++ .../integration/edit_record_1_field.spec.js | 4 +- tests/cypress/support/index.js | 2 + 3 files changed, 154 insertions(+), 2 deletions(-) create mode 100644 tests/cypress/integration/api_getcontent.spec.js diff --git a/tests/cypress/integration/api_getcontent.spec.js b/tests/cypress/integration/api_getcontent.spec.js new file mode 100644 index 000000000..5dd0b4ae6 --- /dev/null +++ b/tests/cypress/integration/api_getcontent.spec.js @@ -0,0 +1,150 @@ +/// + +describe('As a user I want to fetch all contents of an API' , () => { + it('Checks that GET response equals 200', () => { + cy.login(); + cy.visit('/bolt/api'); + cy.get('#operations-Content-getContentCollection').eq(0).click(); + cy.get('.response-col_status').should('contain', '200'); + }) + + it('Checks if the contents.json is filled with all content', () => { + cy.login(); + cy.request({ + url: '/api/contents.json', + failOnStatusCode: false, + auth: { + username: 'admin', + password: 'admin%1', + }, + }).then((response) => { + return new Promise(resolve => { + expect(response).property('status').to.eq(200) + expect(response.body[0]).property('id').to.not.be.oneOf([null, ""]) + const respBody = response.body[0]; + const fieldId = respBody; + resolve(fieldId) + }); + }) + }) + + it('Check if it returns JSON of a single record', () => { + cy.login(); + cy.request({ + url: '/api/contents/1.json', + failOnStatusCode: false, + auth: { + username: 'admin', + password: 'admin%1', + }, + }).then((response) => { + return new Promise(resolve => { + expect(response).property('status').to.eq(200) + expect(response.body).property('id').to.not.be.oneOf([null, ""]) + const respBody = response.body[0]; + const fieldId = respBody; + resolve(fieldId) + }); + }) + }) + + it('Check if the JSON LD format is working', () => { + cy.login(); + cy.request({ + url: '/api/contents.jsonld', + failOnStatusCode: false, + auth: { + username: 'admin', + password: 'admin%1', + }, + }).then((response) => { + return new Promise(resolve => { + expect(response).property('status').to.eq(200) + expect(response.body).property('hydra:totalItems').to.not.be.oneOf([null, "", 0]) + const respBody = response.body; + const fieldId = respBody; + resolve(fieldId) + }); + }) + }) + //TODO fix this test once we can navigate inside object + it('Check if the JSON LD format is working for single contenttypes like homepage', () => { + cy.login(); + cy.request({ + url: '/api/contents.jsonld?contentType=homepage', + failOnStatusCode: false, + auth: { + username: 'admin', + password: 'admin%1', + }, + }).then((response) => { + return new Promise(resolve => { + expect(response).property('status').to.eq(200) + expect(response.body).property('hydra:totalItems').to.not.be.oneOf([null, "", 0]) + const respBody = response.body; + const fieldId = respBody; + resolve(fieldId) + }); + }) + }) + + it('Check if the JSON LD format is working for single records', () => { + cy.login(); + cy.request({ + url: '/api/contents/1.jsonld', + failOnStatusCode: false, + auth: { + username: 'admin', + password: 'admin%1', + }, + }).then((response) => { + return new Promise(resolve => { + expect(response).property('status').to.eq(200) + expect(response.body).property('id').to.not.be.oneOf([null, ""]) + const respBody = response.body; + const fieldId = respBody; + resolve(fieldId) + }); + }) + }) +}) + +describe('Test reading JSON Fields', () => { + it('should read the values of the returned data in JSON', () => { + cy.request({ + url:`/api/contents/1/fields.json`, + failOnStatusCode: false, + auth: { + username: 'admin', + password: 'admin%1', + }, + }).then((response) => { + return new Promise(resolve => { + expect(response).property('status').to.eq(200) + expect(response.body[0]).property('name').to.not.be.oneOf([null, ""]) + const respBody = response.body[0]; + const fieldId = respBody; + resolve(fieldId) + }); + }) + }) + + it('should read the values of the returned data in JSON ld', () => { + cy.request({ + url:`/api/contents/1/fields.jsonld`, + failOnStatusCode: false, + auth: { + username: 'admin', + password: 'admin%1', + }, + }).then((response) => { + return new Promise(resolve => { + expect(response).property('status').to.eq(200) + expect(response.body).property('hydra:totalItems').to.not.be.oneOf([null, "", 0]) + const respBody = response.body; + const fieldId = respBody; + resolve(fieldId) + }); + }) + }) +}) diff --git a/tests/cypress/integration/edit_record_1_field.spec.js b/tests/cypress/integration/edit_record_1_field.spec.js index c4019d898..fa5e4ece4 100644 --- a/tests/cypress/integration/edit_record_1_field.spec.js +++ b/tests/cypress/integration/edit_record_1_field.spec.js @@ -46,8 +46,8 @@ describe('As an Admin, I want to reset an image field', () => { cy.get('a[id="media-tab"]').click(); cy.get("label[for=field-image]").should('contain', 'Image'); - cy.get('input[name="fields[image][filename]"]').should('have.value', 'foal.jpg'); - cy.get('input[name="fields[image][alt]"]').should('have.value', 'Ex veniam repellat ipsam autem delectus.'); + cy.get('input[name="fields[image][filename]"]').should('have.value', 'stock/image_40862.jpg'); + cy.get('input[name="fields[image][alt]"]').should('have.value', 'Voluptate nemo quam natus harum numquam.'); cy.get('button[class="btn btn-sm btn-hidden-danger"]').should('contain', 'Remove').eq(0).click(); cy.get('input[name="fields[image][filename]"]').should('have.value', ''); diff --git a/tests/cypress/support/index.js b/tests/cypress/support/index.js index cfcd5aebc..02fe3beb7 100644 --- a/tests/cypress/support/index.js +++ b/tests/cypress/support/index.js @@ -31,6 +31,8 @@ Cypress.Commands.add('login', (username = 'admin', password = 'admin%1') => { cy.url().should('include', '/bolt/login'); + cy.get('input[name="login[username]"]').type('{selectall}{backspace}'); + cy.get('input[name="login[username]"]').type(username); cy.get('input[name="login[password]"]').type(password + '{enter}'); From 3dfd45a23f287aa5cbc9b2920727a914fc7fa464 Mon Sep 17 00:00:00 2001 From: Joossensei Date: Tue, 23 Nov 2021 13:55:46 +0100 Subject: [PATCH 30/30] Revert changes to standard tests Ill make a different pull request for these --- tests/cypress/integration/edit_record_1_field.spec.js | 4 ++-- tests/cypress/support/index.js | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/cypress/integration/edit_record_1_field.spec.js b/tests/cypress/integration/edit_record_1_field.spec.js index fa5e4ece4..c4019d898 100644 --- a/tests/cypress/integration/edit_record_1_field.spec.js +++ b/tests/cypress/integration/edit_record_1_field.spec.js @@ -46,8 +46,8 @@ describe('As an Admin, I want to reset an image field', () => { cy.get('a[id="media-tab"]').click(); cy.get("label[for=field-image]").should('contain', 'Image'); - cy.get('input[name="fields[image][filename]"]').should('have.value', 'stock/image_40862.jpg'); - cy.get('input[name="fields[image][alt]"]').should('have.value', 'Voluptate nemo quam natus harum numquam.'); + cy.get('input[name="fields[image][filename]"]').should('have.value', 'foal.jpg'); + cy.get('input[name="fields[image][alt]"]').should('have.value', 'Ex veniam repellat ipsam autem delectus.'); cy.get('button[class="btn btn-sm btn-hidden-danger"]').should('contain', 'Remove').eq(0).click(); cy.get('input[name="fields[image][filename]"]').should('have.value', ''); diff --git a/tests/cypress/support/index.js b/tests/cypress/support/index.js index 02fe3beb7..cfcd5aebc 100644 --- a/tests/cypress/support/index.js +++ b/tests/cypress/support/index.js @@ -31,8 +31,6 @@ Cypress.Commands.add('login', (username = 'admin', password = 'admin%1') => { cy.url().should('include', '/bolt/login'); - cy.get('input[name="login[username]"]').type('{selectall}{backspace}'); - cy.get('input[name="login[username]"]').type(username); cy.get('input[name="login[password]"]').type(password + '{enter}');