From 0ef5628d44d5ee64ecce348383fe4b822362a80d Mon Sep 17 00:00:00 2001 From: Dan Chadwick Date: Thu, 22 May 2025 17:11:07 -0400 Subject: [PATCH] adding everything --- block_content.info.yml | 10 + block_content.install | 65 ++ block_content.links.action.yml | 13 + block_content.links.contextual.yml | 4 + block_content.links.menu.yml | 5 + block_content.links.task.yml | 19 + block_content.module | 243 +++++++ block_content.pages.inc | 37 ++ block_content.permissions.yml | 16 + block_content.post_update.php | 94 +++ block_content.routing.yml | 132 ++++ block_content.services.yml | 11 + ...re.entity_view_mode.block_content.full.yml | 10 + .../field.storage.block_content.body.yml | 18 + config/optional/views.view.block_content.yml | 550 ++++++++++++++++ config/schema/block_content.schema.yml | 24 + help_topics/block_content.add.html.twig | 20 + help_topics/block_content.type.html.twig | 24 + migrations/block_content_body_field.yml | 38 ++ migrations/block_content_entity_display.yml | 40 ++ .../block_content_entity_form_display.yml | 37 ++ migrations/block_content_type.yml | 24 + migrations/d6_custom_block.yml | 24 + migrations/d7_custom_block.yml | 24 + .../state/block_content.migrate_drupal.yml | 5 + src/Access/AccessGroupAnd.php | 49 ++ src/Access/DependentAccessInterface.php | 35 + .../RefinableDependentAccessInterface.php | 48 ++ src/Access/RefinableDependentAccessTrait.php | 52 ++ src/BlockContentAccessControlHandler.php | 116 ++++ src/BlockContentEvents.php | 31 + src/BlockContentForm.php | 134 ++++ src/BlockContentInterface.php | 84 +++ src/BlockContentListBuilder.php | 60 ++ src/BlockContentPermissions.php | 84 +++ src/BlockContentStorageSchema.php | 27 + src/BlockContentTranslationHandler.php | 22 + src/BlockContentTypeForm.php | 128 ++++ src/BlockContentTypeInterface.php | 21 + src/BlockContentTypeListBuilder.php | 53 ++ src/BlockContentUuidLookup.php | 64 ++ src/BlockContentViewBuilder.php | 50 ++ src/BlockContentViewsData.php | 42 ++ src/BlockTypeAccessControlHandler.php | 33 + src/Controller/BlockContentController.php | 239 +++++++ src/Entity/BlockContent.php | 274 ++++++++ src/Entity/BlockContentType.php | 100 +++ src/Event/BlockContentGetDependencyEvent.php | 70 ++ src/Form/BlockContentDeleteForm.php | 25 + src/Form/BlockContentListFiltersForm.php | 80 +++ src/Form/BlockContentTypeDeleteForm.php | 34 + src/Plugin/Block/BlockContentBlock.php | 218 ++++++ src/Plugin/Derivative/BlockContent.php | 60 ++ .../BlockContentAddLocalAction.php | 65 ++ .../BlockContentEntityChangedConstraint.php | 18 + ...ontentEntityChangedConstraintValidator.php | 30 + src/Plugin/migrate/source/d6/Box.php | 53 ++ .../migrate/source/d6/BoxTranslation.php | 28 + src/Plugin/migrate/source/d7/BlockCustom.php | 49 ++ .../source/d7/BlockCustomTranslation.php | 105 +++ src/Plugin/views/area/ListingEmpty.php | 89 +++ src/Plugin/views/wizard/BlockContent.php | 36 + src/Routing/RouteSubscriber.php | 237 +++++++ templates/block-content-add-list.html.twig | 24 + .../views.view.block_content_2862564.yml | 540 +++++++++++++++ .../block_content_test.info.yml | 7 + .../block_content_test.module | 69 ++ .../block_content_test.routing.yml | 6 + .../install/block.block.foobar_gorilla.yml | 26 + .../TestSelection.php | 80 +++ .../block_content_test_views.info.yml | 8 + ...est_block_content_redirect_destination.yml | 233 +++++++ ...ws.view.test_block_content_revision_id.yml | 66 ++ ...est_block_content_revision_revision_id.yml | 69 ++ ....view.test_block_content_revision_user.yml | 324 +++++++++ .../views.view.test_block_content_view.yml | 190 ++++++ .../views.view.test_field_filters.yml | 338 ++++++++++ .../test_views/views.view.test_field_type.yml | 27 + .../Functional/BlockContentCacheTagsTest.php | 100 +++ .../BlockContentContextualLinksTest.php | 44 ++ .../Functional/BlockContentCreationTest.php | 336 ++++++++++ tests/src/Functional/BlockContentListTest.php | 203 ++++++ .../Functional/BlockContentListViewsTest.php | 205 ++++++ .../Functional/BlockContentPageViewTest.php | 40 ++ .../Functional/BlockContentRedirectTest.php | 74 +++ .../BlockContentRevisionDeleteTest.php | 96 +++ .../BlockContentRevisionRevertTest.php | 98 +++ ...BlockContentRevisionVersionHistoryTest.php | 94 +++ .../Functional/BlockContentRevisionsTest.php | 111 ++++ tests/src/Functional/BlockContentSaveTest.php | 112 ++++ tests/src/Functional/BlockContentTestBase.php | 138 ++++ .../BlockContentTranslationUITest.php | 158 +++++ tests/src/Functional/BlockContentTypeTest.php | 266 ++++++++ tests/src/Functional/GenericTest.php | 14 + tests/src/Functional/PageEditTest.php | 98 +++ .../Rest/BlockContentJsonAnonTest.php | 31 + .../Rest/BlockContentJsonBasicAuthTest.php | 41 ++ .../Rest/BlockContentJsonCookieTest.php | 36 + .../Rest/BlockContentResourceTestBase.php | 239 +++++++ .../Rest/BlockContentTypeJsonAnonTest.php | 31 + .../BlockContentTypeJsonBasicAuthTest.php | 41 ++ .../Rest/BlockContentTypeJsonCookieTest.php | 36 + .../Rest/BlockContentTypeResourceTestBase.php | 74 +++ .../Rest/BlockContentTypeXmlAnonTest.php | 33 + .../Rest/BlockContentTypeXmlBasicAuthTest.php | 43 ++ .../Rest/BlockContentTypeXmlCookieTest.php | 38 ++ .../Rest/BlockContentXmlAnonTest.php | 33 + .../Rest/BlockContentXmlBasicAuthTest.php | 43 ++ .../Rest/BlockContentXmlCookieTest.php | 38 ++ tests/src/Functional/UnpublishedBlockTest.php | 54 ++ .../Update/BlockContentRemoveConstraint.php | 71 ++ ...lockContentReusableIndexUpdatePathTest.php | 36 + .../Update/BlockContentUpdateTest.php | 128 ++++ .../Views/BlockContentFieldFilterTest.php | 117 ++++ .../Views/BlockContentIntegrationTest.php | 76 +++ .../Views/BlockContentRedirectTest.php | 55 ++ .../Functional/Views/BlockContentTestBase.php | 109 +++ .../Views/BlockContentWizardTest.php | 59 ++ .../Kernel/BlockContentAccessHandlerTest.php | 621 ++++++++++++++++++ tests/src/Kernel/BlockContentDeletionTest.php | 86 +++ tests/src/Kernel/BlockContentDeriverTest.php | 124 ++++ ...ockContentEntityReferenceSelectionTest.php | 195 ++++++ .../Kernel/BlockContentPermissionsTest.php | 87 +++ .../src/Kernel/BlockContentRevisionsTest.php | 55 ++ tests/src/Kernel/BlockContentTest.php | 77 +++ .../Kernel/BlockContentTypeValidationTest.php | 41 ++ .../Kernel/BlockTemplateSuggestionsTest.php | 88 +++ .../MigrateBlockContentBodyFieldTest.php | 58 ++ .../MigrateBlockContentEntityDisplayTest.php | 59 ++ ...grateBlockContentEntityFormDisplayTest.php | 59 ++ .../Migrate/MigrateBlockContentStubTest.php | 55 ++ .../Migrate/MigrateBlockContentTypeTest.php | 43 ++ .../Migrate/d6/MigrateBlockContentTest.php | 62 ++ ...grateCustomBlockContentTranslationTest.php | 73 ++ ...grateCustomBlockContentTranslationTest.php | 69 ++ .../Migrate/d7/MigrateCustomBlockTest.php | 57 ++ .../Plugin/migrate/source/d6/BoxTest.php | 48 ++ .../migrate/source/d6/BoxTranslationTest.php | 143 ++++ .../migrate/source/d7/BlockCustomTest.php | 42 ++ .../source/d7/BlockCustomTranslationTest.php | 166 +++++ tests/src/Kernel/Views/FieldTypeTest.php | 77 +++ .../Views/RevisionRelationshipsTest.php | 102 +++ tests/src/Kernel/Views/RevisionUserTest.php | 153 +++++ tests/src/Unit/Access/AccessGroupAndTest.php | 57 ++ .../Unit/Access/AccessibleTestingTrait.php | 38 ++ tests/src/Unit/Access/DependentAccessTest.php | 161 +++++ .../Unit/Menu/BlockContentLocalTasksTest.php | 89 +++ 147 files changed, 13071 insertions(+) create mode 100644 block_content.info.yml create mode 100644 block_content.install create mode 100644 block_content.links.action.yml create mode 100644 block_content.links.contextual.yml create mode 100644 block_content.links.menu.yml create mode 100644 block_content.links.task.yml create mode 100644 block_content.module create mode 100644 block_content.pages.inc create mode 100644 block_content.permissions.yml create mode 100644 block_content.post_update.php create mode 100644 block_content.routing.yml create mode 100644 block_content.services.yml create mode 100644 config/install/core.entity_view_mode.block_content.full.yml create mode 100644 config/install/field.storage.block_content.body.yml create mode 100644 config/optional/views.view.block_content.yml create mode 100644 config/schema/block_content.schema.yml create mode 100644 help_topics/block_content.add.html.twig create mode 100644 help_topics/block_content.type.html.twig create mode 100644 migrations/block_content_body_field.yml create mode 100644 migrations/block_content_entity_display.yml create mode 100644 migrations/block_content_entity_form_display.yml create mode 100644 migrations/block_content_type.yml create mode 100644 migrations/d6_custom_block.yml create mode 100644 migrations/d7_custom_block.yml create mode 100644 migrations/state/block_content.migrate_drupal.yml create mode 100644 src/Access/AccessGroupAnd.php create mode 100644 src/Access/DependentAccessInterface.php create mode 100644 src/Access/RefinableDependentAccessInterface.php create mode 100644 src/Access/RefinableDependentAccessTrait.php create mode 100644 src/BlockContentAccessControlHandler.php create mode 100644 src/BlockContentEvents.php create mode 100644 src/BlockContentForm.php create mode 100644 src/BlockContentInterface.php create mode 100644 src/BlockContentListBuilder.php create mode 100644 src/BlockContentPermissions.php create mode 100644 src/BlockContentStorageSchema.php create mode 100644 src/BlockContentTranslationHandler.php create mode 100644 src/BlockContentTypeForm.php create mode 100644 src/BlockContentTypeInterface.php create mode 100644 src/BlockContentTypeListBuilder.php create mode 100644 src/BlockContentUuidLookup.php create mode 100644 src/BlockContentViewBuilder.php create mode 100644 src/BlockContentViewsData.php create mode 100644 src/BlockTypeAccessControlHandler.php create mode 100644 src/Controller/BlockContentController.php create mode 100644 src/Entity/BlockContent.php create mode 100644 src/Entity/BlockContentType.php create mode 100644 src/Event/BlockContentGetDependencyEvent.php create mode 100644 src/Form/BlockContentDeleteForm.php create mode 100644 src/Form/BlockContentListFiltersForm.php create mode 100644 src/Form/BlockContentTypeDeleteForm.php create mode 100644 src/Plugin/Block/BlockContentBlock.php create mode 100644 src/Plugin/Derivative/BlockContent.php create mode 100644 src/Plugin/Menu/LocalAction/BlockContentAddLocalAction.php create mode 100644 src/Plugin/Validation/Constraint/BlockContentEntityChangedConstraint.php create mode 100644 src/Plugin/Validation/Constraint/BlockContentEntityChangedConstraintValidator.php create mode 100644 src/Plugin/migrate/source/d6/Box.php create mode 100644 src/Plugin/migrate/source/d6/BoxTranslation.php create mode 100644 src/Plugin/migrate/source/d7/BlockCustom.php create mode 100644 src/Plugin/migrate/source/d7/BlockCustomTranslation.php create mode 100644 src/Plugin/views/area/ListingEmpty.php create mode 100644 src/Plugin/views/wizard/BlockContent.php create mode 100644 src/Routing/RouteSubscriber.php create mode 100644 templates/block-content-add-list.html.twig create mode 100644 tests/fixtures/update/views.view.block_content_2862564.yml create mode 100644 tests/modules/block_content_test/block_content_test.info.yml create mode 100644 tests/modules/block_content_test/block_content_test.module create mode 100644 tests/modules/block_content_test/block_content_test.routing.yml create mode 100644 tests/modules/block_content_test/config/install/block.block.foobar_gorilla.yml create mode 100644 tests/modules/block_content_test/src/Plugin/EntityReferenceSelection/TestSelection.php create mode 100644 tests/modules/block_content_test_views/block_content_test_views.info.yml create mode 100644 tests/modules/block_content_test_views/test_views/views.view.test_block_content_redirect_destination.yml create mode 100644 tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_id.yml create mode 100644 tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_revision_id.yml create mode 100644 tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_user.yml create mode 100644 tests/modules/block_content_test_views/test_views/views.view.test_block_content_view.yml create mode 100644 tests/modules/block_content_test_views/test_views/views.view.test_field_filters.yml create mode 100644 tests/modules/block_content_test_views/test_views/views.view.test_field_type.yml create mode 100644 tests/src/Functional/BlockContentCacheTagsTest.php create mode 100644 tests/src/Functional/BlockContentContextualLinksTest.php create mode 100644 tests/src/Functional/BlockContentCreationTest.php create mode 100644 tests/src/Functional/BlockContentListTest.php create mode 100644 tests/src/Functional/BlockContentListViewsTest.php create mode 100644 tests/src/Functional/BlockContentPageViewTest.php create mode 100644 tests/src/Functional/BlockContentRedirectTest.php create mode 100644 tests/src/Functional/BlockContentRevisionDeleteTest.php create mode 100644 tests/src/Functional/BlockContentRevisionRevertTest.php create mode 100644 tests/src/Functional/BlockContentRevisionVersionHistoryTest.php create mode 100644 tests/src/Functional/BlockContentRevisionsTest.php create mode 100644 tests/src/Functional/BlockContentSaveTest.php create mode 100644 tests/src/Functional/BlockContentTestBase.php create mode 100644 tests/src/Functional/BlockContentTranslationUITest.php create mode 100644 tests/src/Functional/BlockContentTypeTest.php create mode 100644 tests/src/Functional/GenericTest.php create mode 100644 tests/src/Functional/PageEditTest.php create mode 100644 tests/src/Functional/Rest/BlockContentJsonAnonTest.php create mode 100644 tests/src/Functional/Rest/BlockContentJsonBasicAuthTest.php create mode 100644 tests/src/Functional/Rest/BlockContentJsonCookieTest.php create mode 100644 tests/src/Functional/Rest/BlockContentResourceTestBase.php create mode 100644 tests/src/Functional/Rest/BlockContentTypeJsonAnonTest.php create mode 100644 tests/src/Functional/Rest/BlockContentTypeJsonBasicAuthTest.php create mode 100644 tests/src/Functional/Rest/BlockContentTypeJsonCookieTest.php create mode 100644 tests/src/Functional/Rest/BlockContentTypeResourceTestBase.php create mode 100644 tests/src/Functional/Rest/BlockContentTypeXmlAnonTest.php create mode 100644 tests/src/Functional/Rest/BlockContentTypeXmlBasicAuthTest.php create mode 100644 tests/src/Functional/Rest/BlockContentTypeXmlCookieTest.php create mode 100644 tests/src/Functional/Rest/BlockContentXmlAnonTest.php create mode 100644 tests/src/Functional/Rest/BlockContentXmlBasicAuthTest.php create mode 100644 tests/src/Functional/Rest/BlockContentXmlCookieTest.php create mode 100644 tests/src/Functional/UnpublishedBlockTest.php create mode 100644 tests/src/Functional/Update/BlockContentRemoveConstraint.php create mode 100644 tests/src/Functional/Update/BlockContentReusableIndexUpdatePathTest.php create mode 100644 tests/src/Functional/Update/BlockContentUpdateTest.php create mode 100644 tests/src/Functional/Views/BlockContentFieldFilterTest.php create mode 100644 tests/src/Functional/Views/BlockContentIntegrationTest.php create mode 100644 tests/src/Functional/Views/BlockContentRedirectTest.php create mode 100644 tests/src/Functional/Views/BlockContentTestBase.php create mode 100644 tests/src/Functional/Views/BlockContentWizardTest.php create mode 100644 tests/src/Kernel/BlockContentAccessHandlerTest.php create mode 100644 tests/src/Kernel/BlockContentDeletionTest.php create mode 100644 tests/src/Kernel/BlockContentDeriverTest.php create mode 100644 tests/src/Kernel/BlockContentEntityReferenceSelectionTest.php create mode 100644 tests/src/Kernel/BlockContentPermissionsTest.php create mode 100644 tests/src/Kernel/BlockContentRevisionsTest.php create mode 100644 tests/src/Kernel/BlockContentTest.php create mode 100644 tests/src/Kernel/BlockContentTypeValidationTest.php create mode 100644 tests/src/Kernel/BlockTemplateSuggestionsTest.php create mode 100644 tests/src/Kernel/Migrate/MigrateBlockContentBodyFieldTest.php create mode 100644 tests/src/Kernel/Migrate/MigrateBlockContentEntityDisplayTest.php create mode 100644 tests/src/Kernel/Migrate/MigrateBlockContentEntityFormDisplayTest.php create mode 100644 tests/src/Kernel/Migrate/MigrateBlockContentStubTest.php create mode 100644 tests/src/Kernel/Migrate/MigrateBlockContentTypeTest.php create mode 100644 tests/src/Kernel/Migrate/d6/MigrateBlockContentTest.php create mode 100644 tests/src/Kernel/Migrate/d6/MigrateCustomBlockContentTranslationTest.php create mode 100644 tests/src/Kernel/Migrate/d7/MigrateCustomBlockContentTranslationTest.php create mode 100644 tests/src/Kernel/Migrate/d7/MigrateCustomBlockTest.php create mode 100644 tests/src/Kernel/Plugin/migrate/source/d6/BoxTest.php create mode 100644 tests/src/Kernel/Plugin/migrate/source/d6/BoxTranslationTest.php create mode 100644 tests/src/Kernel/Plugin/migrate/source/d7/BlockCustomTest.php create mode 100644 tests/src/Kernel/Plugin/migrate/source/d7/BlockCustomTranslationTest.php create mode 100644 tests/src/Kernel/Views/FieldTypeTest.php create mode 100644 tests/src/Kernel/Views/RevisionRelationshipsTest.php create mode 100644 tests/src/Kernel/Views/RevisionUserTest.php create mode 100644 tests/src/Unit/Access/AccessGroupAndTest.php create mode 100644 tests/src/Unit/Access/AccessibleTestingTrait.php create mode 100644 tests/src/Unit/Access/DependentAccessTest.php create mode 100644 tests/src/Unit/Menu/BlockContentLocalTasksTest.php diff --git a/block_content.info.yml b/block_content.info.yml new file mode 100644 index 0000000..eb68ec9 --- /dev/null +++ b/block_content.info.yml @@ -0,0 +1,10 @@ +name: 'Block Content' +type: module +description: 'Allows the creation of content blocks and block types.' +package: Core +version: VERSION +dependencies: + - drupal:block + - drupal:text + - drupal:user +configure: entity.block_content.collection diff --git a/block_content.install b/block_content.install new file mode 100644 index 0000000..fe4526e --- /dev/null +++ b/block_content.install @@ -0,0 +1,65 @@ +getEntityType('block_content'); + $routeProviders = $definition->get('route_provider'); + $routeProviders['revision'] = RevisionHtmlRouteProvider::class; + $definition + ->setFormClass('revision-delete', RevisionDeleteForm::class) + ->setFormClass('revision-revert', RevisionRevertForm::class) + ->set('route_provider', $routeProviders) + ->setLinkTemplate('revision-delete-form', '/admin/content/block/{block_content}/revision/{block_content_revision}/delete') + ->setLinkTemplate('revision-revert-form', '/admin/content/block/{block_content}/revision/{block_content_revision}/revert') + ->setLinkTemplate('version-history', '/admin/content/block/{block_content}/revisions'); + $entityDefinitionUpdateManager->updateEntityType($definition); + return \t('Added revision routes to Content block entity type.'); +} + +/** + * Remove the unique values constraint from block content info fields. + */ +function block_content_update_10200() { + $constraint = 'UniqueField'; + $definition_update_manager = \Drupal::entityDefinitionUpdateManager(); + $field_storage_definition = $definition_update_manager->getFieldStorageDefinition('info', 'block_content'); + $constraints = $field_storage_definition->getConstraints(); + if (isset($constraints[$constraint])) { + unset($constraints[$constraint]); + $field_storage_definition->setConstraints($constraints); + $definition_update_manager->updateFieldStorageDefinition($field_storage_definition); + } +} + +/** + * Apply index to reusable column. + */ +function block_content_update_10300(): void { + $manager = \Drupal::entityDefinitionUpdateManager(); + $entity_type = $manager->getEntityType('block_content') + ->setHandlerClass('storage_schema', BlockContentStorageSchema::class); + $manager->updateEntityType($entity_type); + $manager->updateFieldStorageDefinition(\Drupal::service('entity_field.manager') + ->getBaseFieldDefinitions('block_content')['reusable']); +} diff --git a/block_content.links.action.yml b/block_content.links.action.yml new file mode 100644 index 0000000..a0978bd --- /dev/null +++ b/block_content.links.action.yml @@ -0,0 +1,13 @@ +block_content_type_add: + route_name: block_content.type_add + title: 'Add block type' + appears_on: + - entity.block_content_type.collection + +block_content_add_action: + route_name: block_content.add_page + title: 'Add content block' + appears_on: + - block.admin_library + - entity.block_content.collection + class: \Drupal\block_content\Plugin\Menu\LocalAction\BlockContentAddLocalAction diff --git a/block_content.links.contextual.yml b/block_content.links.contextual.yml new file mode 100644 index 0000000..b54bcae --- /dev/null +++ b/block_content.links.contextual.yml @@ -0,0 +1,4 @@ +block_content.block_edit: + title: 'Edit' + group: block_content + route_name: 'entity.block_content.edit_form' diff --git a/block_content.links.menu.yml b/block_content.links.menu.yml new file mode 100644 index 0000000..015dfb2 --- /dev/null +++ b/block_content.links.menu.yml @@ -0,0 +1,5 @@ +entity.block_content_type.collection: + title: 'Block types' + parent: system.admin_structure + description: 'Create and manage fields, forms, and display settings for your content blocks.' + route_name: entity.block_content_type.collection diff --git a/block_content.links.task.yml b/block_content.links.task.yml new file mode 100644 index 0000000..61ac881 --- /dev/null +++ b/block_content.links.task.yml @@ -0,0 +1,19 @@ +entity.block_content.collection: + title: Blocks + route_name: entity.block_content.collection + base_route: system.admin_content + +entity.block_content.canonical: + title: Edit + route_name: entity.block_content.canonical + base_route: entity.block_content.canonical +entity.block_content.delete_form: + title: Delete + route_name: entity.block_content.delete_form + base_route: entity.block_content.canonical + +# Default tab for block type editing. +entity.block_content_type.edit_form: + title: 'Edit' + route_name: entity.block_content_type.edit_form + base_route: entity.block_content_type.edit_form diff --git a/block_content.module b/block_content.module new file mode 100644 index 0000000..00fc5a5 --- /dev/null +++ b/block_content.module @@ -0,0 +1,243 @@ +moduleExists('field_ui') ? Url::fromRoute('help.page', ['name' => 'field_ui'])->toString() : '#'; + $output = ''; + $output .= '

' . t('About') . '

'; + $output .= '

' . t('The Block Content module allows you to create and manage custom block types and content-containing blocks. For more information, see the online documentation for the Block Content module.', [':online-help' => 'https://www.drupal.org/documentation/modules/block_content']) . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '
'; + $output .= '
' . t('Creating and managing block types') . '
'; + $output .= '
' . t('Users with the Administer blocks permission can create and edit block types with fields and display settings, from the Block types page under the Structure menu. For more information about managing fields and display settings, see the Field UI module help and Field module help.', [':types' => Url::fromRoute('entity.block_content_type.collection')->toString(), ':field-ui' => $field_ui, ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '
'; + $output .= '
' . t('Creating content blocks') . '
'; + $output .= '
' . t('Users with the Administer blocks permission can create, edit, and delete content blocks of each defined block type, from the Content blocks page. After creating a block, place it in a region from the Block layout page, just like blocks provided by other modules.', [':blocks' => Url::fromRoute('block.admin_display')->toString(), ':block-library' => Url::fromRoute('entity.block_content.collection')->toString()]) . '
'; + $output .= '
'; + return $output; + } +} + +/** + * Implements hook_theme(). + */ +function block_content_theme($existing, $type, $theme, $path) { + return [ + 'block_content_add_list' => [ + 'variables' => ['content' => NULL], + 'file' => 'block_content.pages.inc', + ], + ]; +} + +/** + * Implements hook_entity_type_alter(). + */ +function block_content_entity_type_alter(array &$entity_types) { + /** @var \Drupal\Core\Entity\EntityTypeInterface[] $entity_types */ + // Add a translation handler for fields if the language module is enabled. + if (\Drupal::moduleHandler()->moduleExists('language')) { + $translation = $entity_types['block_content']->get('translation'); + $translation['block_content'] = TRUE; + $entity_types['block_content']->set('translation', $translation); + } + + // Swap out the default EntityChanged constraint with a custom one with + // different logic for inline blocks. + $constraints = $entity_types['block_content']->getConstraints(); + unset($constraints['EntityChanged']); + $constraints['BlockContentEntityChanged'] = NULL; + $entity_types['block_content']->setConstraints($constraints); +} + +/** + * Adds the default body field to a block type. + * + * @param string $block_type_id + * Id of the block type. + * @param string $label + * (optional) The label for the body instance. Defaults to 'Body' + * + * @return \Drupal\field\Entity\FieldConfig + * A Body field object. + */ +function block_content_add_body_field($block_type_id, $label = 'Body') { + // Add or remove the body field, as needed. + $field = FieldConfig::loadByName('block_content', $block_type_id, 'body'); + if (empty($field)) { + $field = FieldConfig::create([ + 'field_storage' => FieldStorageConfig::loadByName('block_content', 'body'), + 'bundle' => $block_type_id, + 'label' => $label, + 'settings' => [ + 'display_summary' => FALSE, + 'allowed_formats' => [], + ], + ]); + $field->save(); + + /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */ + $display_repository = \Drupal::service('entity_display.repository'); + + // Assign widget settings for the default form mode. + $display_repository->getFormDisplay('block_content', $block_type_id) + ->setComponent('body', [ + 'type' => 'text_textarea_with_summary', + ]) + ->save(); + + // Assign display settings for default view mode. + $display_repository->getViewDisplay('block_content', $block_type_id) + ->setComponent('body', [ + 'label' => 'hidden', + 'type' => 'text_default', + ]) + ->save(); + } + + return $field; +} + +/** + * Implements hook_query_TAG_alter(). + * + * Alters any 'entity_reference' query where the entity type is + * 'block_content' and the query has the tag 'block_content_access'. + * + * These queries should only return reusable blocks unless a condition on + * 'reusable' is explicitly set. + * + * Block_content entities that are not reusable should by default not be + * selectable as entity reference values. A module can still create an instance + * of \Drupal\Core\Entity\EntityReferenceSelection\SelectionInterface + * that will allow selection of non-reusable blocks by explicitly setting + * a condition on the 'reusable' field. + * + * @see \Drupal\block_content\BlockContentAccessControlHandler + */ +function block_content_query_entity_reference_alter(AlterableInterface $query) { + if ($query instanceof SelectInterface && $query->getMetaData('entity_type') === 'block_content' && $query->hasTag('block_content_access')) { + $data_table = \Drupal::entityTypeManager()->getDefinition('block_content')->getDataTable(); + if (array_key_exists($data_table, $query->getTables()) && !_block_content_has_reusable_condition($query->conditions(), $query->getTables())) { + $query->condition("$data_table.reusable", TRUE); + } + } +} + +/** + * Utility function to find nested conditions using the reusable field. + * + * @todo Replace this function with a call to the API in + * https://www.drupal.org/project/drupal/issues/2984930 + * + * @param array $condition + * The condition or condition group to check. + * @param array $tables + * The tables from the related select query. + * + * @see \Drupal\Core\Database\Query\SelectInterface::getTables + * + * @return bool + * Whether the conditions contain any condition using the reusable field. + */ +function _block_content_has_reusable_condition(array $condition, array $tables) { + // If this is a condition group call this function recursively for each nested + // condition until a condition is found that return TRUE. + if (isset($condition['#conjunction'])) { + foreach (array_filter($condition, 'is_array') as $nested_condition) { + if (_block_content_has_reusable_condition($nested_condition, $tables)) { + return TRUE; + } + } + return FALSE; + } + if (isset($condition['field'])) { + $field = $condition['field']; + if (is_object($field) && $field instanceof ConditionInterface) { + return _block_content_has_reusable_condition($field->conditions(), $tables); + } + $field_parts = explode('.', $field); + $data_table = \Drupal::entityTypeManager()->getDefinition('block_content')->getDataTable(); + foreach ($tables as $table) { + if ($table['table'] === $data_table && $field_parts[0] === $table['alias'] && $field_parts[1] === 'reusable') { + return TRUE; + } + } + + } + return FALSE; +} + +/** + * Implements hook_theme_suggestions_HOOK_alter() for block templates. + */ +function block_content_theme_suggestions_block_alter(array &$suggestions, array $variables) { + $suggestions_new = []; + $content = $variables['elements']['content']; + + $block_content = $variables['elements']['content']['#block_content'] ?? NULL; + + if ($block_content instanceof BlockContentInterface) { + $bundle = $content['#block_content']->bundle(); + $view_mode = strtr($variables['elements']['#configuration']['view_mode'], '.', '_'); + + $suggestions_new[] = 'block__block_content__view__' . $view_mode; + $suggestions_new[] = 'block__block_content__type__' . $bundle; + $suggestions_new[] = 'block__block_content__view_type__' . $bundle . '__' . $view_mode; + + if (!empty($variables['elements']['#id'])) { + $suggestions_new[] = 'block__block_content__id__' . $variables['elements']['#id']; + $suggestions_new[] = 'block__block_content__id_view__' . $variables['elements']['#id'] . '__' . $view_mode; + } + + // Remove duplicate block__block_content. + $suggestions = array_unique($suggestions); + array_splice($suggestions, 1, 0, $suggestions_new); + } + + return $suggestions; +} + +/** + * Implements hook_entity_operation(). + */ +function block_content_entity_operation(EntityInterface $entity): array { + $operations = []; + if ($entity instanceof BlockInterface) { + $plugin = $entity->getPlugin(); + if ($plugin->getBaseId() === 'block_content') { + $custom_block = \Drupal::entityTypeManager()->getStorage('block_content')->loadByProperties([ + 'uuid' => $plugin->getDerivativeId(), + ]); + $custom_block = reset($custom_block); + if ($custom_block && $custom_block->access('update')) { + $operations['block-edit'] = [ + 'title' => t('Edit block'), + 'url' => $custom_block->toUrl('edit-form')->setOptions([]), + 'weight' => 50, + ]; + } + } + } + + return $operations; +} diff --git a/block_content.pages.inc b/block_content.pages.inc new file mode 100644 index 0000000..ff1528c --- /dev/null +++ b/block_content.pages.inc @@ -0,0 +1,37 @@ +query->all(); + foreach ($variables['content'] as $type) { + $variables['types'][$type->id()] = [ + 'link' => Link::fromTextAndUrl($type->label(), Url::fromRoute('block_content.add_form', ['block_content_type' => $type->id()], ['query' => $query]))->toString(), + 'description' => [ + '#markup' => $type->getDescription(), + ], + 'title' => $type->label(), + 'localized_options' => [ + 'query' => $query, + ], + ]; + } +} diff --git a/block_content.permissions.yml b/block_content.permissions.yml new file mode 100644 index 0000000..f558328 --- /dev/null +++ b/block_content.permissions.yml @@ -0,0 +1,16 @@ +permission_callbacks: + - \Drupal\block_content\BlockContentPermissions::blockTypePermissions + +access block library: + title: 'Access the Content blocks overview page' + description: 'Get an overview of all content blocks.' + +administer block types: + title: 'Administer block types' + description: 'Maintain the block types of block content available and the fields that are associated with those types.' + restrict access: TRUE + +administer block content: + title: 'Administer block content' + description: 'View, edit and delete all block content regardless of permission restrictions.' + restrict access: TRUE diff --git a/block_content.post_update.php b/block_content.post_update.php new file mode 100644 index 0000000..3955a70 --- /dev/null +++ b/block_content.post_update.php @@ -0,0 +1,94 @@ + '9.0.0', + ]; +} + +/** + * Clear the entity type cache. + */ +function block_content_post_update_entity_changed_constraint() { + // Empty post_update hook. +} + +/** + * Moves the custom block library to Content. + */ +function block_content_post_update_move_custom_block_library() { + + if (!\Drupal::service('module_handler')->moduleExists('views')) { + return; + } + if (!$view = View::load('block_content')) { + return; + } + + $display =& $view->getDisplay('page_1'); + if (empty($display) || $display['display_options']['path'] !== 'admin/structure/block/block-content') { + return; + } + + $display['display_options']['path'] = 'admin/content/block'; + $menu =& $display['display_options']['menu']; + $menu['title'] = 'Blocks'; + $menu['description'] = 'Create and edit block content.'; + $menu['expanded'] = FALSE; + $menu['parent'] = 'system.admin_content'; + $view->set('label', 'Content blocks'); + + $view->save(); +} + +/** + * Update block_content 'block library' view permission. + */ +function block_content_post_update_block_library_view_permission() { + $config_factory = \Drupal::configFactory(); + $config = $config_factory->getEditable('views.view.block_content'); + $current_perm = $config->get('display.default.display_options.access.options.perm'); + if ($current_perm === 'administer blocks') { + $config->set('display.default.display_options.access.options.perm', 'access block library') + ->save(TRUE); + } +} + +/** + * Update permissions for users with "administer blocks" permission. + */ +function block_content_post_update_sort_permissions(&$sandbox = NULL) { + \Drupal::classResolver(ConfigEntityUpdater::class)->update($sandbox, 'user_role', function (Role $role) { + if ($role->hasPermission('administer blocks')) { + $role->grantPermission('administer block content'); + $role->grantPermission('access block library'); + $role->grantPermission('administer block types'); + return TRUE; + } + return FALSE; + }); +} + +/** + * Update configuration for revision type. + */ +function block_content_post_update_revision_type(&$sandbox = NULL) { + \Drupal::classResolver(ConfigEntityUpdater::class) + ->update($sandbox, 'block_content_type', function (BlockContentTypeInterface $block_content_type) { + $block_content_type->set('revision', (bool) $block_content_type->get('revision')); + return TRUE; + }); +} diff --git a/block_content.routing.yml b/block_content.routing.yml new file mode 100644 index 0000000..1d41aa3 --- /dev/null +++ b/block_content.routing.yml @@ -0,0 +1,132 @@ +block_content.add_page: + path: '/block/add' + defaults: + _controller: '\Drupal\block_content\Controller\BlockContentController::add' + _title: 'Add content block' + options: + _admin_route: TRUE + requirements: + _entity_create_any_access: 'block_content' + +# @todo Deprecate this route once +# https://www.drupal.org/project/drupal/issues/3159210 is fixed, or remove +# it in Drupal 11. +# @see https://www.drupal.org/node/3320855 +entity.block_content_type.collection.bc: + path: '/admin/structure/block/block-content/types' + defaults: + _controller: '\Drupal\block_content\Controller\BlockContentController::blockContentTypeRedirect' + options: + _admin_route: TRUE + requirements: + _permission: 'administer block types' + +# @todo Deprecate this route once +# https://www.drupal.org/project/drupal/issues/3159210 is fixed, or remove +# it in Drupal 11. +# @see https://www.drupal.org/node/3320855 +entity.block_content_type.edit_form.bc: + path: '/admin/structure/block/block-content/manage/{block_content_type}' + defaults: + _controller: '\Drupal\block_content\Controller\BlockContentController::blockContentTypeRedirect' + options: + _admin_route: TRUE + requirements: + _permission: 'administer block types' + +block_content.add_form: + path: '/block/add/{block_content_type}' + defaults: + _controller: '\Drupal\block_content\Controller\BlockContentController::addForm' + _title_callback: '\Drupal\block_content\Controller\BlockContentController::getAddFormTitle' + options: + _admin_route: TRUE + requirements: + _entity_create_access: 'block_content:{block_content_type}' + +entity.block_content.canonical: + path: '/admin/content/block/{block_content}' + defaults: + _entity_form: 'block_content.edit' + _title_callback: '\Drupal\Core\Entity\Controller\EntityController::title' + options: + _admin_route: TRUE + requirements: + _entity_access: 'block_content.update' + block_content: \d+ + +entity.block_content.edit_form: + path: '/admin/content/block/{block_content}' + defaults: + _entity_form: 'block_content.edit' + options: + _admin_route: TRUE + requirements: + _entity_access: 'block_content.update' + block_content: \d+ + +# @todo Deprecate this route once +# https://www.drupal.org/project/drupal/issues/3159210 is fixed, or remove +# it in Drupal 11. +# @see https://www.drupal.org/node/2317981 +entity.block_content.edit_form.bc: + path: '/block/{block_content}' + defaults: + _controller: '\Drupal\block_content\Controller\BlockContentController::editRedirect' + options: + _admin_route: TRUE + requirements: + _entity_access: 'block_content.update' + block_content: \d+ + +entity.block_content.delete_form: + path: '/admin/content/block/{block_content}/delete' + defaults: + _entity_form: 'block_content.delete' + _title: 'Delete' + options: + _admin_route: TRUE + requirements: + _entity_access: 'block_content.delete' + block_content: \d+ + +# @todo Deprecate this route once +# https://www.drupal.org/project/drupal/issues/3159210 is fixed, or remove +# it in Drupal 11. +# @see https://www.drupal.org/node/2317981 +entity.block_content.delete_form.bc: + path: '/block/{block_content}/delete' + defaults: + _controller: '\Drupal\block_content\Controller\BlockContentController::editRedirect' + options: + _admin_route: TRUE + requirements: + _entity_access: 'block_content.delete' + block_content: \d+ + +block_content.type_add: + path: '/admin/structure/block-content/add' + defaults: + _entity_form: 'block_content_type.add' + _title: 'Add' + requirements: + _entity_create_access: 'block_content_type' + +entity.block_content.collection: + path: '/admin/content/block' + defaults: + _title: 'Content blocks' + _entity_list: 'block_content' + requirements: + _permission: 'access block library+administer block content' + +# @todo Deprecate this route once +# https://www.drupal.org/project/drupal/issues/3159210 is fixed, or remove +# it in Drupal 11. +# @see https://www.drupal.org/node/3320855 +entity.block_content.collection.bc: + path: '/admin/structure/block/block-content' + defaults: + _controller: '\Drupal\block_content\Controller\BlockContentController::blockLibraryRedirect' + requirements: + _permission: 'access block library+administer block content' diff --git a/block_content.services.yml b/block_content.services.yml new file mode 100644 index 0000000..ce9c7bd --- /dev/null +++ b/block_content.services.yml @@ -0,0 +1,11 @@ +services: + _defaults: + autoconfigure: true + block_content.uuid_lookup: + class: \Drupal\block_content\BlockContentUuidLookup + arguments: ['@cache.bootstrap', '@lock', '@entity_type.manager'] + tags: + - { name: needs_destruction } + block_content.bc_subscriber: + class: Drupal\block_content\Routing\RouteSubscriber + arguments: ['@entity_type.manager', '@module_handler'] diff --git a/config/install/core.entity_view_mode.block_content.full.yml b/config/install/core.entity_view_mode.block_content.full.yml new file mode 100644 index 0000000..a1d5b09 --- /dev/null +++ b/config/install/core.entity_view_mode.block_content.full.yml @@ -0,0 +1,10 @@ +langcode: en +status: false +dependencies: + module: + - block_content +id: block_content.full +label: Full +description: '' +targetEntityType: block_content +cache: true diff --git a/config/install/field.storage.block_content.body.yml b/config/install/field.storage.block_content.body.yml new file mode 100644 index 0000000..f7c504d --- /dev/null +++ b/config/install/field.storage.block_content.body.yml @@ -0,0 +1,18 @@ +langcode: en +status: true +dependencies: + module: + - block_content + - text +id: block_content.body +field_name: body +entity_type: block_content +type: text_with_summary +settings: { } +module: text +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: true +custom_storage: false diff --git a/config/optional/views.view.block_content.yml b/config/optional/views.view.block_content.yml new file mode 100644 index 0000000..c1f3ea6 --- /dev/null +++ b/config/optional/views.view.block_content.yml @@ -0,0 +1,550 @@ +langcode: en +status: true +dependencies: + module: + - block_content + - user +id: block_content +label: 'Content blocks' +module: views +description: 'Find and manage content blocks.' +tag: default +base_table: block_content_field_data +base_field: id +display: + default: + id: default + display_title: Default + display_plugin: default + position: 0 + display_options: + title: 'Content blocks' + fields: + info: + id: info + table: block_content_field_data + field: info + relationship: none + group_type: group + admin_label: '' + entity_type: null + entity_field: info + plugin_id: field + label: 'Block description' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: + link_to_entity: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + type: + id: type + table: block_content_field_data + field: type + relationship: none + group_type: group + admin_label: '' + entity_type: block_content + entity_field: type + plugin_id: field + label: 'Block type' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: target_id + type: entity_reference_label + settings: + link: false + group_column: target_id + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + changed: + id: changed + table: block_content_field_data + field: changed + relationship: none + group_type: group + admin_label: '' + entity_type: block_content + entity_field: changed + plugin_id: field + label: Updated + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + type: timestamp + settings: + date_format: short + custom_date_format: '' + timezone: '' + tooltip: + date_format: long + custom_date_format: '' + time_diff: + enabled: false + future_format: '@interval hence' + past_format: '@interval ago' + granularity: 2 + refresh: 60 + operations: + id: operations + table: block_content + field: operations + relationship: none + group_type: group + admin_label: '' + entity_type: block_content + plugin_id: entity_operations + label: Operations + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + destination: true + pager: + type: mini + options: + offset: 0 + pagination_heading_level: h4 + items_per_page: 50 + total_pages: null + id: 0 + tags: + next: 'Next ›' + previous: '‹ Previous' + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: true + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + access: + type: perm + options: + perm: 'access block library' + cache: + type: tag + options: { } + empty: + area_text_custom: + id: area_text_custom + table: views + field: area_text_custom + relationship: none + group_type: group + admin_label: '' + plugin_id: text_custom + empty: true + content: 'There are no content blocks available.' + tokenize: false + block_content_listing_empty: + id: block_content_listing_empty + table: block_content + field: block_content_listing_empty + relationship: none + group_type: group + admin_label: '' + entity_type: block_content + plugin_id: block_content_listing_empty + label: '' + empty: true + sorts: { } + arguments: { } + filters: + info: + id: info + table: block_content_field_data + field: info + relationship: none + group_type: group + admin_label: '' + entity_type: block_content + entity_field: info + plugin_id: string + operator: contains + value: '' + group: 1 + exposed: true + expose: + operator_id: info_op + label: 'Block description' + description: '' + use_operator: false + operator: info_op + operator_limit_selection: false + operator_list: { } + identifier: info + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + type: + id: type + table: block_content_field_data + field: type + relationship: none + group_type: group + admin_label: '' + entity_type: block_content + entity_field: type + plugin_id: bundle + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: type_op + label: 'Block type' + description: '' + use_operator: false + operator: type_op + operator_limit_selection: false + operator_list: { } + identifier: type + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + reusable: + id: reusable + table: block_content_field_data + field: reusable + relationship: none + group_type: group + admin_label: '' + entity_type: block_content + entity_field: reusable + plugin_id: boolean + operator: '=' + value: '1' + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + operator_limit_selection: false + operator_list: { } + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + columns: + info: info + type: type + changed: changed + operations: operations + default: changed + info: + info: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + type: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + changed: + sortable: true + default_sort_order: desc + align: '' + separator: '' + empty_column: false + responsive: '' + operations: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + override: true + sticky: false + summary: '' + empty_table: true + caption: '' + description: '' + row: + type: fields + query: + type: views_query + options: + query_comment: '' + disable_sql_rewrite: false + distinct: false + replica: false + query_tags: { } + relationships: { } + header: { } + footer: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } + page_1: + id: page_1 + display_title: Page + display_plugin: page + position: 1 + display_options: + display_extenders: { } + path: admin/content/block + menu: + type: tab + title: 'Blocks' + description: 'Create and edit content blocks.' + weight: 0 + menu_name: admin + parent: system.admin_content + context: '0' + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } diff --git a/config/schema/block_content.schema.yml b/config/schema/block_content.schema.yml new file mode 100644 index 0000000..0e683d8 --- /dev/null +++ b/config/schema/block_content.schema.yml @@ -0,0 +1,24 @@ +# Schema for the configuration files of the Content Block module. + +block_content.type.*: + type: config_entity + label: 'Block type settings' + mapping: + id: + type: machine_name + label: 'ID' + label: + type: required_label + label: 'Label' + revision: + type: boolean + label: 'Whether a new revision should be created by default' + description: + type: text + label: 'Description' + nullable: true + constraints: + NotBlank: + allowNull: true + constraints: + FullyValidatable: ~ diff --git a/help_topics/block_content.add.html.twig b/help_topics/block_content.add.html.twig new file mode 100644 index 0000000..b50f8bb --- /dev/null +++ b/help_topics/block_content.add.html.twig @@ -0,0 +1,20 @@ +--- +label: 'Creating a content block' +related: + - block.overview + - block.configure + - block.place + - block_content.type +--- +{% set library_link_text %}{% trans %}Content blocks{% endtrans %}{% endset %} +{% set library_link = render_var(help_route_link(library_link_text, 'entity.block_content.collection')) %} +

{% trans %}Goal{% endtrans %}

+

{% trans %}Create a content block, which can later be placed on the site.{% endtrans %}

+

{% trans %}Steps{% endtrans %}

+
    +
  1. {% trans %}In the Manage administrative menu, navigate to Content.{% endtrans %}
  2. +
  3. {% trans %}Open the {{ library_link }} tab.{% endtrans %}
  4. +
  5. {% trans %}Click Add content block. If you have more than one block type defined on your site, click the name of the type you want to create.{% endtrans %}
  6. +
  7. {% trans %}Enter a description of your block (to be shown to administrators) and the body text for your block.{% endtrans %}
  8. +
  9. {% trans %}Click Save.{% endtrans %}
  10. +
diff --git a/help_topics/block_content.type.html.twig b/help_topics/block_content.type.html.twig new file mode 100644 index 0000000..66bb9b1 --- /dev/null +++ b/help_topics/block_content.type.html.twig @@ -0,0 +1,24 @@ +--- +label: 'Defining a block type' +related: + - block.overview + - block.configure + - block.place + - block_content.add + - field_ui.add_field + - field_ui.manage_form + - field_ui.manage_display +--- +{% set types_link_text %}{% trans %}Block types{% endtrans %}{% endset %} +{% set types_link = render_var(help_route_link(types_link_text, 'entity.block_content_type.collection')) %} +

{% trans %}Goal{% endtrans %}

+

{% trans %}Define a block type and its fields.{% endtrans %}

+

{% trans %}Steps{% endtrans %}

+
    +
  1. {% trans %}In the Manage administrative menu, navigate to Structure > {{ types_link }}.{% endtrans %}
  2. +
  3. {% trans %}Click Add block type.{% endtrans %}
  4. +
  5. {% trans %}Enter a label for this block type (shown in the administrative interface). Optionally, edit the automatically-generated machine name or the description.{% endtrans %}
  6. +
  7. {% trans %}Click Save. You will be returned to the Block types page.{% endtrans %}
  8. +
  9. {% trans %}Click Manage fields in the row of your new block type, and add the desired fields to your block type.{% endtrans %}
  10. +
  11. {% trans %}Optionally, click Manage form display or Manage display to change the editing form or field display for your block type.{% endtrans %}
  12. +
diff --git a/migrations/block_content_body_field.yml b/migrations/block_content_body_field.yml new file mode 100644 index 0000000..bc30e0f --- /dev/null +++ b/migrations/block_content_body_field.yml @@ -0,0 +1,38 @@ +id: block_content_body_field +label: Block content body field configuration +migration_tags: + - Drupal 6 + - Drupal 7 + - Configuration +source: + plugin: embedded_data + data_rows: + - + entity_type: block_content + bundle: basic + field_name: body + label: Body + display_summary: false + allowed_formats: { } + ids: + entity_type: + type: string + bundle: + type: string + field_name: + type: string + source_module: block +process: + entity_type: entity_type + bundle: bundle + field_name: field_name + label: label + 'settings/display_summary': display_summary +destination: + plugin: entity:field_config +migration_dependencies: + required: + - block_content_type +provider: + - block_content + - migrate_drupal diff --git a/migrations/block_content_entity_display.yml b/migrations/block_content_entity_display.yml new file mode 100644 index 0000000..d30701f --- /dev/null +++ b/migrations/block_content_entity_display.yml @@ -0,0 +1,40 @@ +id: block_content_entity_display +label: Body field display configuration +migration_tags: + - Drupal 6 + - Drupal 7 + - Configuration +source: + plugin: embedded_data + data_rows: + - + entity_type: block_content + bundle: basic + view_mode: default + field_name: body + options: + label: hidden + ids: + entity_type: + type: string + bundle: + type: string + view_mode: + type: string + field_name: + type: string + source_module: block +process: + entity_type: entity_type + bundle: bundle + view_mode: view_mode + field_name: field_name + options: options +destination: + plugin: component_entity_display +migration_dependencies: + required: + - block_content_body_field +provider: + - block_content + - migrate_drupal diff --git a/migrations/block_content_entity_form_display.yml b/migrations/block_content_entity_form_display.yml new file mode 100644 index 0000000..91076d0 --- /dev/null +++ b/migrations/block_content_entity_form_display.yml @@ -0,0 +1,37 @@ +id: block_content_entity_form_display +label: Body field form display configuration +migration_tags: + - Drupal 6 + - Drupal 7 + - Configuration +source: + plugin: embedded_data + data_rows: + - + entity_type: block_content + bundle: basic + form_mode: default + field_name: body + ids: + entity_type: + type: string + bundle: + type: string + form_mode: + type: string + field_name: + type: string + source_module: block +process: + entity_type: entity_type + bundle: bundle + form_mode: form_mode + field_name: field_name +destination: + plugin: component_entity_form_display +migration_dependencies: + required: + - block_content_body_field +provider: + - block_content + - migrate_drupal diff --git a/migrations/block_content_type.yml b/migrations/block_content_type.yml new file mode 100644 index 0000000..55236fb --- /dev/null +++ b/migrations/block_content_type.yml @@ -0,0 +1,24 @@ +id: block_content_type +label: Block content type +migration_tags: + - Drupal 6 + - Drupal 7 + - Configuration +source: + plugin: embedded_data + data_rows: + - + id: basic + label: Basic + ids: + id: + type: string + source_module: block +process: + id: id + label: label +destination: + plugin: entity:block_content_type +provider: + - block_content + - migrate_drupal diff --git a/migrations/d6_custom_block.yml b/migrations/d6_custom_block.yml new file mode 100644 index 0000000..b53d751 --- /dev/null +++ b/migrations/d6_custom_block.yml @@ -0,0 +1,24 @@ +id: d6_custom_block +label: Content blocks +audit: true +migration_tags: + - Drupal 6 + - Content +source: + plugin: d6_box +process: + id: bid + info: info + 'body/format': + plugin: migration_lookup + migration: d6_filter_format + source: format + 'body/value': body +destination: + plugin: entity:block_content + default_bundle: basic + no_stub: true +migration_dependencies: + required: + - d6_filter_format + - block_content_body_field diff --git a/migrations/d7_custom_block.yml b/migrations/d7_custom_block.yml new file mode 100644 index 0000000..fff788c --- /dev/null +++ b/migrations/d7_custom_block.yml @@ -0,0 +1,24 @@ +id: d7_custom_block +label: Content blocks +audit: true +migration_tags: + - Drupal 7 + - Content +source: + plugin: d7_block_custom +process: + id: bid + info: info + 'body/format': + plugin: migration_lookup + migration: d7_filter_format + source: format + 'body/value': body +destination: + plugin: entity:block_content + default_bundle: basic + no_stub: true +migration_dependencies: + required: + - d7_filter_format + - block_content_body_field diff --git a/migrations/state/block_content.migrate_drupal.yml b/migrations/state/block_content.migrate_drupal.yml new file mode 100644 index 0000000..88391ee --- /dev/null +++ b/migrations/state/block_content.migrate_drupal.yml @@ -0,0 +1,5 @@ +finished: + 6: + block: block_content + 7: + block: block_content diff --git a/src/Access/AccessGroupAnd.php b/src/Access/AccessGroupAnd.php new file mode 100644 index 0000000..be86d9e --- /dev/null +++ b/src/Access/AccessGroupAnd.php @@ -0,0 +1,49 @@ +dependencies[] = $dependency; + return $this; + } + + /** + * {@inheritdoc} + */ + public function access($operation, ?AccountInterface $account = NULL, $return_as_object = FALSE) { + $access_result = AccessResult::neutral(); + foreach (array_slice($this->dependencies, 1) as $dependency) { + $access_result = $access_result->andIf($dependency->access($operation, $account, TRUE)); + } + return $return_as_object ? $access_result : $access_result->isAllowed(); + } + + /** + * {@inheritdoc} + */ + public function getDependencies() { + return $this->dependencies; + } + +} diff --git a/src/Access/DependentAccessInterface.php b/src/Access/DependentAccessInterface.php new file mode 100644 index 0000000..bc6a6dc --- /dev/null +++ b/src/Access/DependentAccessInterface.php @@ -0,0 +1,35 @@ +getAccessDependency()->access($op, $account, TRUE); + * @endcode + * + * @internal + */ +interface DependentAccessInterface { + + /** + * Gets the access dependency. + * + * @return \Drupal\Core\Access\AccessibleInterface|null + * The access dependency or NULL if none has been set. + */ + public function getAccessDependency(); + +} diff --git a/src/Access/RefinableDependentAccessInterface.php b/src/Access/RefinableDependentAccessInterface.php new file mode 100644 index 0000000..5d9eaf4 --- /dev/null +++ b/src/Access/RefinableDependentAccessInterface.php @@ -0,0 +1,48 @@ +accessDependency = $access_dependency; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getAccessDependency() { + return $this->accessDependency; + } + + /** + * {@inheritdoc} + */ + public function addAccessDependency(AccessibleInterface $access_dependency) { + if (empty($this->accessDependency)) { + $this->accessDependency = $access_dependency; + return $this; + } + if (!$this->accessDependency instanceof AccessGroupAnd) { + $accessGroup = new AccessGroupAnd(); + $this->accessDependency = $accessGroup->addDependency($this->accessDependency); + } + $this->accessDependency->addDependency($access_dependency); + return $this; + } + +} diff --git a/src/BlockContentAccessControlHandler.php b/src/BlockContentAccessControlHandler.php new file mode 100644 index 0000000..13d4fc1 --- /dev/null +++ b/src/BlockContentAccessControlHandler.php @@ -0,0 +1,116 @@ +eventDispatcher = $dispatcher; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('event_dispatcher') + ); + } + + /** + * {@inheritdoc} + */ + protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { + assert($entity instanceof BlockContentInterface); + $bundle = $entity->bundle(); + $forbidIfNotReusable = fn (): AccessResultInterface => AccessResult::forbiddenIf($entity->isReusable() === FALSE, sprintf('Block content must be reusable to use `%s` operation', $operation)); + $access = AccessResult::allowedIfHasPermissions($account, ['administer block content']); + if (!$access->isAllowed()) { + $access = match ($operation) { + // Allow view and update access to user with the 'edit any (type) block + // content' permission or the 'administer block content' permission. + 'view' => AccessResult::allowedIf($entity->isPublished()) + ->orIf(AccessResult::allowedIfHasPermission($account, 'access block library')), + 'update' => AccessResult::allowedIfHasPermission($account, 'edit any ' . $bundle . ' block content'), + 'delete' => AccessResult::allowedIfHasPermission($account, 'delete any ' . $bundle . ' block content'), + // Revisions. + 'view revision', 'view all revisions' => AccessResult::allowedIfHasPermission($account, 'view any ' . $bundle . ' block content history'), + 'revert' => AccessResult::allowedIfHasPermission($account, 'revert any ' . $bundle . ' block content revisions') + ->orIf($forbidIfNotReusable()), + 'delete revision' => AccessResult::allowedIfHasPermission($account, 'delete any ' . $bundle . ' block content revisions') + ->orIf($forbidIfNotReusable()), + + default => parent::checkAccess($entity, $operation, $account), + }; + } + + // Add the entity as a cacheable dependency because access will at least be + // determined by whether the block is reusable. + $access->addCacheableDependency($entity); + if ($entity->isReusable() === FALSE && $access->isForbidden() !== TRUE) { + if (!$entity instanceof DependentAccessInterface) { + throw new \LogicException("Non-reusable block entities must implement \Drupal\block_content\Access\DependentAccessInterface for access control."); + } + $dependency = $entity->getAccessDependency(); + if (empty($dependency)) { + // If an access dependency has not been set let modules set one. + $event = new BlockContentGetDependencyEvent($entity); + $this->eventDispatcher->dispatch($event, BlockContentEvents::BLOCK_CONTENT_GET_DEPENDENCY); + $dependency = $event->getAccessDependency(); + if (empty($dependency)) { + return AccessResult::forbidden("Non-reusable blocks must set an access dependency for access control."); + } + } + /** @var \Drupal\Core\Entity\EntityInterface $dependency */ + $access = $access->andIf($dependency->access($operation, $account, TRUE)); + } + return $access; + } + + /** + * {@inheritdoc} + */ + protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { + return AccessResult::allowedIfHasPermissions($account, [ + 'create ' . $entity_bundle . ' block content', + 'access block library', + ])->orIf(AccessResult::allowedIfHasPermissions($account, [ + 'administer block content', + ])); + } + +} diff --git a/src/BlockContentEvents.php b/src/BlockContentEvents.php new file mode 100644 index 0000000..85931b7 --- /dev/null +++ b/src/BlockContentEvents.php @@ -0,0 +1,31 @@ +entity; + $form = parent::form($form, $form_state); + + if ($this->operation == 'edit') { + $form['#title'] = $this->t('Edit content block %label', ['%label' => $block->label()]); + } + // Override the default CSS class name, since the user-defined content block + // type name in 'TYPE-block-form' potentially clashes with third-party class + // names. + $form['#attributes']['class'][0] = 'block-' . Html::getClass($block->bundle()) . '-form'; + + return $form; + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state): array { + $element = parent::actions($form, $form_state); + + if ($this->getRequest()->query->has('theme')) { + $element['submit']['#value'] = $this->t('Save and configure'); + } + + if ($this->currentUser()->hasPermission('administer blocks') && !$this->getRequest()->query->has('theme') && $this->entity->isNew()) { + $element['configure_block'] = [ + '#type' => 'submit', + '#value' => $this->t('Save and configure'), + '#weight' => 20, + '#submit' => array_merge($element['submit']['#submit'], ['::configureBlock']), + ]; + } + + return $element; + } + + /** + * Form submission handler for the 'configureBlock' action. + * + * @param array $form + * An associative array containing the structure of the form. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + */ + public function configureBlock(array $form, FormStateInterface $form_state): void { + $block = $this->entity; + if (!$theme = $block->getTheme()) { + $theme = $this->config('system.theme')->get('default'); + } + $form_state->setRedirect( + 'block.admin_add', + [ + 'plugin_id' => 'block_content:' . $block->uuid(), + 'theme' => $theme, + ] + ); + $form_state->setIgnoreDestination(); + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $block = $this->entity; + + $insert = $block->isNew(); + $block->save(); + $context = ['@type' => $block->bundle(), '%info' => $block->label()]; + $logger = $this->logger('block_content'); + $block_type = $this->getBundleEntity(); + $t_args = ['@type' => $block_type->label(), '%info' => $block->label()]; + + if ($insert) { + $logger->info('@type: added %info.', $context); + $this->messenger()->addStatus($this->t('@type %info has been created.', $t_args)); + } + else { + $logger->info('@type: updated %info.', $context); + $this->messenger()->addStatus($this->t('@type %info has been updated.', $t_args)); + } + + if ($block->id()) { + $form_state->setValue('id', $block->id()); + $form_state->set('id', $block->id()); + $theme = $block->getTheme(); + if ($insert && $theme) { + $form_state->setRedirect( + 'block.admin_add', + [ + 'plugin_id' => 'block_content:' . $block->uuid(), + 'theme' => $theme, + 'region' => $this->getRequest()->query->getString('region'), + ] + ); + } + else { + $form_state->setRedirectUrl($block->toUrl('collection')); + } + } + else { + // In the unlikely case something went wrong on save, the block will be + // rebuilt and block form redisplayed. + $this->messenger()->addError($this->t('The block could not be saved.')); + $form_state->setRebuild(); + } + } + +} diff --git a/src/BlockContentInterface.php b/src/BlockContentInterface.php new file mode 100644 index 0000000..f6763f4 --- /dev/null +++ b/src/BlockContentInterface.php @@ -0,0 +1,84 @@ +t('Block description'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row['label'] = $entity->label(); + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + protected function getEntityIds() { + $request = \Drupal::request(); + $query = $this->getStorage()->getQuery()->accessCheck(TRUE); + $foo = $request->get('foo') ?? 0; + if ($foo) { + $query->condition('label', "%" . $foo . "%", 'LIKE'); + } + $query->sort($this->entityType->getKey('id')); + $query->condition('reusable', TRUE); + // Only add the pager if a limit is specified. + if ($this->limit) { + $query->pager($this->limit); + } + return $query->execute(); + } + + + /** + * {@inheritdoc} + */ + public function render() { + $build['form'] = \Drupal::formBuilder()->getForm('Drupal\block_content\Form\BlockContentListFiltersForm'); + $build += parent::render(); + return $build; + } + +} diff --git a/src/BlockContentPermissions.php b/src/BlockContentPermissions.php new file mode 100644 index 0000000..5aae958 --- /dev/null +++ b/src/BlockContentPermissions.php @@ -0,0 +1,84 @@ +get('entity_type.manager'), + ); + } + + /** + * Build permissions for each block type. + * + * @return array + * The block type permissions. + */ + public function blockTypePermissions() { + return $this->generatePermissions($this->entityTypeManager->getStorage('block_content_type')->loadMultiple(), [$this, 'buildPermissions']); + } + + /** + * Return all the permissions available for a block type. + * + * @param \Drupal\block_content\Entity\BlockContentType $type + * The block type. + * + * @return array + * Permissions available for the given block type. + */ + protected function buildPermissions(BlockContentType $type) { + $type_id = $type->id(); + $type_params = ['%type_name' => $type->label()]; + return [ + "create $type_id block content" => [ + 'title' => $this->t('%type_name: Create new content block', $type_params), + ], + "edit any $type_id block content" => [ + 'title' => $this->t('%type_name: Edit content block', $type_params), + ], + "delete any $type_id block content" => [ + 'title' => $this->t('%type_name: Delete content block', $type_params), + ], + "view any $type_id block content history" => [ + 'title' => $this->t('%type_name: View content block history pages', $type_params), + ], + "revert any $type_id block content revisions" => [ + 'title' => $this->t('%type_name: Revert content block revisions', $type_params), + ], + "delete any $type_id block content revisions" => [ + 'title' => $this->t('%type_name: Delete content block revisions', $type_params), + ], + ]; + } + +} diff --git a/src/BlockContentStorageSchema.php b/src/BlockContentStorageSchema.php new file mode 100644 index 0000000..362a158 --- /dev/null +++ b/src/BlockContentStorageSchema.php @@ -0,0 +1,27 @@ +getName(); + + if ($table_name === $this->storage->getDataTable() && $field_name === 'reusable') { + $this->addSharedTableFieldIndex($storage_definition, $schema); + } + + return $schema; + } + +} diff --git a/src/BlockContentTranslationHandler.php b/src/BlockContentTranslationHandler.php new file mode 100644 index 0000000..31e7a51 --- /dev/null +++ b/src/BlockContentTranslationHandler.php @@ -0,0 +1,22 @@ +bundle()); + return $this->t('Edit @type @title', ['@type' => $block_type->label(), '@title' => $entity->label()]); + } + +} diff --git a/src/BlockContentTypeForm.php b/src/BlockContentTypeForm.php new file mode 100644 index 0000000..7d82eee --- /dev/null +++ b/src/BlockContentTypeForm.php @@ -0,0 +1,128 @@ +entity; + + if ($this->operation == 'add') { + $form['#title'] = $this->t('Add block type'); + } + else { + $form['#title'] = $this->t('Edit %label block type', ['%label' => $block_type->label()]); + } + + $form['label'] = [ + '#type' => 'textfield', + '#title' => $this->t('Label'), + '#maxlength' => 255, + '#default_value' => $block_type->label(), + '#description' => $this->t("The human-readable name for this block type, displayed on the Block types page."), + '#required' => TRUE, + ]; + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $block_type->id(), + '#machine_name' => [ + 'exists' => '\Drupal\block_content\Entity\BlockContentType::load', + ], + '#description' => $this->t("Unique machine-readable name: lowercase letters, numbers, and underscores only."), + '#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH, + ]; + + $form['description'] = [ + '#type' => 'textarea', + '#default_value' => $block_type->getDescription(), + '#description' => $this->t('Displays on the Block types page.'), + '#title' => $this->t('Description'), + ]; + + $form['revision'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Create new revision'), + '#default_value' => $block_type->shouldCreateNewRevision(), + '#description' => $this->t('Create a new revision by default for this block type.'), + ]; + + if ($this->moduleHandler->moduleExists('language')) { + $form['language'] = [ + '#type' => 'details', + '#title' => $this->t('Language settings'), + '#group' => 'additional_settings', + ]; + + $language_configuration = ContentLanguageSettings::loadByEntityTypeBundle('block_content', $block_type->id()); + $form['language']['language_configuration'] = [ + '#type' => 'language_configuration', + '#entity_information' => [ + 'entity_type' => 'block_content', + 'bundle' => $block_type->id(), + ], + '#default_value' => $language_configuration, + ]; + + $form['#submit'][] = 'language_configuration_element_submit'; + } + + $form['actions'] = ['#type' => 'actions']; + $form['actions']['submit'] = [ + '#type' => 'submit', + '#value' => $this->t('Save'), + ]; + + return $this->protectBundleIdElement($form); + } + + /** + * {@inheritdoc} + */ + protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) { + // An empty description violates config schema. + if (trim($form_state->getValue('description', '')) === '') { + $form_state->unsetValue('description'); + } + parent::copyFormValuesToEntity($entity, $form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $block_type = $this->entity; + $status = $block_type->save(); + + $edit_link = $this->entity->toLink($this->t('Edit'), 'edit-form')->toString(); + $logger = $this->logger('block_content'); + if ($status == SAVED_UPDATED) { + $this->messenger()->addStatus($this->t('Block type %label has been updated.', ['%label' => $block_type->label()])); + $logger->notice('Block type %label has been updated.', ['%label' => $block_type->label(), 'link' => $edit_link]); + } + else { + block_content_add_body_field($block_type->id()); + $this->messenger()->addStatus($this->t('Block type %label has been added.', ['%label' => $block_type->label()])); + $logger->notice('Block type %label has been added.', ['%label' => $block_type->label(), 'link' => $edit_link]); + } + + $form_state->setRedirectUrl($this->entity->toUrl('collection')); + } + +} diff --git a/src/BlockContentTypeInterface.php b/src/BlockContentTypeInterface.php new file mode 100644 index 0000000..aaf7957 --- /dev/null +++ b/src/BlockContentTypeInterface.php @@ -0,0 +1,21 @@ +t('Block type'); + $header['description'] = $this->t('Description'); + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row['type'] = $entity->toLink(NULL, 'edit-form')->toString(); + $row['description']['data']['#markup'] = $entity->getDescription(); + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + protected function getTitle() { + return $this->t('Block types'); + } + +} diff --git a/src/BlockContentUuidLookup.php b/src/BlockContentUuidLookup.php new file mode 100644 index 0000000..cfca3c9 --- /dev/null +++ b/src/BlockContentUuidLookup.php @@ -0,0 +1,64 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + protected function resolveCacheMiss($key) { + $ids = $this->entityTypeManager->getStorage('block_content')->getQuery() + ->accessCheck(FALSE) + ->condition('uuid', $key) + ->execute(); + + // Only cache if there is a match, otherwise creating new entities would + // require to invalidate the cache. + $id = reset($ids); + if ($id) { + $this->storage[$key] = $id; + $this->persist($key); + } + return $id; + } + +} diff --git a/src/BlockContentViewBuilder.php b/src/BlockContentViewBuilder.php new file mode 100644 index 0000000..68a453b --- /dev/null +++ b/src/BlockContentViewBuilder.php @@ -0,0 +1,50 @@ +viewMultiple([$entity], $view_mode, $langcode)[0]; + } + + /** + * {@inheritdoc} + */ + public function viewMultiple(array $entities = [], $view_mode = 'full', $langcode = NULL) { + $build_list = parent::viewMultiple($entities, $view_mode, $langcode); + // Apply the buildMultiple() #pre_render callback immediately, to make + // bubbling of attributes and contextual links to the actual block work. + // @see \Drupal\block\BlockViewBuilder::buildBlock() + unset($build_list['#pre_render'][0]); + return $this->buildMultiple($build_list); + } + + /** + * {@inheritdoc} + */ + protected function getBuildDefaults(EntityInterface $entity, $view_mode) { + $build = parent::getBuildDefaults($entity, $view_mode); + // The content block will be rendered in the wrapped block template already + // and thus has no entity template itself. + unset($build['#theme']); + return $build; + } + +} diff --git a/src/BlockContentViewsData.php b/src/BlockContentViewsData.php new file mode 100644 index 0000000..06c9de7 --- /dev/null +++ b/src/BlockContentViewsData.php @@ -0,0 +1,42 @@ + $this->t('Empty block library behavior'), + 'help' => $this->t('Provides a link to add a new block.'), + 'area' => [ + 'id' => 'block_content_listing_empty', + ], + ]; + // Advertise this table as a possible base table. + $data['block_content_field_revision']['table']['base']['help'] = $this->t('Block Content revision is a history of changes to block content.'); + $data['block_content_field_revision']['table']['base']['defaults']['title'] = 'info'; + + return $data; + } + +} diff --git a/src/BlockTypeAccessControlHandler.php b/src/BlockTypeAccessControlHandler.php new file mode 100644 index 0000000..60d21a0 --- /dev/null +++ b/src/BlockTypeAccessControlHandler.php @@ -0,0 +1,33 @@ +orIf(parent::checkAccess($entity, $operation, $account)); + } + return parent::checkAccess($entity, $operation, $account); + } + +} diff --git a/src/Controller/BlockContentController.php b/src/Controller/BlockContentController.php new file mode 100644 index 0000000..dbe844d --- /dev/null +++ b/src/Controller/BlockContentController.php @@ -0,0 +1,239 @@ +get('entity_type.manager'); + return new static( + $entity_type_manager->getStorage('block_content'), + $entity_type_manager->getStorage('block_content_type'), + $container->get('theme_handler') + ); + } + + /** + * Constructs a BlockContent object. + * + * @param \Drupal\Core\Entity\EntityStorageInterface $block_content_storage + * The content block storage. + * @param \Drupal\Core\Entity\EntityStorageInterface $block_content_type_storage + * The block type storage. + * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler + * The theme handler. + */ + public function __construct(EntityStorageInterface $block_content_storage, EntityStorageInterface $block_content_type_storage, ThemeHandlerInterface $theme_handler) { + $this->blockContentStorage = $block_content_storage; + $this->blockContentTypeStorage = $block_content_type_storage; + $this->themeHandler = $theme_handler; + } + + /** + * Displays add content block links for available types. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request object. + * + * @return array + * A render array for a list of the block types that can be added or + * if there is only one block type defined for the site, the function + * returns the content block add page for that block type. + */ + public function add(Request $request) { + // @todo deprecate see https://www.drupal.org/project/drupal/issues/3346394. + $types = []; + // Only use block types the user has access to. + foreach ($this->blockContentTypeStorage->loadMultiple() as $type) { + $access = $this->entityTypeManager()->getAccessControlHandler('block_content')->createAccess($type->id(), NULL, [], TRUE); + if ($access->isAllowed()) { + $types[$type->id()] = $type; + } + } + uasort($types, [$this->blockContentTypeStorage->getEntityType()->getClass(), 'sort']); + if ($types && count($types) == 1) { + $type = reset($types); + return $this->addForm($type, $request); + } + if (count($types) === 0) { + return [ + '#markup' => $this->t('You have not created any block types yet. Go to the block type creation page to add a new block type.', [ + ':url' => Url::fromRoute('block_content.type_add')->toString(), + ]), + ]; + } + + return ['#theme' => 'block_content_add_list', '#content' => $types]; + } + + /** + * Presents the content block creation form. + * + * @param \Drupal\block_content\BlockContentTypeInterface $block_content_type + * The block type to add. + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request object. + * + * @return array + * A form array as expected by + * \Drupal\Core\Render\RendererInterface::render(). + */ + public function addForm(BlockContentTypeInterface $block_content_type, Request $request) { + $block = $this->blockContentStorage->create([ + 'type' => $block_content_type->id(), + ]); + if (($theme = $request->query->get('theme')) && in_array($theme, array_keys($this->themeHandler->listInfo()))) { + // We have navigated to this page from the block library and will keep track + // of the theme for redirecting the user to the configuration page for the + // newly created block in the given theme. + $block->setTheme($theme); + } + return $this->entityFormBuilder()->getForm($block); + } + + /** + * Provides the page title for this controller. + * + * @param \Drupal\block_content\BlockContentTypeInterface $block_content_type + * The block type being added. + * + * @return string + * The page title. + */ + public function getAddFormTitle(BlockContentTypeInterface $block_content_type) { + return $this->t('Add %type content block', ['%type' => $block_content_type->label()]); + } + + /** + * Provides a redirect to the list of block types. + * + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * A route match object, used for the route name and the parameters. + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request object. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * + * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use + * /admin/structure/block-content directly instead of + * /admin/structure/block/block-content/types. + * + * @see https://www.drupal.org/node/3320855 + */ + public function blockContentTypeRedirect(RouteMatchInterface $route_match, Request $request): RedirectResponse { + @trigger_error('The path /admin/structure/block/block-content/types is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use /admin/structure/block-content. See https://www.drupal.org/node/3320855', E_USER_DEPRECATED); + $helper = new PathChangedHelper($route_match, $request); + $params = [ + '%old_path' => $helper->oldPath(), + '%new_path' => $helper->newPath(), + '%change_record' => 'https://www.drupal.org/node/3320855', + ]; + $warning_message = $this->t('You have been redirected from %old_path. Update links, shortcuts, and bookmarks to use %new_path.', $params); + $this->messenger()->addWarning($warning_message); + $this->getLogger('block_content')->warning('A user was redirected from %old_path. This redirect will be removed in a future version of Drupal. Update links, shortcuts, and bookmarks to use %new_path. See %change_record for more information.', $params); + + return $helper->redirect(); + } + + /** + * Provides a redirect to the content block library. + * + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * A route match object, used for the route name and the parameters. + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request object. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * + * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use + * /admin/content/block directly instead of + * /admin/structure/block/block-content. + * + * @see https://www.drupal.org/node/3320855 + */ + public function blockLibraryRedirect(RouteMatchInterface $route_match, Request $request) { + @trigger_error('The path /admin/structure/block/block-content is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use /admin/content/block. See https://www.drupal.org/node/3320855', E_USER_DEPRECATED); + $helper = new PathChangedHelper($route_match, $request); + $params = [ + '%old_path' => $helper->oldPath(), + '%new_path' => $helper->newPath(), + '%change_record' => 'https://www.drupal.org/node/3320855', + ]; + $warning_message = $this->t('You have been redirected from %old_path. Update links, shortcuts, and bookmarks to use %new_path.', $params); + $this->messenger()->addWarning($warning_message); + $this->getLogger('block_content') + ->warning('A user was redirected from %old_path. This redirect will be removed in a future version of Drupal. Update links, shortcuts, and bookmarks to use %new_path. See %change_record for more information.', $params); + + return $helper->redirect(); + } + + /** + * Provides a redirect to block edit page. + * + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * A route match object, used for the route name and the parameters. + * @param \Symfony\Component\HttpFoundation\Request $request + * The current request object. + * @param Drupal\block_content\BlockContentInterface $block_content + * The block to be edited. + * + * @return \Symfony\Component\HttpFoundation\RedirectResponse + * + * @deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use + * /admin/content/block/{block_content} directly instead of + * /block/{block_content}. + * + * @see https://www.drupal.org/node/3320855 + */ + public function editRedirect(RouteMatchInterface $route_match, Request $request, BlockContentInterface $block_content): RedirectResponse { + @trigger_error('The path /block/{block_content} is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use /admin/content/block/{block_content}. See https://www.drupal.org/node/3320855', E_USER_DEPRECATED); + $helper = new PathChangedHelper($route_match, $request); + $params = [ + '%old_path' => $helper->oldPath(), + '%new_path' => $helper->newPath(), + '%change_record' => 'https://www.drupal.org/node/3320855', + ]; + $warning_message = $this->t('You have been redirected from %old_path. Update links, shortcuts, and bookmarks to use %new_path.', $params); + $this->messenger()->addWarning($warning_message); + $this->getLogger('block_content')->warning('A user was redirected from %old_path to %new_path. This redirect will be removed in a future version of Drupal. Update links, shortcuts, and bookmarks to use %new_path. See %change_record for more information.', $params); + + return $helper->redirect(); + } + +} diff --git a/src/Entity/BlockContent.php b/src/Entity/BlockContent.php new file mode 100644 index 0000000..81e6bfc --- /dev/null +++ b/src/Entity/BlockContent.php @@ -0,0 +1,274 @@ +revision_id->value = NULL; + $duplicate->id->value = NULL; + return $duplicate; + } + + /** + * {@inheritdoc} + */ + public function setTheme($theme) { + $this->theme = $theme; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getTheme() { + return $this->theme; + } + + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + if ($this->isReusable() || (isset($this->original) && $this->original->isReusable())) { + static::invalidateBlockPluginCache(); + } + } + + /** + * {@inheritdoc} + */ + public static function preDelete(EntityStorageInterface $storage, array $entities) { + parent::preDelete($storage, $entities); + + /** @var \Drupal\block_content\BlockContentInterface $block */ + foreach ($entities as $block) { + foreach ($block->getInstances() as $instance) { + $instance->delete(); + } + } + } + + /** + * {@inheritdoc} + */ + public static function postDelete(EntityStorageInterface $storage, array $entities) { + parent::postDelete($storage, $entities); + /** @var \Drupal\block_content\BlockContentInterface $block */ + foreach ($entities as $block) { + if ($block->isReusable()) { + // If any deleted blocks are reusable clear the block cache. + static::invalidateBlockPluginCache(); + return; + } + } + } + + /** + * {@inheritdoc} + */ + public function getInstances() { + return \Drupal::entityTypeManager()->getStorage('block')->loadByProperties(['plugin' => 'block_content:' . $this->uuid()]); + } + + /** + * {@inheritdoc} + */ + public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) { + parent::preSaveRevision($storage, $record); + + if (!$this->isNewRevision() && isset($this->original) && empty($record->revision_log_message)) { + // If we are updating an existing block_content without adding a new + // revision and the user did not supply a revision log, keep the existing + // one. + $record->revision_log = $this->original->getRevisionLogMessage(); + } + } + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + /** @var \Drupal\Core\Field\BaseFieldDefinition[] $fields */ + $fields = parent::baseFieldDefinitions($entity_type); + + $fields['id']->setLabel(t('Content block ID')) + ->setDescription(t('The content block ID.')); + + $fields['uuid']->setDescription(t('The content block UUID.')); + + $fields['revision_id']->setDescription(t('The revision ID.')); + + $fields['langcode']->setDescription(t('The content block language code.')); + + $fields['type']->setLabel(t('Block type')) + ->setDescription(t('The block type.')); + + $fields['revision_log']->setDescription(t('The log entry explaining the changes in this revision.')); + + $fields['info'] = BaseFieldDefinition::create('string') + ->setLabel(t('Block description')) + ->setDescription(t('A brief description of your block.')) + ->setRevisionable(TRUE) + ->setTranslatable(TRUE) + ->setRequired(TRUE) + ->setDisplayOptions('form', [ + 'type' => 'string_textfield', + 'weight' => -5, + ]) + ->setDisplayConfigurable('form', TRUE); + + $fields['changed'] = BaseFieldDefinition::create('changed') + ->setLabel(t('Changed')) + ->setDescription(t('The time that the content block was last edited.')) + ->setTranslatable(TRUE) + ->setRevisionable(TRUE); + + $fields['reusable'] = BaseFieldDefinition::create('boolean') + ->setLabel(t('Reusable')) + ->setDescription(t('A boolean indicating whether this block is reusable.')) + ->setTranslatable(FALSE) + ->setRevisionable(FALSE) + ->setDefaultValue(TRUE); + + return $fields; + } + + /** + * {@inheritdoc} + */ + public function setInfo($info) { + $this->set('info', $info); + return $this; + } + + /** + * {@inheritdoc} + */ + public function isReusable() { + return (bool) $this->get('reusable')->value; + } + + /** + * {@inheritdoc} + */ + public function setReusable() { + return $this->set('reusable', TRUE); + } + + /** + * {@inheritdoc} + */ + public function setNonReusable() { + return $this->set('reusable', FALSE); + } + + /** + * Invalidates the block plugin cache after changes and deletions. + */ + protected static function invalidateBlockPluginCache() { + // Invalidate the block cache to update content block-based derivatives. + \Drupal::service('plugin.manager.block')->clearCachedDefinitions(); + } + +} diff --git a/src/Entity/BlockContentType.php b/src/Entity/BlockContentType.php new file mode 100644 index 0000000..c2f22e7 --- /dev/null +++ b/src/Entity/BlockContentType.php @@ -0,0 +1,100 @@ +description ?? ''; + } + + /** + * {@inheritdoc} + */ + public function shouldCreateNewRevision() { + return $this->revision; + } + +} diff --git a/src/Event/BlockContentGetDependencyEvent.php b/src/Event/BlockContentGetDependencyEvent.php new file mode 100644 index 0000000..423d89f --- /dev/null +++ b/src/Event/BlockContentGetDependencyEvent.php @@ -0,0 +1,70 @@ +blockContent = $blockContent; + } + + /** + * Gets the block content entity. + * + * @return \Drupal\block_content\BlockContentInterface + * The block content entity. + */ + public function getBlockContentEntity() { + return $this->blockContent; + } + + /** + * Gets the access dependency. + * + * @return \Drupal\Core\Access\AccessibleInterface + * The access dependency. + */ + public function getAccessDependency() { + return $this->accessDependency; + } + + /** + * Sets the access dependency. + * + * @param \Drupal\Core\Access\AccessibleInterface $access_dependency + * The access dependency. + */ + public function setAccessDependency(AccessibleInterface $access_dependency) { + $this->accessDependency = $access_dependency; + } + +} diff --git a/src/Form/BlockContentDeleteForm.php b/src/Form/BlockContentDeleteForm.php new file mode 100644 index 0000000..41cdba5 --- /dev/null +++ b/src/Form/BlockContentDeleteForm.php @@ -0,0 +1,25 @@ +entity->getInstances(); + if (!empty($instances)) { + return $this->formatPlural(count($instances), 'This will also remove 1 placed block instance. This action cannot be undone.', 'This will also remove @count placed block instances. This action cannot be undone.'); + } + return parent::getDescription(); + } + +} diff --git a/src/Form/BlockContentListFiltersForm.php b/src/Form/BlockContentListFiltersForm.php new file mode 100644 index 0000000..ad1ab10 --- /dev/null +++ b/src/Form/BlockContentListFiltersForm.php @@ -0,0 +1,80 @@ + 'container', + '#attributes' => [ + 'class' => ['form--inline', 'clearfix'], + ], + ]; + + $form['filter']['foo'] = [ + '#type' => 'textfield', + '#title' => 'Search for block', + '#default_value' => $request->get('foo') ?? "", + ]; + + $form['actions']['wrapper'] = [ + '#type' => 'container', + '#attributes' => ['class' => ['form-item']], + ]; + + $form['actions']['wrapper']['submit'] = [ + '#type' => 'submit', + '#value' => 'Filter', + ]; + + if ($request->getQueryString()) { + $form['actions']['wrapper']['reset'] = [ + '#type' => 'submit', + '#value' => 'Reset', + '#submit' => ['::resetForm'], + ]; + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $query = []; + + $foo = $form_state->getValue('foo') ?? 0; + if ($foo) { + $query['foo'] = $foo; + } + + $form_state->setRedirect('entity.block_content.collection', $query); + } + + /** + * {@inheritdoc} + */ + public function resetForm(array $form, FormStateInterface &$form_state) { + $form_state->setRedirect('entity.block_content.collection'); + } + +} diff --git a/src/Form/BlockContentTypeDeleteForm.php b/src/Form/BlockContentTypeDeleteForm.php new file mode 100644 index 0000000..4f69022 --- /dev/null +++ b/src/Form/BlockContentTypeDeleteForm.php @@ -0,0 +1,34 @@ +entityTypeManager->getStorage('block_content')->getQuery() + ->accessCheck(FALSE) + ->condition('type', $this->entity->id()) + ->count() + ->execute(); + if ($block_count) { + $caption = '

' . $this->formatPlural($block_count, '%label is used by 1 content block on your site. You can not remove this block type until you have removed all of the %label blocks.', '%label is used by @count content blocks on your site. You may not remove %label until you have removed all of the %label content blocks.', ['%label' => $this->entity->label()]) . '

'; + $form['description'] = ['#markup' => $caption]; + return $form; + } + else { + return parent::buildForm($form, $form_state); + } + } + +} diff --git a/src/Plugin/Block/BlockContentBlock.php b/src/Plugin/Block/BlockContentBlock.php new file mode 100644 index 0000000..7c862f4 --- /dev/null +++ b/src/Plugin/Block/BlockContentBlock.php @@ -0,0 +1,218 @@ +blockManager = $block_manager; + $this->entityTypeManager = $entity_type_manager; + $this->account = $account; + $this->urlGenerator = $url_generator; + $this->uuidLookup = $uuid_lookup; + $this->entityDisplayRepository = $entity_display_repository; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('plugin.manager.block'), + $container->get('entity_type.manager'), + $container->get('current_user'), + $container->get('url_generator'), + $container->get('block_content.uuid_lookup'), + $container->get('entity_display.repository') + ); + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'status' => TRUE, + 'info' => '', + 'view_mode' => 'full', + ]; + } + + /** + * Overrides \Drupal\Core\Block\BlockBase::blockForm(). + * + * Adds body and description fields to the block configuration form. + */ + public function blockForm($form, FormStateInterface $form_state) { + $block = $this->getEntity(); + if (!$block) { + return $form; + } + $options = $this->entityDisplayRepository->getViewModeOptionsByBundle('block_content', $block->bundle()); + + $form['view_mode'] = [ + '#type' => 'select', + '#options' => $options, + '#title' => $this->t('View mode'), + '#description' => $this->t('Output the block in this view mode.'), + '#default_value' => $this->configuration['view_mode'], + '#access' => (count($options) > 1), + ]; + $form['title']['#description'] = $this->t('The title of the block as shown to the user.'); + return $form; + } + + /** + * {@inheritdoc} + */ + public function blockSubmit($form, FormStateInterface $form_state) { + // Invalidate the block cache to update content block-based derivatives. + $this->configuration['view_mode'] = $form_state->getValue('view_mode'); + $this->blockManager->clearCachedDefinitions(); + } + + /** + * {@inheritdoc} + */ + protected function blockAccess(AccountInterface $account) { + if ($this->getEntity()) { + return $this->getEntity()->access('view', $account, TRUE); + } + return AccessResult::forbidden(); + } + + /** + * {@inheritdoc} + */ + public function build() { + if ($block = $this->getEntity()) { + return $this->entityTypeManager->getViewBuilder($block->getEntityTypeId())->view($block, $this->configuration['view_mode']); + } + else { + return [ + '#markup' => $this->t('Block with uuid %uuid does not exist. Add content block.', [ + '%uuid' => $this->getDerivativeId(), + ':url' => $this->urlGenerator->generate('block_content.add_page'), + ]), + '#access' => $this->account->hasPermission('administer blocks'), + ]; + } + } + + /** + * Loads the block content entity of the block. + * + * @return \Drupal\block_content\BlockContentInterface|null + * The block content entity. + */ + protected function getEntity() { + if (!isset($this->blockContent)) { + $uuid = $this->getDerivativeId(); + if ($id = $this->uuidLookup->get($uuid)) { + $this->blockContent = $this->entityTypeManager->getStorage('block_content')->load($id); + } + } + return $this->blockContent; + } + +} diff --git a/src/Plugin/Derivative/BlockContent.php b/src/Plugin/Derivative/BlockContent.php new file mode 100644 index 0000000..472f041 --- /dev/null +++ b/src/Plugin/Derivative/BlockContent.php @@ -0,0 +1,60 @@ +blockContentStorage = $block_content_storage; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + $entity_type_manager = $container->get('entity_type.manager'); + return new static( + $entity_type_manager->getStorage('block_content') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $block_contents = $this->blockContentStorage->loadByProperties(['reusable' => TRUE]); + // Reset the discovered definitions. + $this->derivatives = []; + /** @var \Drupal\block_content\Entity\BlockContent $block_content */ + foreach ($block_contents as $block_content) { + $this->derivatives[$block_content->uuid()] = $base_plugin_definition; + $this->derivatives[$block_content->uuid()]['admin_label'] = $block_content->label() ?? ($block_content->type->entity->label() . ': ' . $block_content->id()); + $this->derivatives[$block_content->uuid()]['config_dependencies']['content'] = [ + $block_content->getConfigDependencyName(), + ]; + } + return parent::getDerivativeDefinitions($base_plugin_definition); + } + +} diff --git a/src/Plugin/Menu/LocalAction/BlockContentAddLocalAction.php b/src/Plugin/Menu/LocalAction/BlockContentAddLocalAction.php new file mode 100644 index 0000000..844e068 --- /dev/null +++ b/src/Plugin/Menu/LocalAction/BlockContentAddLocalAction.php @@ -0,0 +1,65 @@ +get('router.route_provider'), + $container->get('request_stack'), + ); + } + + /** + * {@inheritdoc} + */ + public function getOptions(RouteMatchInterface $route_match) { + $options = parent::getOptions($route_match); + // If the route specifies a theme, append it to the query string. + if ($theme = $route_match->getParameter('theme')) { + $options['query']['theme'] = $theme; + } + + // If the current request has a region, append it to the query string. + if ($region = $this->requestStack->getCurrentRequest()->query->getString('region')) { + $options['query']['region'] = $region; + } + + // Adds a destination on content block listing. + if ($route_match->getRouteName() == 'entity.block_content.collection') { + $options['query']['destination'] = Url::fromRoute('')->toString(); + } + return $options; + } + +} diff --git a/src/Plugin/Validation/Constraint/BlockContentEntityChangedConstraint.php b/src/Plugin/Validation/Constraint/BlockContentEntityChangedConstraint.php new file mode 100644 index 0000000..bc1d8aa --- /dev/null +++ b/src/Plugin/Validation/Constraint/BlockContentEntityChangedConstraint.php @@ -0,0 +1,18 @@ + 'Validation']), + type: ['entity'] +)] +class BlockContentEntityChangedConstraint extends EntityChangedConstraint { +} diff --git a/src/Plugin/Validation/Constraint/BlockContentEntityChangedConstraintValidator.php b/src/Plugin/Validation/Constraint/BlockContentEntityChangedConstraintValidator.php new file mode 100644 index 0000000..b044e1d --- /dev/null +++ b/src/Plugin/Validation/Constraint/BlockContentEntityChangedConstraintValidator.php @@ -0,0 +1,30 @@ +isReusable()) { + return; + } + parent::validate($entity, $constraint); + } + +} diff --git a/src/Plugin/migrate/source/d6/Box.php b/src/Plugin/migrate/source/d6/Box.php new file mode 100644 index 0000000..fcb8be0 --- /dev/null +++ b/src/Plugin/migrate/source/d6/Box.php @@ -0,0 +1,53 @@ +select('boxes', 'b') + ->fields('b', ['bid', 'body', 'info', 'format']); + $query->orderBy('b.bid'); + + return $query; + } + + /** + * {@inheritdoc} + */ + public function fields() { + return [ + 'bid' => $this->t('The numeric identifier of the block/box'), + 'body' => $this->t('The block/box content'), + 'info' => $this->t('Admin title of the block/box.'), + 'format' => $this->t('Input format of the content block/box content.'), + ]; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + $ids['bid']['type'] = 'integer'; + return $ids; + } + +} diff --git a/src/Plugin/migrate/source/d6/BoxTranslation.php b/src/Plugin/migrate/source/d6/BoxTranslation.php new file mode 100644 index 0000000..c017fe3 --- /dev/null +++ b/src/Plugin/migrate/source/d6/BoxTranslation.php @@ -0,0 +1,28 @@ +select('block_custom', 'b')->fields('b'); + } + + /** + * {@inheritdoc} + */ + public function fields() { + return [ + 'bid' => $this->t('The numeric identifier of the block/box'), + 'body' => $this->t('The block/box content'), + 'info' => $this->t('Admin title of the block/box.'), + 'format' => $this->t('Input format of the content block/box content.'), + ]; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + $ids['bid']['type'] = 'integer'; + return $ids; + } + +} diff --git a/src/Plugin/migrate/source/d7/BlockCustomTranslation.php b/src/Plugin/migrate/source/d7/BlockCustomTranslation.php new file mode 100644 index 0000000..ce592d2 --- /dev/null +++ b/src/Plugin/migrate/source/d7/BlockCustomTranslation.php @@ -0,0 +1,105 @@ +select(static::CUSTOM_BLOCK_TABLE, 'b') + ->fields('b', ['bid', 'format', 'body']) + ->fields('i18n', ['property']) + ->fields('lt', ['lid', 'translation', 'language']) + ->orderBy('b.bid'); + + // Use 'title' for the info field to match the property name in + // i18nStringTable. + $query->addField('b', 'info', 'title'); + + // Add in the property, which is either title or body. Cast the bid to text + // so PostgreSQL can make the join. + $query->leftJoin(static::I18N_STRING_TABLE, 'i18n', '[i18n].[objectid] = CAST([b].[bid] AS CHAR(255))'); + $query->condition('i18n.type', 'block'); + + // Add in the translation for the property. + $query->innerJoin('locales_target', 'lt', '[lt].[lid] = [i18n].[lid]'); + return $query; + } + + /** + * {@inheritdoc} + */ + public function prepareRow(Row $row) { + if (!parent::prepareRow($row)) { + return FALSE; + } + // Set the i18n string table for use in I18nQueryTrait. + $this->i18nStringTable = static::I18N_STRING_TABLE; + // Save the translation for this property. + $property_in_row = $row->getSourceProperty('property'); + // Get the translation for the property not already in the row and save it + // in the row. + $property_not_in_row = ($property_in_row === 'title') ? 'body' : 'title'; + return $this->getPropertyNotInRowTranslation($row, $property_not_in_row, 'bid', $this->idMap); + } + + /** + * {@inheritdoc} + */ + public function fields() { + return [ + 'bid' => $this->t('The block numeric identifier.'), + 'format' => $this->t('Input format of the content block/box content.'), + 'lid' => $this->t('i18n_string table id'), + 'language' => $this->t('Language for this field.'), + 'property' => $this->t('Block property'), + 'translation' => $this->t('The translation of the value of "property".'), + 'title' => $this->t('Block title.'), + 'title_translated' => $this->t('Block title translation.'), + 'body' => $this->t('Block body.'), + 'body_translated' => $this->t('Block body translation.'), + ]; + } + + /** + * {@inheritdoc} + */ + public function getIds() { + $ids['bid']['type'] = 'integer'; + $ids['bid']['alias'] = 'b'; + $ids['language']['type'] = 'string'; + return $ids; + } + +} diff --git a/src/Plugin/views/area/ListingEmpty.php b/src/Plugin/views/area/ListingEmpty.php new file mode 100644 index 0000000..b4ee473 --- /dev/null +++ b/src/Plugin/views/area/ListingEmpty.php @@ -0,0 +1,89 @@ +accessManager = $access_manager; + $this->currentUser = $current_user; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('access_manager'), + $container->get('current_user') + ); + } + + /** + * {@inheritdoc} + */ + public function render($empty = FALSE) { + if (!$empty || !empty($this->options['empty'])) { + /** @var \Drupal\Core\Access\AccessResultInterface|\Drupal\Core\Cache\CacheableDependencyInterface $access_result */ + $access_result = $this->accessManager->checkNamedRoute('block_content.add_page', [], $this->currentUser, TRUE); + $element = [ + '#markup' => $this->t('Add a content block.', [':url' => Url::fromRoute('block_content.add_page')->toString()]), + '#access' => $access_result->isAllowed(), + '#cache' => [ + 'contexts' => $access_result->getCacheContexts(), + 'tags' => $access_result->getCacheTags(), + 'max-age' => $access_result->getCacheMaxAge(), + ], + ]; + return $element; + } + return []; + } + +} diff --git a/src/Plugin/views/wizard/BlockContent.php b/src/Plugin/views/wizard/BlockContent.php new file mode 100644 index 0000000..eb2de7e --- /dev/null +++ b/src/Plugin/views/wizard/BlockContent.php @@ -0,0 +1,36 @@ + 'reusable', + 'plugin_id' => 'boolean', + 'table' => $this->base_table, + 'field' => 'reusable', + 'value' => '1', + 'entity_type' => $this->entityTypeId, + 'entity_field' => 'reusable', + ]; + return $filters; + } + +} diff --git a/src/Routing/RouteSubscriber.php b/src/Routing/RouteSubscriber.php new file mode 100644 index 0000000..f8ae019 --- /dev/null +++ b/src/Routing/RouteSubscriber.php @@ -0,0 +1,237 @@ +entityTypeManager = $entity_type_manager; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} + */ + protected function alterRoutes(RouteCollection $collection) { + $this->collection = $collection; + + // @see block_content.routing.yml + if ($this->setUpBaseRoute('entity.block_content_type.collection')) { + $this->addRedirectRoute('block_content.type_add'); + } + + $entity_type = $this->entityTypeManager->getDefinition('block_content'); + if ($this->setUpBaseRoute($entity_type->get('field_ui_base_route'))) { + foreach ($this->childRoutes($entity_type) as $route_name) { + $this->addRedirectRoute($route_name); + } + } + } + + /** + * Gets parameters from a base route and saves them in class variables. + * + * @param string $base_route_name + * The name of a base route that already has a BC variant. + * + * @return bool + * TRUE if all parameters are set, FALSE if not. + */ + protected function setUpBaseRoute(string $base_route_name): bool { + $base_route = $this->collection->get($base_route_name); + $base_route_bc = $this->collection->get("$base_route_name.bc"); + if (empty($base_route) || empty($base_route_bc)) { + return FALSE; + } + + $this->basePath = $base_route->getPath(); + $this->basePathBc = $base_route_bc->getPath(); + $this->controller = $base_route_bc->getDefault('_controller'); + if (empty($this->basePath) || empty($this->basePathBc) || empty($this->controller) || $this->basePathBc === $this->basePath) { + return FALSE; + } + + return TRUE; + } + + /** + * Adds a redirect route. + * + * @param string $route_name + * The name of a route whose path has changed. + */ + protected function addRedirectRoute(string $route_name): void { + // Exit early if the BC route is already there. + if (!empty($this->collection->get("$route_name.bc"))) { + return; + } + + $route = $this->collection->get($route_name); + if (empty($route)) { + return; + } + + $new_path = $route->getPath(); + if (!str_starts_with($new_path, $this->basePath)) { + return; + } + + $bc_route = clone $route; + // Set the path to what it was in earlier versions of Drupal. + $bc_route->setPath($this->basePathBc . substr($new_path, strlen($this->basePath))); + if ($bc_route->getPath() === $route->getPath()) { + return; + } + + // Replace the handler with the stored redirect controller. + $defaults = array_diff_key($route->getDefaults(), array_flip([ + '_entity_form', + '_entity_list', + '_entity_view', + '_form', + ])); + $defaults['_controller'] = $this->controller; + $bc_route->setDefaults($defaults); + + $this->collection->add("$route_name.bc", $bc_route); + } + + /** + * Creates a list of routes that need BC redirects. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type. + * + * @return string[] + * A list of route names. + */ + protected function childRoutes(EntityTypeInterface $entity_type): array { + $route_names = []; + + if ($field_ui_base_route = $entity_type->get('field_ui_base_route')) { + $updated_routes = new RouteCollection(); + $updated_routes->add($field_ui_base_route, $this->collection->get($field_ui_base_route)); + $event = new RouteBuildEvent($updated_routes); + + // Apply route subscribers that add routes based on field_ui_base_route, + // in the order of their weights. + $subscribers = [ + 'field_ui' => 'field_ui.subscriber', + 'content_translation' => 'content_translation.subscriber', + ]; + foreach ($subscribers as $module_name => $service_name) { + if ($this->moduleHandler->moduleExists($module_name)) { + \Drupal::service($service_name)->onAlterRoutes($event); + } + } + + $updated_routes->remove($field_ui_base_route); + $route_names = array_merge($route_names, array_keys($updated_routes->all())); + $route_names = array_merge($route_names, [ + // @see \Drupal\config_translation\Routing\RouteSubscriber::alterRoutes() + "config_translation.item.add.{$field_ui_base_route}", + "config_translation.item.edit.{$field_ui_base_route}", + "config_translation.item.delete.{$field_ui_base_route}", + ]); + } + + if ($entity_type_id = $entity_type->getBundleEntityType()) { + $route_names = array_merge($route_names, [ + // @see \Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider::getRoutes() + "entity.{$entity_type_id}.delete_form", + // @see \Drupal\config_translation\Routing\RouteSubscriber::alterRoutes() + "entity.{$entity_type_id}.config_translation_overview", + // @see \Drupal\user\Entity\EntityPermissionsRouteProvider::getRoutes() + "entity.{$entity_type_id}.entity_permissions_form", + ]); + } + + if ($entity_id = $entity_type->id()) { + $route_names = array_merge($route_names, [ + // @see \Drupal\config_translation\Routing\RouteSubscriber::alterRoutes() + "entity.field_config.config_translation_overview.{$entity_id}", + "config_translation.item.add.entity.field_config.{$entity_id}_field_edit_form", + "config_translation.item.edit.entity.field_config.{$entity_id}_field_edit_form", + "config_translation.item.delete.entity.field_config.{$entity_id}_field_edit_form", + // @see \Drupal\layout_builder\Plugin\SectionStorage\DefaultsSectionStorage::buildRoutes() + "layout_builder.defaults.{$entity_id}.disable", + "layout_builder.defaults.{$entity_id}.discard_changes", + "layout_builder.defaults.{$entity_id}.view", + ]); + } + + return $route_names; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents(): array { + $events = parent::getSubscribedEvents(); + // Go after ContentTranslationRouteSubscriber. + $events[RoutingEvents::ALTER] = ['onAlterRoutes', -300]; + return $events; + } + +} diff --git a/templates/block-content-add-list.html.twig b/templates/block-content-add-list.html.twig new file mode 100644 index 0000000..a512228 --- /dev/null +++ b/templates/block-content-add-list.html.twig @@ -0,0 +1,24 @@ +{# +/** + * @file + * Default theme implementation to present a list of block types. + * + * Available variables: + * - types: A collection of all the available content block types. + * Each block type contains the following: + * - link: A link to add a block of this type. + * - description: A description of this block type. + * + * @see template_preprocess_block_content_add_list() + * + * @ingroup themeable + */ +#} +{% apply spaceless %} +
+ {% for type in types %} +
{{ type.link }}
+
{{ type.description }}
+ {% endfor %} +
+{% endapply %} diff --git a/tests/fixtures/update/views.view.block_content_2862564.yml b/tests/fixtures/update/views.view.block_content_2862564.yml new file mode 100644 index 0000000..9ec2c50 --- /dev/null +++ b/tests/fixtures/update/views.view.block_content_2862564.yml @@ -0,0 +1,540 @@ +langcode: en +status: true +dependencies: + module: + - block_content + - user +id: block_content +label: 'Content blocks' +module: views +description: 'Create and edit content blocks.' +tag: default +base_table: block_content_field_data +base_field: id +display: + default: + id: default + display_title: Default + display_plugin: default + position: 0 + display_options: + title: 'Content blocks' + fields: + info: + id: info + table: block_content_field_data + field: info + relationship: none + group_type: group + admin_label: '' + entity_type: null + entity_field: info + plugin_id: field + label: 'Block description' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: + link_to_entity: true + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + type: + id: type + table: block_content_field_data + field: type + relationship: none + group_type: group + admin_label: '' + entity_type: block_content + entity_field: type + plugin_id: field + label: 'Block type' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: target_id + type: entity_reference_label + settings: + link: false + group_column: target_id + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + changed: + id: changed + table: block_content_field_data + field: changed + relationship: none + group_type: group + admin_label: '' + entity_type: block_content + entity_field: changed + plugin_id: field + label: Updated + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + type: timestamp + settings: + date_format: short + custom_date_format: '' + timezone: '' + operations: + id: operations + table: block_content + field: operations + relationship: none + group_type: group + admin_label: '' + entity_type: block_content + plugin_id: entity_operations + label: Operations + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + destination: true + pager: + type: mini + options: + offset: 0 + items_per_page: 50 + total_pages: null + id: 0 + tags: + next: 'Next ›' + previous: '‹ Previous' + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: true + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + access: + type: perm + options: + perm: 'administer blocks' + cache: + type: tag + options: { } + empty: + area_text_custom: + id: area_text_custom + table: views + field: area_text_custom + relationship: none + group_type: group + admin_label: '' + plugin_id: text_custom + empty: true + content: 'There are no content blocks available.' + tokenize: false + block_content_listing_empty: + id: block_content_listing_empty + table: block_content + field: block_content_listing_empty + relationship: none + group_type: group + admin_label: '' + entity_type: block_content + plugin_id: block_content_listing_empty + label: '' + empty: true + sorts: { } + arguments: { } + filters: + info: + id: info + table: block_content_field_data + field: info + relationship: none + group_type: group + admin_label: '' + entity_type: block_content + entity_field: info + plugin_id: string + operator: contains + value: '' + group: 1 + exposed: true + expose: + operator_id: info_op + label: 'Block description' + description: '' + use_operator: false + operator: info_op + operator_limit_selection: false + operator_list: { } + identifier: info + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + type: + id: type + table: block_content_field_data + field: type + relationship: none + group_type: group + admin_label: '' + entity_type: block_content + entity_field: type + plugin_id: bundle + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: type_op + label: 'Block type' + description: '' + use_operator: false + operator: type_op + operator_limit_selection: false + operator_list: { } + identifier: type + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + reusable: + id: reusable + table: block_content_field_data + field: reusable + relationship: none + group_type: group + admin_label: '' + entity_type: block_content + entity_field: reusable + plugin_id: boolean + operator: '=' + value: '1' + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + operator_limit_selection: false + operator_list: { } + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + columns: + info: info + type: type + changed: changed + operations: operations + default: changed + info: + info: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + type: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + changed: + sortable: true + default_sort_order: desc + align: '' + separator: '' + empty_column: false + responsive: '' + operations: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + override: true + sticky: false + summary: '' + empty_table: true + caption: '' + description: '' + row: + type: fields + query: + type: views_query + options: + query_comment: '' + disable_sql_rewrite: false + distinct: false + replica: false + query_tags: { } + relationships: { } + header: { } + footer: { } + display_extenders: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } + page_1: + id: page_1 + display_title: Page + display_plugin: page + position: 1 + display_options: + display_extenders: { } + path: admin/content/block + menu: + type: tab + title: 'Blocks' + description: 'Create and edit content blocks.' + weight: 0 + menu_name: admin + parent: system.admin_content + context: '0' + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } diff --git a/tests/modules/block_content_test/block_content_test.info.yml b/tests/modules/block_content_test/block_content_test.info.yml new file mode 100644 index 0000000..48f475b --- /dev/null +++ b/tests/modules/block_content_test/block_content_test.info.yml @@ -0,0 +1,7 @@ +name: "Content Block module tests" +type: module +description: "Support module for content block related testing." +package: Testing +version: VERSION +dependencies: + - drupal:block_content diff --git a/tests/modules/block_content_test/block_content_test.module b/tests/modules/block_content_test/block_content_test.module new file mode 100644 index 0000000..8d10f1a --- /dev/null +++ b/tests/modules/block_content_test/block_content_test.module @@ -0,0 +1,69 @@ + 'Wow', + ]; +} + +/** + * Implements hook_block_content_presave(). + */ +function block_content_test_block_content_presave(BlockContent $block_content) { + if ($block_content->label() == 'testing_block_content_presave') { + $block_content->setInfo($block_content->label() . '_presave'); + } + // Determine changes. + if (!empty($block_content->original) && $block_content->original->label() == 'test_changes') { + if ($block_content->original->label() != $block_content->label()) { + $block_content->setInfo($block_content->label() . '_presave'); + // Drupal 1.0 release. + $block_content->changed = 979534800; + } + } +} + +/** + * Implements hook_block_content_update(). + */ +function block_content_test_block_content_update(BlockContent $block_content) { + // Determine changes on update. + if (!empty($block_content->original) && $block_content->original->label() == 'test_changes') { + if ($block_content->original->label() != $block_content->label()) { + $block_content->setInfo($block_content->label() . '_update'); + } + } +} + +/** + * Implements hook_block_content_insert(). + * + * This tests saving a block_content on block_content insert. + * + * @see \Drupal\block_content\Tests\BlockContentSaveTest::testBlockContentSaveOnInsert() + */ +function block_content_test_block_content_insert(BlockContent $block_content) { + // Set the block_content title to the block_content ID and save. + if ($block_content->label() == 'new') { + $block_content->setInfo('BlockContent ' . $block_content->id()); + $block_content->setNewRevision(FALSE); + $block_content->save(); + } + if ($block_content->label() == 'fail_creation') { + throw new Exception('Test exception for rollback.'); + } +} diff --git a/tests/modules/block_content_test/block_content_test.routing.yml b/tests/modules/block_content_test/block_content_test.routing.yml new file mode 100644 index 0000000..c25abf9 --- /dev/null +++ b/tests/modules/block_content_test/block_content_test.routing.yml @@ -0,0 +1,6 @@ +block_content_test.block_content_view: + path: '/block-content/{block_content}' + defaults: + _entity_view: 'block_content' + requirements: + _entity_access: 'block_content.view' diff --git a/tests/modules/block_content_test/config/install/block.block.foobar_gorilla.yml b/tests/modules/block_content_test/config/install/block.block.foobar_gorilla.yml new file mode 100644 index 0000000..d1b1d76 --- /dev/null +++ b/tests/modules/block_content_test/config/install/block.block.foobar_gorilla.yml @@ -0,0 +1,26 @@ +langcode: en +status: true +dependencies: + module: + - block_content + theme: + - stark +id: foobar_gorilla +theme: stark +region: content +weight: null +provider: null +plugin: 'block_content:fb5e8434-3617-4a1d-a252-8273e95ec30e' +settings: + id: 'block_content:fb5e8434-3617-4a1d-a252-8273e95ec30e' + label: 'Foobar Gorilla' + label_display: visible + provider: block_content + status: true + info: '' + view_mode: default +visibility: + request_path: + id: request_path + negate: false + pages: '' diff --git a/tests/modules/block_content_test/src/Plugin/EntityReferenceSelection/TestSelection.php b/tests/modules/block_content_test/src/Plugin/EntityReferenceSelection/TestSelection.php new file mode 100644 index 0000000..1a46c48 --- /dev/null +++ b/tests/modules/block_content_test/src/Plugin/EntityReferenceSelection/TestSelection.php @@ -0,0 +1,80 @@ +conditionType = $condition_type; + $this->isReusable = $is_reusable; + } + + /** + * {@inheritdoc} + */ + protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') { + $query = parent::buildEntityQuery($match, $match_operator); + if ($this->conditionType) { + /** @var \Drupal\Core\Database\Query\ConditionInterface $add_condition */ + $add_condition = NULL; + switch ($this->conditionType) { + case 'base': + $add_condition = $query; + break; + + case 'group': + $group = $query->andConditionGroup() + ->exists('type'); + $add_condition = $group; + $query->condition($group); + break; + + case "nested_group": + $query->exists('type'); + $sub_group = $query->andConditionGroup() + ->exists('type'); + $add_condition = $sub_group; + $group = $query->andConditionGroup() + ->exists('type') + ->condition($sub_group); + $query->condition($group); + break; + } + if ($this->isReusable) { + $add_condition->condition('reusable', 1); + } + else { + $add_condition->condition('reusable', 0); + } + } + return $query; + } + +} diff --git a/tests/modules/block_content_test_views/block_content_test_views.info.yml b/tests/modules/block_content_test_views/block_content_test_views.info.yml new file mode 100644 index 0000000..9ecd356 --- /dev/null +++ b/tests/modules/block_content_test_views/block_content_test_views.info.yml @@ -0,0 +1,8 @@ +name: 'Block Content test views' +type: module +description: 'Provides default views for views block_content tests.' +package: Testing +version: VERSION +dependencies: + - drupal:block_content + - drupal:views diff --git a/tests/modules/block_content_test_views/test_views/views.view.test_block_content_redirect_destination.yml b/tests/modules/block_content_test_views/test_views/views.view.test_block_content_redirect_destination.yml new file mode 100644 index 0000000..8f902c9 --- /dev/null +++ b/tests/modules/block_content_test_views/test_views/views.view.test_block_content_redirect_destination.yml @@ -0,0 +1,233 @@ +langcode: en +status: true +dependencies: + module: + - block_content +id: test_block_content_redirect_destination +label: 'Redirect destination' +module: views +description: '' +tag: '' +base_table: block_content_field_data +base_field: id +display: + default: + display_plugin: default + id: default + display_title: Default + position: 0 + display_options: + access: + type: none + options: { } + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: mini + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: ‹‹ + next: ›› + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + override: true + sticky: false + caption: '' + summary: '' + description: '' + columns: + info: info + info: + info: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + default: '-1' + empty_table: false + row: + type: 'entity:block_content' + fields: + info: + table: block_content_field_data + field: info + id: info + entity_type: null + entity_field: info + plugin_id: field + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: string + settings: { } + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + operations: + id: operations + table: block_content + field: operations + relationship: none + group_type: group + admin_label: '' + label: 'Operations links' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + destination: true + entity_type: block_content + plugin_id: entity_operations + filters: { } + sorts: { } + title: 'Redirect destination' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + tags: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + display_extenders: { } + path: /admin/content/redirect_destination + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url.query_args + tags: { } diff --git a/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_id.yml b/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_id.yml new file mode 100644 index 0000000..e0f00b6 --- /dev/null +++ b/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_id.yml @@ -0,0 +1,66 @@ +langcode: en +status: true +dependencies: + module: + - block_content +id: test_block_content_revision_id +label: test_block_content_revision_id +module: views +description: '' +tag: '' +base_table: block_content_field_revision +base_field: revision_id +display: + default: + display_options: + relationships: + id: + id: id + table: block_content_field_revision + field: id + required: true + plugin_id: standard + fields: + revision_id: + id: revision_id + table: block_content_field_revision + field: revision_id + plugin_id: field + entity_type: block_content + entity_field: revision_id + id_1: + id: id_1 + table: block_content_field_revision + field: id + plugin_id: field + entity_type: block_content + entity_field: id + id: + id: id + table: block_content_field_data + field: id + relationship: id + plugin_id: field + entity_type: block_content + entity_field: id + arguments: + id: + id: id + table: block_content_field_revision + field: id + plugin_id: numeric + entity_type: block_content + entity_field: id + sorts: + revision_id: + id: revision_id + table: block_content_field_revision + field: revision_id + order: ASC + plugin_id: field + entity_type: block_content + entity_field: revision_id + display_plugin: default + display_title: Default + id: default + position: 0 diff --git a/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_revision_id.yml b/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_revision_id.yml new file mode 100644 index 0000000..5511add --- /dev/null +++ b/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_revision_id.yml @@ -0,0 +1,69 @@ +langcode: en +status: true +dependencies: + module: + - block_content +id: test_block_content_revision_revision_id +label: test_block_content_revision_revision_id +module: views +description: '' +tag: '' +base_table: block_content_field_revision +base_field: revision_id +display: + default: + display_options: + relationships: + revision_id: + id: revision_id + table: block_content_field_revision + field: revision_id + required: true + entity_type: block_content + entity_field: revision_id + plugin_id: standard + fields: + revision_id: + id: revision_id + table: block_content_field_revision + field: revision_id + plugin_id: field + entity_type: block_content + entity_field: revision_id + id_1: + id: id_1 + table: block_content_field_revision + field: id + plugin_id: field + entity_type: block_content + entity_field: id + id: + id: id + table: block_content_field_data + field: id + relationship: revision_id + plugin_id: field + entity_type: block_content + entity_field: id + arguments: + id: + id: id + table: block_content_field_revision + field: id + plugin_id: block_content_id + entity_type: block_content + entity_field: id + sorts: + revision_id: + id: revision_id + table: block_content_field_revision + field: revision_id + order: ASC + plugin_id: field + entity_type: block_content + entity_field: revision_id + display_extenders: { } + display_plugin: default + display_title: Default + id: default + position: 0 diff --git a/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_user.yml b/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_user.yml new file mode 100644 index 0000000..8d7f550 --- /dev/null +++ b/tests/modules/block_content_test_views/test_views/views.view.test_block_content_revision_user.yml @@ -0,0 +1,324 @@ +langcode: en +status: true +dependencies: + module: + - block_content + - user +id: test_block_content_revision_user +label: 'Test block content revision user' +module: views +description: '' +tag: '' +base_table: block_content_field_data +base_field: id +display: + default: + display_plugin: default + id: default + display_title: Default + position: 0 + display_options: + access: + type: perm + options: + perm: 'access content' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: none + options: + offset: 0 + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + id: + id: id + table: block_content_field_revision + field: id + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: number_integer + settings: + thousand_separator: '' + prefix_suffix: false + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: block_content + entity_field: id + plugin_id: field + revision_id: + id: revision_id + table: block_content_field_revision + field: revision_id + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: number_integer + settings: + thousand_separator: '' + prefix_suffix: false + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: block_content + entity_field: revision_id + plugin_id: field + revision_user: + id: revision_user + table: block_content_revision + field: revision_user + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: target_id + type: entity_reference_label + settings: + link: false + group_column: target_id + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: block_content + entity_field: revision_user + plugin_id: field + filters: + revision_user: + id: revision_user + table: block_content_revision + field: revision_user + relationship: none + group_type: group + admin_label: '' + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: revision_user_op + label: 'Revision user' + description: '' + use_operator: false + operator: revision_user_op + operator_limit_selection: false + operator_list: { } + identifier: revision_user + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: block_content + entity_field: revision_user + plugin_id: user_name + sorts: { } + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + filter_groups: + operator: AND + groups: { } + cache_metadata: + max-age: -1 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - 'user.block_content_grants:view' + - user.permissions + tags: { } diff --git a/tests/modules/block_content_test_views/test_views/views.view.test_block_content_view.yml b/tests/modules/block_content_test_views/test_views/views.view.test_block_content_view.yml new file mode 100644 index 0000000..25e9e8a --- /dev/null +++ b/tests/modules/block_content_test_views/test_views/views.view.test_block_content_view.yml @@ -0,0 +1,190 @@ +langcode: en +status: true +dependencies: + module: + - block_content +id: test_block_content_view +label: test_block_content_view +module: views +description: '' +tag: '' +base_table: block_content_field_data +base_field: id +display: + default: + display_plugin: default + id: default + display_title: Default + position: null + display_options: + access: + type: none + options: { } + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + quantity: 9 + style: + type: default + row: + type: fields + fields: + id: + id: id + table: block_content_field_data + field: id + relationship: none + group_type: group + admin_label: '' + label: Id + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + plugin_id: field + entity_type: block_content + entity_field: id + sorts: + id: + id: id + table: block_content_field_data + field: id + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + entity_type: block_content + entity_field: id + plugin_id: standard + title: test_block_content_view + header: { } + footer: { } + empty: { } + relationships: { } + display_extenders: { } + arguments: + type: + id: type + table: block_content_field_data + field: type + relationship: none + group_type: group + admin_label: '' + default_action: 'not found' + exception: + value: all + title_enable: false + title: All + title_enable: false + title: '' + default_argument_type: fixed + default_argument_options: + argument: '' + summary_options: + base_path: '' + count: true + items_per_page: 25 + override: false + summary: + sort_order: asc + number_of_records: 0 + format: default_summary + specify_validation: false + validate: + type: none + fail: 'not found' + validate_options: { } + glossary: false + limit: 0 + case: none + path_case: none + transform_dash: false + break_phrase: false + entity_type: block_content + entity_field: type + plugin_id: string + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: null + display_options: + path: test-block_content-view + display_extenders: { } diff --git a/tests/modules/block_content_test_views/test_views/views.view.test_field_filters.yml b/tests/modules/block_content_test_views/test_views/views.view.test_field_filters.yml new file mode 100644 index 0000000..a55edad --- /dev/null +++ b/tests/modules/block_content_test_views/test_views/views.view.test_field_filters.yml @@ -0,0 +1,338 @@ +langcode: en +status: true +dependencies: + module: + - block_content +id: test_field_filters +label: 'Test field filters' +module: views +description: '' +tag: '' +base_table: block_content_field_data +base_field: id +display: + default: + display_plugin: default + id: default + display_title: Default + position: 0 + display_options: + access: + type: none + options: { } + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: none + options: + items_per_page: 0 + offset: 0 + style: + type: default + row: + type: 'entity:block_content' + options: + relationship: none + view_mode: default + fields: + info: + id: info + table: block_content_field_data + field: info + label: '' + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + relationship: none + group_type: group + admin_label: '' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + entity_type: block_content + type: string + settings: + link_to_entity: true + entity_field: title + plugin_id: field + filters: + info: + id: info + table: block_content_field_data + field: info + relationship: none + group_type: group + admin_label: '' + operator: contains + value: Paris + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + entity_type: block_content + entity_field: info + sorts: + changed: + id: changed + table: block_content_field_data + field: changed + order: DESC + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + plugin_id: date + entity_type: block_content + entity_field: changed + title: 'Test field filters' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + rendering_language: '***LANGUAGE_entity_translation***' + display_extenders: { } + page_bf: + display_plugin: page + id: page_bf + display_title: 'Body filter page' + position: 1 + display_options: + path: test-body-filter + display_description: '' + title: 'Test body filters' + defaults: + title: false + filters: false + filter_groups: false + filters: + body_value: + id: body_value + table: block_content__body + field: body_value + relationship: none + group_type: group + admin_label: '' + operator: contains + value: Comida + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + entity_type: block_content + entity_field: body + filter_groups: + operator: AND + groups: + 1: AND + display_extenders: { } + page_bfp: + display_plugin: page + id: page_bfp + display_title: 'Body filter page Paris' + position: 1 + display_options: + path: test-body-paris + display_description: '' + title: 'Test body filters' + defaults: + title: false + filters: false + filter_groups: false + filters: + body_value: + id: body_value + table: block_content__body + field: body_value + relationship: none + group_type: group + admin_label: '' + operator: contains + value: Paris + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + entity_type: block_content + entity_field: body + filter_groups: + operator: AND + groups: + 1: AND + display_extenders: { } + page_if: + display_plugin: page + id: page_if + display_title: 'Info filter page' + position: 1 + display_options: + path: test-info-filter + display_description: '' + title: 'Test info filter' + defaults: + title: false + filters: false + filter_groups: false + filters: + info: + id: info + table: block_content_field_data + field: info + relationship: none + group_type: group + admin_label: '' + operator: contains + value: Comida + group: 1 + exposed: false + expose: + operator_id: '' + label: '' + description: '' + use_operator: false + operator: '' + identifier: '' + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + plugin_id: string + entity_type: block_content + entity_field: info + filter_groups: + operator: AND + groups: + 1: AND + display_extenders: { } + page_ifp: + display_plugin: page + id: page_ifp + display_title: 'Info filter page Paris' + position: 1 + display_options: + path: test-info-paris + display_description: '' + title: 'Test info filter' + defaults: + title: false + display_extenders: { } diff --git a/tests/modules/block_content_test_views/test_views/views.view.test_field_type.yml b/tests/modules/block_content_test_views/test_views/views.view.test_field_type.yml new file mode 100644 index 0000000..a3bc882 --- /dev/null +++ b/tests/modules/block_content_test_views/test_views/views.view.test_field_type.yml @@ -0,0 +1,27 @@ +langcode: en +status: true +dependencies: + module: + - block_content +id: test_field_type +label: test_field_type +module: views +description: '' +tag: '' +base_table: block_content_field_data +base_field: id +display: + default: + display_options: + fields: + type: + field: type + id: type + table: block_content_field_data + plugin_id: field + entity_type: block_content + entity_field: type + display_plugin: default + display_title: Default + id: default + position: 0 diff --git a/tests/src/Functional/BlockContentCacheTagsTest.php b/tests/src/Functional/BlockContentCacheTagsTest.php new file mode 100644 index 0000000..b8a01bc --- /dev/null +++ b/tests/src/Functional/BlockContentCacheTagsTest.php @@ -0,0 +1,100 @@ + 'basic', + 'label' => 'basic', + 'revision' => FALSE, + ]); + $block_content_type->save(); + block_content_add_body_field($block_content_type->id()); + + // Create a "Llama" content block. + $block_content = BlockContent::create([ + 'info' => 'Llama', + 'type' => 'basic', + 'body' => [ + 'value' => 'The name "llama" was adopted by European settlers from native Peruvians.', + 'format' => 'plain_text', + ], + ]); + $block_content->save(); + + return $block_content; + } + + /** + * {@inheritdoc} + * + * @see \Drupal\block_content\BlockContentAccessControlHandler::checkAccess() + */ + protected function getAccessCacheContextsForEntity(EntityInterface $entity) { + return []; + } + + /** + * {@inheritdoc} + * + * Each comment must have a comment body, which always has a text format. + */ + protected function getAdditionalCacheTagsForEntity(EntityInterface $entity) { + return ['config:filter.format.plain_text']; + } + + /** + * Tests that the block is cached with the correct contexts and tags. + */ + public function testBlock(): void { + $block = $this->drupalPlaceBlock('block_content:' . $this->entity->uuid()); + $build = $this->container->get('entity_type.manager')->getViewBuilder('block')->view($block, 'block'); + + // Render the block. + $this->container->get('renderer')->renderRoot($build); + + // Expected keys, contexts, and tags for the block. + // @see \Drupal\block\BlockViewBuilder::viewMultiple() + $expected_block_cache_keys = ['entity_view', 'block', $block->id()]; + $expected_block_cache_tags = Cache::mergeTags(['block_view', 'rendered'], $block->getCacheTags()); + $expected_block_cache_tags = Cache::mergeTags($expected_block_cache_tags, $block->getPlugin()->getCacheTags()); + + // Expected contexts and tags for the BlockContent entity. + // @see \Drupal\Core\Entity\EntityViewBuilder::getBuildDefaults(). + $expected_entity_cache_tags = Cache::mergeTags(['block_content_view'], $this->entity->getCacheTags()); + $expected_entity_cache_tags = Cache::mergeTags($expected_entity_cache_tags, $this->getAdditionalCacheTagsForEntity($this->entity)); + + // Verify that what was render cached matches the above expectations. + $this->verifyRenderCache($expected_block_cache_keys, Cache::mergeTags($expected_block_cache_tags, $expected_entity_cache_tags), CacheableMetadata::createFromRenderArray($build)); + } + +} diff --git a/tests/src/Functional/BlockContentContextualLinksTest.php b/tests/src/Functional/BlockContentContextualLinksTest.php new file mode 100644 index 0000000..c01a2ff --- /dev/null +++ b/tests/src/Functional/BlockContentContextualLinksTest.php @@ -0,0 +1,44 @@ +createBlockContent(); + + $block = $this->placeBlock('block_content:' . $block_content->uuid()); + + $user = $this->drupalCreateUser([ + 'administer blocks', + 'access contextual links', + ]); + $this->drupalLogin($user); + + $this->drupalGet(''); + $this->assertSession()->elementAttributeContains('css', 'div[data-contextual-id]', 'data-contextual-id', 'block:block=' . $block->id() . ':langcode=en|block_content:block_content=' . $block_content->id() . ':'); + } + +} diff --git a/tests/src/Functional/BlockContentCreationTest.php b/tests/src/Functional/BlockContentCreationTest.php new file mode 100644 index 0000000..2b70765 --- /dev/null +++ b/tests/src/Functional/BlockContentCreationTest.php @@ -0,0 +1,336 @@ +drupalLogin($this->adminUser); + } + + /** + * Creates a "Basic block" block and verifies its consistency in the database. + */ + public function testBlockContentCreation(): void { + $this->drupalLogin($this->adminUser); + + // Create a block. + $edit = []; + $edit['info[0][value]'] = 'Test Block'; + $edit['body[0][value]'] = $this->randomMachineName(16); + $this->drupalGet('block/add/basic'); + $this->submitForm($edit, 'Save'); + + // Check that the Basic block has been created. + $this->assertSession()->pageTextContains('basic ' . $edit['info[0][value]'] . ' has been created.'); + + // Check that the view mode setting is hidden because only one exists. + $this->assertSession()->fieldNotExists('settings[view_mode]'); + + // Check that the block exists in the database. + $block = $this->getBlockByLabel($edit['info[0][value]']); + $this->assertNotEmpty($block, 'Content Block found in database.'); + } + + /** + * Creates a "Basic page" block with multiple view modes. + */ + public function testBlockContentCreationMultipleViewModes(): void { + // Add a new view mode and verify if it is selected as expected. + $this->drupalLogin($this->drupalCreateUser(['administer display modes'])); + $this->drupalGet('admin/structure/display-modes/view/add/block_content'); + $edit = [ + 'id' => 'test_view_mode', + 'label' => 'Test View Mode', + ]; + $this->submitForm($edit, 'Save'); + $this->assertSession()->pageTextContains('Saved the ' . $edit['label'] . ' view mode.'); + + $this->drupalLogin($this->adminUser); + + // Create a block. + $edit = []; + $edit['info[0][value]'] = 'Test Block'; + $edit['body[0][value]'] = $this->randomMachineName(16); + $this->drupalGet('block/add/basic'); + $this->submitForm($edit, 'Save and configure'); + + // Save our block permanently + $this->submitForm(['region' => 'content'], 'Save block'); + + // Set test_view_mode as a custom display to be available on the list. + $this->drupalGet('admin/structure/block-content/manage/basic/display'); + $custom_view_mode = [ + 'display_modes_custom[test_view_mode]' => 1, + ]; + $this->submitForm($custom_view_mode, 'Save'); + + // Go to the configure page and change the view mode. + $this->drupalGet('admin/structure/block/manage/stark_testblock'); + + // Test the available view mode options. + // Verify that the default view mode is available. + $this->assertSession()->optionExists('edit-settings-view-mode', 'default'); + // Verify that the test view mode is available. + $this->assertSession()->optionExists('edit-settings-view-mode', 'test_view_mode'); + + $view_mode['settings[view_mode]'] = 'test_view_mode'; + $this->submitForm($view_mode, 'Save block'); + + // Check that the view mode setting is shown because more than one exists. + $this->drupalGet('admin/structure/block/manage/stark_testblock'); + $this->assertSession()->fieldExists('settings[view_mode]'); + + // Change the view mode. + $view_mode['region'] = 'content'; + $view_mode['settings[view_mode]'] = 'test_view_mode'; + $this->submitForm($view_mode, 'Save block'); + + // Go to the configure page and verify the view mode has changed. + $this->drupalGet('admin/structure/block/manage/stark_testblock'); + $this->assertSession()->fieldValueEquals('settings[view_mode]', 'test_view_mode'); + + // Check that the block exists in the database. + $block = $this->getBlockByLabel($edit['info[0][value]']); + $this->assertNotEmpty($block, 'Content Block found in database.'); + } + + /** + * Tests the redirect workflow of creating a block_content and block. + */ + public function testBlockContentFormSubmitHandlers(): void { + $this->drupalLogin($this->adminUser); + + // Create a block and place in block layout. + $this->drupalGet('/admin/content/block'); + $this->clickLink('Add content block'); + // Verify destination URL, when clicking "Save and configure" this + // destination will be ignored. + $base = base_path(); + $url = 'block/add?destination=' . $base . 'admin/content/block'; + $this->assertSession()->addressEquals($url); + $edit = []; + $edit['info[0][value]'] = 'Test Block'; + $edit['body[0][value]'] = $this->randomMachineName(16); + $this->submitForm($edit, 'Save and configure'); + $this->assertSession()->pageTextContains('basic ' . $edit['info[0][value]'] . ' has been created.'); + $this->assertSession()->pageTextContains('Configure block'); + + // Verify when editing a block "Save and configure" does not appear. + $this->drupalGet('/admin/content/block/1'); + $this->assertSession()->buttonNotExists('Save and configure'); + + // Create a block but go back to block library. + $edit = []; + $edit['info[0][value]'] = 'Test Block'; + $edit['body[0][value]'] = $this->randomMachineName(16); + $this->drupalGet('block/add/basic'); + $this->submitForm($edit, 'Save'); + // Check that the Basic block has been created. + $this->assertSession()->pageTextContains('basic ' . $edit['info[0][value]'] . ' has been created.'); + $this->assertSession()->addressEquals('/admin/content/block'); + + // Check that the user is redirected to the block library on edit. + $block = $this->getBlockByLabel($edit['info[0][value]']); + $this->drupalGet($block->toUrl('edit-form')); + $this->submitForm([ + 'info[0][value]' => 'Test Block Updated', + ], 'Save'); + $this->assertSession()->addressEquals('admin/content/block'); + + // Test with user who doesn't have permission to place a block. + $this->drupalLogin($this->drupalCreateUser(['administer block content'])); + $this->drupalGet('block/add/basic'); + $this->assertSession()->buttonNotExists('Save and configure'); + + } + + /** + * Create a default content block. + * + * Creates a content block from defaults and ensures that the 'basic block' + * type is being used. + */ + public function testDefaultBlockContentCreation(): void { + $edit = []; + $edit['info[0][value]'] = $this->randomMachineName(8); + $edit['body[0][value]'] = $this->randomMachineName(16); + // Don't pass the content block type in the URL so the default is forced. + $this->drupalGet('block/add'); + $this->submitForm($edit, 'Save'); + + // Check that the block has been created and that it is a basic block. + $this->assertSession()->pageTextContains('basic ' . $edit['info[0][value]'] . ' has been created.'); + + // Check that the block exists in the database. + $block = $this->getBlockByLabel($edit['info[0][value]']); + $this->assertNotEmpty($block, 'Default Content Block found in database.'); + } + + /** + * Verifies that a transaction rolls back the failed creation. + */ + public function testFailedBlockCreation(): void { + // Create a block. + try { + $this->createBlockContent('fail_creation'); + $this->fail('Expected exception has not been thrown.'); + } + catch (\Exception $e) { + // Expected exception; just continue testing. + } + + $connection = Database::getConnection(); + + // Check that the block does not exist in the database. + $id = $connection->select('block_content_field_data', 'b') + ->fields('b', ['id']) + ->condition('info', 'fail_creation') + ->execute() + ->fetchField(); + $this->assertFalse($id); + } + + /** + * Tests deleting a block. + */ + public function testBlockDelete(): void { + // Create a block. + $edit = []; + $edit['info[0][value]'] = $this->randomMachineName(8); + $body = $this->randomMachineName(16); + $edit['body[0][value]'] = $body; + $this->drupalGet('block/add/basic'); + $this->submitForm($edit, 'Save'); + + // Place the block. + $instance = [ + 'id' => mb_strtolower($edit['info[0][value]']), + 'settings[label]' => $edit['info[0][value]'], + 'region' => 'sidebar_first', + ]; + $block = BlockContent::load(1); + $url = 'admin/structure/block/add/block_content:' . $block->uuid() . '/' . $this->config('system.theme')->get('default'); + $this->drupalGet($url); + $this->submitForm($instance, 'Save block'); + + $block = BlockContent::load(1); + + // Test getInstances method. + $this->assertCount(1, $block->getInstances()); + + // Navigate to home page. + $this->drupalGet(''); + $this->assertSession()->pageTextContains($body); + + // Delete the block. + $this->drupalGet('admin/content/block/1/delete'); + $this->assertSession()->pageTextContains('This will also remove 1 placed block instance.'); + + $this->submitForm([], 'Delete'); + $this->assertSession()->pageTextContains('The content block ' . $edit['info[0][value]'] . ' has been deleted.'); + + // Create another block and force the plugin cache to flush. + $edit2 = []; + $edit2['info[0][value]'] = $this->randomMachineName(8); + $body2 = $this->randomMachineName(16); + $edit2['body[0][value]'] = $body2; + $this->drupalGet('block/add/basic'); + $this->submitForm($edit2, 'Save'); + + $this->assertSession()->responseNotContains('Error message'); + + // Create another block with no instances, and test we don't get a + // confirmation message about deleting instances. + $edit3 = []; + $edit3['info[0][value]'] = $this->randomMachineName(8); + $body = $this->randomMachineName(16); + $edit3['body[0][value]'] = $body; + $this->drupalGet('block/add/basic'); + $this->submitForm($edit3, 'Save'); + + // Show the delete confirm form. + $this->drupalGet('admin/content/block/3/delete'); + $this->assertSession()->pageTextNotContains('This will also remove'); + } + + /** + * Tests placed content blocks create a dependency in the block placement. + */ + public function testConfigDependencies(): void { + $block = $this->createBlockContent(); + // Place the block. + $block_placement_id = mb_strtolower($block->label()); + $instance = [ + 'id' => $block_placement_id, + 'settings[label]' => $block->label(), + 'region' => 'sidebar_first', + ]; + $block = BlockContent::load(1); + $url = 'admin/structure/block/add/block_content:' . $block->uuid() . '/' . $this->config('system.theme')->get('default'); + $this->drupalGet($url); + $this->submitForm($instance, 'Save block'); + + $dependencies = \Drupal::service('config.manager')->findConfigEntityDependenciesAsEntities('content', [$block->getConfigDependencyName()]); + $block_placement = reset($dependencies); + $this->assertEquals($block_placement_id, $block_placement->id(), "The block placement config entity has a dependency on the block content entity."); + } + + /** + * Load a block based on the label. + */ + private function getBlockByLabel(string $label): ?BlockContentInterface { + $blocks = \Drupal::entityTypeManager() + ->getStorage('block_content') + ->loadByProperties(['info' => $label]); + if (empty($blocks)) { + return NULL; + } + return reset($blocks); + } + +} diff --git a/tests/src/Functional/BlockContentListTest.php b/tests/src/Functional/BlockContentListTest.php new file mode 100644 index 0000000..daa09f2 --- /dev/null +++ b/tests/src/Functional/BlockContentListTest.php @@ -0,0 +1,203 @@ +baseUser1 = $this->drupalCreateUser(['access block library']); + $this->baseUser2 = $this->drupalCreateUser([ + 'access block library', + 'create basic block content', + 'edit any basic block content', + 'delete any basic block content', + ]); + } + + /** + * Tests the region value when a new block is saved. + */ + public function testBlockRegionPlacement(): void { + $this->drupalLogin($this->drupalCreateUser($this->permissions)); + $this->drupalGet("admin/structure/block/library/stark", ['query' => ['region' => 'content']]); + + $this->clickLink('Add content block'); + $this->assertSession()->statusCodeEquals(200); + $edit = [ + 'info[0][value]' => 'foo', + ]; + $this->submitForm($edit, 'Save'); + $this->assertSession()->fieldValueEquals('region', 'content'); + } + + /** + * Tests the content block listing page with different permissions. + */ + public function testListing(): void { + // Test with the admin user. + $this->drupalLogin($this->drupalCreateUser(['access block library', 'administer block content'])); + $this->drupalGet('admin/content/block'); + + // Test for the page title. + $this->assertSession()->titleEquals('Content blocks | Drupal'); + + // Test for the table. + $this->assertSession()->elementExists('xpath', '//div[@class="layout-content"]//table'); + + // Test the table header, two cells should be present. + $this->assertSession()->elementsCount('xpath', '//div[@class="layout-content"]//table/thead/tr/th', 2); + + // Test the contents of each th cell. + $this->assertSession()->elementTextEquals('xpath', '//div[@class="layout-content"]//table/thead/tr/th[1]', 'Block description'); + $this->assertSession()->elementTextEquals('xpath', '//div[@class="layout-content"]//table/thead/tr/th[2]', 'Operations'); + + $label = 'Antelope'; + $new_label = 'Albatross'; + // Add a new entity using the operations link. + $this->clickLink('Add content block'); + $this->assertSession()->statusCodeEquals(200); + $edit = []; + $edit['info[0][value]'] = $label; + $edit['body[0][value]'] = $this->randomMachineName(16); + $this->submitForm($edit, 'Save'); + + // Confirm that once the user returns to the listing, the text of the label + // (versus elsewhere on the page). + $this->assertSession()->elementTextContains('xpath', '//td', $label); + + // Check the number of table row cells. + $this->assertSession()->elementsCount('xpath', '//div[@class="layout-content"]//table/tbody/tr[1]/td', 2); + // Check the contents of the row. The first cell contains the label, + // and the second contains the operations list. + $this->assertSession()->elementTextEquals('xpath', '//div[@class="layout-content"]//table/tbody/tr[1]/td[1]', $label); + + // Edit the entity using the operations link. + $blocks = $this->container + ->get('entity_type.manager') + ->getStorage('block_content') + ->loadByProperties(['info' => $label]); + $block = reset($blocks); + if (!empty($block)) { + $this->assertSession()->linkByHrefExists('admin/content/block/' . $block->id()); + $this->clickLink('Edit'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->titleEquals("Edit content block $label | Drupal"); + $edit = ['info[0][value]' => $new_label]; + $this->submitForm($edit, 'Save'); + } + else { + $this->fail('Did not find Albatross block in the database.'); + } + + // Confirm that once the user returns to the listing, the text of the label + // (versus elsewhere on the page). + $this->assertSession()->elementTextContains('xpath', '//td', $new_label); + + // Delete the added entity using the operations link. + $this->assertSession()->linkByHrefExists('admin/content/block/' . $block->id() . '/delete'); + $this->clickLink('Delete'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->titleEquals("Are you sure you want to delete the content block $new_label? | Drupal"); + $this->submitForm([], 'Delete'); + + // Verify that the text of the label and machine name does not appear in + // the list (though it may appear elsewhere on the page). + $this->assertSession()->elementTextNotContains('xpath', '//td', $new_label); + + // Confirm that the empty text is displayed. + $this->assertSession()->pageTextContains('There are no content blocks yet.'); + + $block_content = BlockContent::create([ + 'info' => 'Non-reusable block', + 'type' => 'basic', + 'reusable' => FALSE, + ]); + $block_content->save(); + + $this->drupalGet('admin/content/block'); + // Confirm that the empty text is displayed. + $this->assertSession()->pageTextContains('There are no content blocks yet.'); + // Confirm the non-reusable block is not on the page. + $this->assertSession()->pageTextNotContains('Non-reusable block'); + + $this->drupalLogout(); + + // Create test block for other user tests. + $test_block = $this->createBlockContent($label); + + $link_text = t('Add content block'); + // Test as a user with view only permissions. + $this->drupalLogin($this->baseUser1); + $this->drupalGet('admin/content/block'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->linkNotExists($link_text); + $this->assertSession()->linkByHrefNotExists('admin/content/block/' . $test_block->id()); + $this->assertSession()->linkByHrefNotExists('admin/content/block/' . $test_block->id() . '/delete'); + + $this->drupalLogout(); + + // Test as a user with permission to create/edit/delete basic blocks. + $this->drupalLogin($this->baseUser2); + $this->drupalGet('admin/content/block'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->linkExists($link_text); + $this->assertSession()->linkByHrefExists('admin/content/block/' . $test_block->id()); + $this->assertSession()->linkByHrefExists('admin/content/block/' . $test_block->id() . '/delete'); + } + +} diff --git a/tests/src/Functional/BlockContentListViewsTest.php b/tests/src/Functional/BlockContentListViewsTest.php new file mode 100644 index 0000000..0d624cc --- /dev/null +++ b/tests/src/Functional/BlockContentListViewsTest.php @@ -0,0 +1,205 @@ +baseUser1 = $this->drupalCreateUser(['access block library']); + $this->baseUser2 = $this->drupalCreateUser([ + 'access block library', + 'create basic block content', + 'edit any basic block content', + 'delete any basic block content', + ]); + } + + /** + * Tests the content block listing page. + */ + public function testListing(): void { + // Test with an admin user. + $this->drupalLogin($this->adminUser); + $this->drupalGet('admin/content/block'); + + // Test for the page title. + $this->assertSession()->titleEquals('Content blocks | Drupal'); + + // Test for the exposed filters. + $this->assertSession()->fieldExists('info'); + $this->assertSession()->fieldExists('type'); + + // Test for the table. + $this->assertSession()->elementExists('xpath', '//div[@class="layout-content"]//table'); + + // Test the table header, four cells should be present. + $this->assertSession()->elementsCount('xpath', '//div[@class="layout-content"]//table/thead/tr/th', 4); + + // Test the contents of each th cell. + $this->assertSession()->elementTextEquals('xpath', '//div[@class="layout-content"]//table/thead/tr/th[1]', 'Block description'); + $this->assertSession()->elementTextEquals('xpath', '//div[@class="layout-content"]//table/thead/tr/th[2]', 'Block type'); + $this->assertSession()->elementTextEquals('xpath', '//div[@class="layout-content"]//table/thead/tr/th[3]', 'Updated Sort ascending'); + $this->assertSession()->elementTextEquals('xpath', '//div[@class="layout-content"]//table/thead/tr/th[4]', 'Operations'); + + $label = 'Antelope'; + $new_label = 'Albatross'; + // Add a new entity using the operations link. + $this->clickLink('Add content block'); + $this->assertSession()->statusCodeEquals(200); + $edit = []; + $edit['info[0][value]'] = $label; + $edit['body[0][value]'] = $this->randomMachineName(16); + $this->submitForm($edit, 'Save'); + + // Confirm that once the user returns to the listing, the text of the label + // (versus elsewhere on the page). + $this->assertSession()->elementTextContains('xpath', '//td/a', $label); + + // Check the number of table row cells. + $this->assertSession()->elementsCount('xpath', '//div[@class="layout-content"]//table/tbody/tr/td', 4); + // Check the contents of each row cell. The first cell contains the label, + // the second contains the machine name, and the third contains the + // operations list. + $this->assertSession()->elementTextEquals('xpath', '//div[@class="layout-content"]//table/tbody/tr/td/a', $label); + + // Edit the entity using the operations link. + $blocks = $this->container + ->get('entity_type.manager') + ->getStorage('block_content') + ->loadByProperties(['info' => $label]); + $block = reset($blocks); + if (!empty($block)) { + $this->assertSession()->linkByHrefExists('admin/content/block/' . $block->id()); + $this->clickLink('Edit'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->titleEquals("Edit content block $label | Drupal"); + $edit = ['info[0][value]' => $new_label]; + $this->submitForm($edit, 'Save'); + } + else { + $this->fail('Did not find Albatross block in the database.'); + } + + // Confirm that once the user returns to the listing, the text of the label + // (versus elsewhere on the page). + $this->assertSession()->elementTextContains('xpath', '//td/a', $new_label); + + // Delete the added entity using the operations link. + $this->assertSession()->linkByHrefExists('admin/content/block/' . $block->id() . '/delete'); + $this->clickLink('Delete'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->titleEquals("Are you sure you want to delete the content block $new_label? | Drupal"); + $this->submitForm([], 'Delete'); + + // Verify that the text of the label and machine name does not appear in + // the list (though it may appear elsewhere on the page). + $this->assertSession()->elementTextNotContains('xpath', '//td', $new_label); + + // Confirm that the empty text is displayed. + $this->assertSession()->pageTextContains('There are no content blocks available.'); + $this->assertSession()->linkExists('content block'); + + $block_content = BlockContent::create([ + 'info' => 'Non-reusable block', + 'type' => 'basic', + 'reusable' => FALSE, + ]); + $block_content->save(); + + $this->drupalGet('admin/content/block'); + // Confirm that the empty text is displayed. + $this->assertSession()->pageTextContains('There are no content blocks available.'); + // Confirm the non-reusable block is not on the page. + $this->assertSession()->pageTextNotContains('Non-reusable block'); + + $this->drupalLogout(); + + // Create test block for other user tests. + $test_block = $this->createBlockContent($label); + + $link_text = t('Add content block'); + // Test as a user with view only permissions. + $this->drupalLogin($this->baseUser1); + $this->drupalGet('admin/content/block'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->linkNotExists($link_text); + $matches = $this->xpath('//td[1]'); + $actual = $matches[0]->getText(); + $this->assertEquals($label, $actual, 'Label found for test block.'); + $this->assertSession()->linkNotExists('Edit'); + $this->assertSession()->linkNotExists('Delete'); + $this->assertSession()->linkByHrefNotExists('admin/content/block/' . $test_block->id() . '/delete'); + + $this->drupalLogout(); + + // Test as a user with permission to create/edit/delete basic blocks. + $this->drupalLogin($this->baseUser2); + $this->drupalGet('admin/content/block'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->linkExists($link_text); + $matches = $this->xpath('//td/a'); + $actual = $matches[0]->getText(); + $this->assertEquals($label, $actual, 'Label found for test block.'); + $this->assertSession()->linkByHrefExists('admin/content/block/' . $test_block->id()); + $this->assertSession()->linkByHrefExists('admin/content/block/' . $test_block->id() . '/delete'); + } + +} diff --git a/tests/src/Functional/BlockContentPageViewTest.php b/tests/src/Functional/BlockContentPageViewTest.php new file mode 100644 index 0000000..7b9aaa8 --- /dev/null +++ b/tests/src/Functional/BlockContentPageViewTest.php @@ -0,0 +1,40 @@ +drupalLogin($this->adminUser); + $block = $this->createBlockContent(); + + // Attempt to view the block. + $this->drupalGet('block-content/' . $block->id()); + + // Ensure user was able to view the block. + $this->assertSession()->statusCodeEquals(200); + $this->drupalGet(''); + $this->assertSession()->pageTextContains('This block is broken or missing. You may be missing content or you might need to install the original module.'); + } + +} diff --git a/tests/src/Functional/BlockContentRedirectTest.php b/tests/src/Functional/BlockContentRedirectTest.php new file mode 100644 index 0000000..9028a4a --- /dev/null +++ b/tests/src/Functional/BlockContentRedirectTest.php @@ -0,0 +1,74 @@ +drupalLogin($this->adminUser); + $this->expectDeprecation('The path /admin/structure/block/block-content/types is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use /admin/structure/block-content. See https://www.drupal.org/node/3320855'); + $this->drupalGet('/admin/structure/block/block-content/types'); + $this->assertSession() + ->pageTextContains("You have been redirected from admin/structure/block/block-content/types. Update links, shortcuts, and bookmarks to use admin/structure/block-content."); + } + + /** + * Tests the deprecation message from the old block library page. + * + * @group legacy + */ + public function testBlockLibraryRedirect(): void { + $this->drupalLogin($this->adminUser); + $this->expectDeprecation('The path /admin/structure/block/block-content is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use /admin/content/block. See https://www.drupal.org/node/3320855'); + $this->drupalGet('admin/structure/block/block-content'); + $this->assertSession() + ->pageTextContains("You have been redirected from admin/structure/block/block-content. Update links, shortcuts, and bookmarks to use admin/content/block."); + } + + /** + * Tests the deprecation message from the old block edit page. + * + * @group legacy + */ + public function testBlockContentEditRedirect(): void { + $block = $this->createBlockContent(); + $this->drupalLogin($this->adminUser); + $this->expectDeprecation('The path /block/{block_content} is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use /admin/content/block/{block_content}. See https://www.drupal.org/node/3320855'); + $this->drupalGet("/block/{$block->id()}"); + $this->assertSession() + ->pageTextContains("You have been redirected from block/{$block->id()}. Update links, shortcuts, and bookmarks to use admin/content/block/{$block->id()}."); + } + + /** + * Tests the deprecation message from the old block delete page. + * + * @group legacy + */ + public function testBlockContentDeleteRedirect(): void { + $block = $this->createBlockContent(); + $this->drupalLogin($this->adminUser); + $this->expectDeprecation('The path /block/{block_content} is deprecated in drupal:10.1.0 and is removed from drupal:11.0.0. Use /admin/content/block/{block_content}. See https://www.drupal.org/node/3320855'); + $this->drupalGet("/block/{$block->id()}/delete"); + $this->assertSession() + ->pageTextContains("You have been redirected from block/{$block->id()}/delete. Update links, shortcuts, and bookmarks to use admin/content/block/{$block->id()}/delete."); + } + +} diff --git a/tests/src/Functional/BlockContentRevisionDeleteTest.php b/tests/src/Functional/BlockContentRevisionDeleteTest.php new file mode 100644 index 0000000..61ff653 --- /dev/null +++ b/tests/src/Functional/BlockContentRevisionDeleteTest.php @@ -0,0 +1,96 @@ +drupalLogin($this->adminUser); + $this->drupalPlaceBlock('page_title_block'); + } + + /** + * Tests revision delete. + */ + public function testDeleteForm(): void { + $entity = $this->createBlockContent(save: FALSE) + ->setRevisionCreationTime((new \DateTimeImmutable('11 January 2009 4pm'))->getTimestamp()) + ->setRevisionTranslationAffected(TRUE); + $entity->setNewRevision(); + $entity->save(); + $revisionId = $entity->getRevisionId(); + + // Cannot delete latest revision. + $this->drupalGet($entity->toUrl('revision-delete-form')); + $this->assertSession()->statusCodeEquals(403); + + // Create a new non default revision. + $entity + ->setRevisionCreationTime((new \DateTimeImmutable('11 January 2009 5pm'))->getTimestamp()) + ->setRevisionTranslationAffected(TRUE) + ->setNewRevision(); + $entity->isDefaultRevision(FALSE); + $entity->save(); + $nonDefaultRevisionId = $entity->getRevisionId(); + + // Reload the default entity. + $revision = \Drupal::entityTypeManager()->getStorage('block_content') + ->loadRevision($revisionId); + // Cannot delete default revision. + $this->drupalGet($revision->toUrl('revision-delete-form')); + $this->assertSession()->statusCodeEquals(403); + $this->assertFalse($revision->access('delete revision', $this->adminUser, FALSE)); + + // Reload the non default entity. + $revision2 = \Drupal::entityTypeManager()->getStorage('block_content') + ->loadRevision($nonDefaultRevisionId); + $this->drupalGet($revision2->toUrl('revision-delete-form')); + $this->assertSession()->pageTextContains('Are you sure you want to delete the revision from Sun, 01/11/2009 - 17:00?'); + $this->assertSession()->buttonExists('Delete'); + $this->assertSession()->linkExists('Cancel'); + $this->assertTrue($revision2->access('delete revision', $this->adminUser, FALSE)); + + $countRevisions = static function (): int { + return (int) \Drupal::entityTypeManager()->getStorage('block_content') + ->getQuery() + ->accessCheck(FALSE) + ->allRevisions() + ->count() + ->execute(); + }; + + $count = $countRevisions(); + $this->submitForm([], 'Delete'); + $this->assertEquals($count - 1, $countRevisions()); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->addressEquals(sprintf('admin/content/block/%s/revisions', $entity->id())); + $this->assertSession()->pageTextContains(sprintf('Revision from Sun, 01/11/2009 - 17:00 of basic %s has been deleted.', $entity->label())); + $this->assertSession()->elementsCount('css', 'table tbody tr', 1); + } + +} diff --git a/tests/src/Functional/BlockContentRevisionRevertTest.php b/tests/src/Functional/BlockContentRevisionRevertTest.php new file mode 100644 index 0000000..8b6732f --- /dev/null +++ b/tests/src/Functional/BlockContentRevisionRevertTest.php @@ -0,0 +1,98 @@ +drupalLogin($this->adminUser); + $this->drupalPlaceBlock('page_title_block'); + } + + /** + * Tests revision revert. + */ + public function testRevertForm(): void { + $entity = $this->createBlockContent(save: FALSE) + ->setRevisionCreationTime((new \DateTimeImmutable('11 January 2009 4pm'))->getTimestamp()) + ->setRevisionTranslationAffected(TRUE); + $entity->setNewRevision(); + $entity->save(); + $revisionId = $entity->getRevisionId(); + + // Cannot revert latest revision. + $this->drupalGet($entity->toUrl('revision-revert-form')); + $this->assertSession()->statusCodeEquals(403); + + // Create a new non default revision. + $entity + ->setRevisionCreationTime((new \DateTimeImmutable('11 January 2009 5pm'))->getTimestamp()) + ->setRevisionTranslationAffected(TRUE) + ->setNewRevision(); + $entity->isDefaultRevision(FALSE); + $entity->save(); + $nonDefaultRevisionId = $entity->getRevisionId(); + + // Reload the default entity. + $revision = \Drupal::entityTypeManager()->getStorage('block_content') + ->loadRevision($revisionId); + // Cannot revert default revision. + $this->drupalGet($revision->toUrl('revision-revert-form')); + $this->assertSession()->statusCodeEquals(403); + $this->assertFalse($revision->access('revert', $this->adminUser, FALSE)); + + // Reload the non default entity. + $revision2 = \Drupal::entityTypeManager()->getStorage('block_content') + ->loadRevision($nonDefaultRevisionId); + $this->drupalGet($revision2->toUrl('revision-revert-form')); + $this->assertSession()->pageTextContains('Are you sure you want to revert to the revision from Sun, 01/11/2009 - 17:00?'); + $this->assertSession()->buttonExists('Revert'); + $this->assertSession()->linkExists('Cancel'); + $this->assertTrue($revision2->access('revert', $this->adminUser, FALSE)); + + $countRevisions = static function (): int { + return (int) \Drupal::entityTypeManager()->getStorage('block_content') + ->getQuery() + ->accessCheck(FALSE) + ->allRevisions() + ->count() + ->execute(); + }; + + $count = $countRevisions(); + $this->submitForm([], 'Revert'); + $this->assertEquals($count + 1, $countRevisions()); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->addressEquals(sprintf('admin/content/block/%s/revisions', $entity->id())); + $this->assertSession()->pageTextContains(sprintf('basic %s has been reverted to the revision from Sun, 01/11/2009 - 17:00.', $entity->label())); + // Three rows, from the top: the newly reverted revision, the revision from + // 5pm, and the revision from 4pm. + $this->assertSession()->elementsCount('css', 'table tbody tr', 3); + } + +} diff --git a/tests/src/Functional/BlockContentRevisionVersionHistoryTest.php b/tests/src/Functional/BlockContentRevisionVersionHistoryTest.php new file mode 100644 index 0000000..a3239c8 --- /dev/null +++ b/tests/src/Functional/BlockContentRevisionVersionHistoryTest.php @@ -0,0 +1,94 @@ +drupalLogin($this->adminUser); + } + + /** + * Tests version history page. + */ + public function testVersionHistory(): void { + $entity = $this->createBlockContent(save: FALSE); + + $entity + ->setInfo('first revision') + ->setRevisionCreationTime((new \DateTimeImmutable('1st June 2020 7am'))->getTimestamp()) + ->setRevisionLogMessage('first revision log') + ->setRevisionUser($this->drupalCreateUser(name: 'first author')) + ->setNewRevision(); + $entity->save(); + + $entity + ->setInfo('second revision') + ->setRevisionCreationTime((new \DateTimeImmutable('2nd June 2020 8am'))->getTimestamp()) + ->setRevisionLogMessage('second revision log') + ->setRevisionUser($this->drupalCreateUser(name: 'second author')) + ->setNewRevision(); + $entity->save(); + + $entity + ->setInfo('third revision') + ->setRevisionCreationTime((new \DateTimeImmutable('3rd June 2020 9am'))->getTimestamp()) + ->setRevisionLogMessage('third revision log') + ->setRevisionUser($this->drupalCreateUser(name: 'third author')) + ->setNewRevision(); + $entity->save(); + + $this->drupalGet($entity->toUrl('version-history')); + $this->assertSession()->elementsCount('css', 'table tbody tr', 3); + + // Order is newest to oldest revision by creation order. + $row1 = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(1)'); + // Latest revision does not have revert or delete revision operation. + $this->assertSession()->elementNotExists('named', ['link', 'Revert'], $row1); + $this->assertSession()->elementNotExists('named', ['link', 'Delete'], $row1); + $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', 'Current revision'); + $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', 'third revision log'); + $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(1)', '06/03/2020 - 09:00 by third author'); + + $row2 = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(2)'); + $this->assertSession()->elementExists('named', ['link', 'Revert'], $row2); + $this->assertSession()->elementExists('named', ['link', 'Delete'], $row2); + $this->assertSession()->elementTextNotContains('css', 'table tbody tr:nth-child(2)', 'Current revision'); + $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(2)', 'second revision log'); + $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(2)', '06/02/2020 - 08:00 by second author'); + + $row3 = $this->assertSession()->elementExists('css', 'table tbody tr:nth-child(3)'); + $this->assertSession()->elementExists('named', ['link', 'Revert'], $row3); + $this->assertSession()->elementExists('named', ['link', 'Delete'], $row3); + $this->assertSession()->elementTextNotContains('css', 'table tbody tr:nth-child(2)', 'Current revision'); + $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(3)', 'first revision log'); + $this->assertSession()->elementTextContains('css', 'table tbody tr:nth-child(3)', '06/01/2020 - 07:00 by first author'); + } + +} diff --git a/tests/src/Functional/BlockContentRevisionsTest.php b/tests/src/Functional/BlockContentRevisionsTest.php new file mode 100644 index 0000000..a58d770 --- /dev/null +++ b/tests/src/Functional/BlockContentRevisionsTest.php @@ -0,0 +1,111 @@ +createBlockContent('initial'); + + $blocks = []; + $logs = []; + + // Get original block. + $blocks[] = $block->getRevisionId(); + $logs[] = ''; + + // Create three revisions. + $revision_count = 3; + for ($i = 0; $i < $revision_count; $i++) { + $block->setNewRevision(TRUE); + $block->setRevisionLogMessage($this->randomMachineName(32)); + $block->setRevisionUser($this->adminUser); + $block->setRevisionCreationTime(time()); + $logs[] = $block->getRevisionLogMessage(); + $block->save(); + $blocks[] = $block->getRevisionId(); + } + + $this->blocks = $blocks; + $this->revisionLogs = $logs; + } + + /** + * Checks block revision related operations. + */ + public function testRevisions(): void { + $blocks = $this->blocks; + $logs = $this->revisionLogs; + + foreach ($blocks as $delta => $revision_id) { + // Confirm the correct revision text appears. + /** @var \Drupal\block_content\BlockContentInterface $loaded */ + $loaded = $this->container->get('entity_type.manager') + ->getStorage('block_content') + ->loadRevision($revision_id); + // Verify revision log is the same. + $this->assertEquals($logs[$delta], $loaded->getRevisionLogMessage(), "Correct log message found for revision $revision_id"); + if ($delta > 0) { + $this->assertInstanceOf(UserInterface::class, $loaded->getRevisionUser()); + $this->assertIsNumeric($loaded->getRevisionUserId()); + $this->assertIsNumeric($loaded->getRevisionCreationTime()); + } + } + + // Confirm that this is the default revision. + $this->assertTrue($loaded->isDefaultRevision(), 'Third block revision is the default one.'); + + // Make a new revision and set it to not be default. + // This will create a new revision that is not "front facing". + // Save this as a non-default revision. + $loaded->setNewRevision(); + $loaded->isDefaultRevision(FALSE); + $loaded->body = $this->randomMachineName(8); + $loaded->save(); + + // Confirm that revision body text is not present on default version of + // block. + $this->drupalGet('admin/content/block/' . $loaded->id()); + $this->assertSession()->pageTextNotContains($loaded->body->value); + + // Verify that the non-default revision id is greater than the default + // revision id. + $default_revision = BlockContent::load($loaded->id()); + // Verify that the revision ID is greater than the default revision ID. + $this->assertGreaterThan($default_revision->getRevisionId(), $loaded->getRevisionId()); + } + +} diff --git a/tests/src/Functional/BlockContentSaveTest.php b/tests/src/Functional/BlockContentSaveTest.php new file mode 100644 index 0000000..7cdaddf --- /dev/null +++ b/tests/src/Functional/BlockContentSaveTest.php @@ -0,0 +1,112 @@ +save() for saving content. + * + * @group block_content + */ +class BlockContentSaveTest extends BlockContentTestBase { + + /** + * {@inheritdoc} + */ + protected static $modules = ['block_content_test']; + + /** + * {@inheritdoc} + */ + protected $defaultTheme = 'stark'; + + /** + * Sets the test up. + */ + protected function setUp(): void { + parent::setUp(); + + $this->drupalLogin($this->adminUser); + } + + /** + * Checks whether content block IDs are saved properly during an import. + */ + public function testImport(): void { + // Content block ID must be a number that is not in the database. + $max_id = (int) \Drupal::entityQueryAggregate('block_content') + ->accessCheck(FALSE) + ->aggregate('id', 'max') + ->execute()[0]['id_max']; + $test_id = $max_id + mt_rand(1000, 1000000); + $info = $this->randomMachineName(8); + $block_array = [ + 'info' => $info, + 'body' => ['value' => $this->randomMachineName(32)], + 'type' => 'basic', + 'id' => $test_id, + ]; + $block = BlockContent::create($block_array); + $block->enforceIsNew(TRUE); + $block->save(); + + // Verify that block_submit did not wipe the provided id. + $this->assertEquals($test_id, $block->id(), 'Block imported using provide id'); + + // Test the import saved. + $block_by_id = BlockContent::load($test_id); + $this->assertNotEmpty($block_by_id, 'Content block load by block ID.'); + $this->assertSame($block_array['body']['value'], $block_by_id->body->value); + } + + /** + * Tests determining changes in hook_block_presave(). + * + * Verifies the static block load cache is cleared upon save. + */ + public function testDeterminingChanges(): void { + // Initial creation. + $block = $this->createBlockContent('test_changes'); + // Creating a block should set the changed date to the current time + // which is always greater than the time set by hooks we're testing. + $this->assertGreaterThan(979534800, $block->getChangedTime(), 'Creating a block sets default "changed" timestamp.'); + + // Update the block without applying changes. + $block->save(); + $this->assertEquals('test_changes', $block->label(), 'No changes have been determined.'); + + // Apply changes. + $block->setInfo('updated'); + $block->save(); + + // The hook implementations block_content_test_block_content_presave() and + // block_content_test_block_content_update() determine changes and change + // the title as well as programmatically set the 'changed' timestamp. + $this->assertEquals('updated_presave_update', $block->label(), 'Changes have been determined.'); + $this->assertEquals(979534800, $block->getChangedTime(), 'Saving a content block uses "changed" timestamp set in presave hook.'); + + // Test the static block load cache to be cleared. + $block = BlockContent::load($block->id()); + $this->assertEquals('updated_presave', $block->label(), 'Static cache has been cleared.'); + } + + /** + * Tests saving a block on block insert. + * + * This test ensures that a block has been fully saved when + * hook_block_content_insert() is invoked, so that the block can be saved again + * in a hook implementation without errors. + * + * @see block_test_block_insert() + */ + public function testBlockContentSaveOnInsert(): void { + // block_content_test_block_content_insert() triggers a save on insert if the + // title equals 'new'. + $block = $this->createBlockContent('new'); + $this->assertEquals('BlockContent ' . $block->id(), $block->label(), 'Content block saved on block insert.'); + } + +} diff --git a/tests/src/Functional/BlockContentTestBase.php b/tests/src/Functional/BlockContentTestBase.php new file mode 100644 index 0000000..7990ff4 --- /dev/null +++ b/tests/src/Functional/BlockContentTestBase.php @@ -0,0 +1,138 @@ +autoCreateBasicBlockType) { + $this->createBlockContentType('basic', TRUE); + } + + $this->adminUser = $this->drupalCreateUser($this->permissions); + $this->drupalPlaceBlock('local_actions_block'); + } + + /** + * Creates a content block. + * + * @param bool|string $title + * (optional) Title of block. When no value is given uses a random name. + * Defaults to FALSE. + * @param string $bundle + * (optional) Bundle name. Defaults to 'basic'. + * @param bool $save + * (optional) Whether to save the block. Defaults to TRUE. + * + * @return \Drupal\block_content\Entity\BlockContent + * Created content block. + */ + protected function createBlockContent($title = FALSE, $bundle = 'basic', $save = TRUE) { + $title = $title ?: $this->randomMachineName(); + $block_content = BlockContent::create([ + 'info' => $title, + 'type' => $bundle, + 'langcode' => 'en', + ]); + if ($block_content && $save === TRUE) { + $block_content->save(); + } + return $block_content; + } + + /** + * Creates a block type (bundle). + * + * @param array|string $values + * The value to create the block content type. If $values is an array + * it should be like: ['id' => 'foo', 'label' => 'Foo']. If $values + * is a string, it will be considered that it represents the label. + * @param bool $create_body + * Whether or not to create the body field + * + * @return \Drupal\block_content\Entity\BlockContentType + * Created block type. + */ + protected function createBlockContentType($values, $create_body = FALSE) { + if (is_array($values)) { + if (!isset($values['id'])) { + do { + $id = $this->randomMachineName(8); + } while (BlockContentType::load($id)); + } + else { + $id = $values['id']; + } + $values += [ + 'id' => $id, + 'label' => $id, + 'revision' => FALSE, + ]; + $bundle = BlockContentType::create($values); + } + else { + $bundle = BlockContentType::create([ + 'id' => $values, + 'label' => $values, + 'revision' => FALSE, + ]); + } + $bundle->save(); + if ($create_body) { + block_content_add_body_field($bundle->id()); + } + return $bundle; + } + +} diff --git a/tests/src/Functional/BlockContentTranslationUITest.php b/tests/src/Functional/BlockContentTranslationUITest.php new file mode 100644 index 0000000..8685166 --- /dev/null +++ b/tests/src/Functional/BlockContentTranslationUITest.php @@ -0,0 +1,158 @@ +entityTypeId = 'block_content'; + $this->bundle = 'basic'; + $this->testLanguageSelector = FALSE; + parent::setUp(); + $this->doSetup(); + + $this->drupalPlaceBlock('page_title_block'); + } + + /** + * {@inheritdoc} + */ + protected function setupBundle() { + // Create the basic bundle since it is provided by standard. + $bundle = BlockContentType::create([ + 'id' => $this->bundle, + 'label' => $this->bundle, + 'revision' => FALSE, + ]); + $bundle->save(); + } + + /** + * {@inheritdoc} + */ + public function getTranslatorPermissions() { + return array_merge(parent::getTranslatorPermissions(), [ + 'translate any entity', + 'access administration pages', + 'administer blocks', + 'administer block_content fields', + 'access block library', + 'create basic block content', + 'edit any basic block content', + 'delete any basic block content', + ]); + } + + /** + * {@inheritdoc} + */ + protected function getNewEntityValues($langcode) { + return ['info' => $this->randomMachineName()] + parent::getNewEntityValues($langcode); + } + + /** + * Returns an edit array containing the values to be posted. + */ + protected function getEditValues($values, $langcode, $new = FALSE) { + $edit = parent::getEditValues($values, $langcode, $new); + foreach ($edit as $property => $value) { + if ($property == 'info') { + $edit['info[0][value]'] = $value; + unset($edit[$property]); + } + } + return $edit; + } + + /** + * {@inheritdoc} + */ + protected function doTestBasicTranslation() { + parent::doTestBasicTranslation(); + + // Ensure that a block translation can be created using the same description + // as in the original language. + $default_langcode = $this->langcodes[0]; + $values = $this->getNewEntityValues($default_langcode); + $storage = \Drupal::entityTypeManager()->getStorage($this->entityTypeId); + /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */ + $entity = $storage->create(['type' => 'basic'] + $values); + $entity->save(); + $entity->addTranslation('it', $values); + + try { + $entity->save(); + } + catch (\Exception $e) { + $this->fail('Blocks can have translations with the same "info" value.'); + } + + // Check that the translate operation link is shown. + $this->drupalGet('admin/content/block'); + $this->assertSession()->linkByHrefExists('admin/content/block/' . $entity->id() . '/translations'); + } + + /** + * {@inheritdoc} + */ + protected function doTestTranslationEdit() { + $storage = $this->container->get('entity_type.manager') + ->getStorage($this->entityTypeId); + $storage->resetCache([$this->entityId]); + $entity = $storage->load($this->entityId); + $languages = $this->container->get('language_manager')->getLanguages(); + + foreach ($this->langcodes as $langcode) { + // We only want to test the title for non-english translations. + if ($langcode != 'en') { + $options = ['language' => $languages[$langcode]]; + $url = $entity->toUrl('edit-form', $options); + $this->drupalGet($url); + $this->assertSession()->pageTextContains("Edit {$entity->bundle()} {$entity->getTranslation($langcode)->label()} [{$languages[$langcode]->getName()} translation]"); + } + } + } + +} diff --git a/tests/src/Functional/BlockContentTypeTest.php b/tests/src/Functional/BlockContentTypeTest.php new file mode 100644 index 0000000..274bb1b --- /dev/null +++ b/tests/src/Functional/BlockContentTypeTest.php @@ -0,0 +1,266 @@ +drupalPlaceBlock('page_title_block'); + } + + /** + * Tests the order of the block content types on the add page. + */ + public function testBlockContentAddPageOrder(): void { + $this->createBlockContentType(['id' => 'bundle_1', 'label' => 'Bundle 1']); + $this->createBlockContentType(['id' => 'bundle_2', 'label' => 'Aaa Bundle 2']); + $this->drupalLogin($this->adminUser); + $this->drupalGet('block/add'); + $this->assertSession()->pageTextMatches('/Aaa Bundle 2(.*)Bundle 1/'); + } + + /** + * Tests creating a block type programmatically and via a form. + */ + public function testBlockContentTypeCreation(): void { + // Log in a test user. + $this->drupalLogin($this->adminUser); + + // Test the page with no block-types. + $this->drupalGet('block/add'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextContains('You have not created any block types yet'); + $this->clickLink('block type creation page'); + + // Create a block type via the user interface. + $edit = [ + 'id' => 'foo', + 'label' => 'title for foo', + ]; + $this->submitForm($edit, 'Save and manage fields'); + + // Asserts that form submit redirects to the expected manage fields page. + $this->assertSession()->addressEquals('admin/structure/block-content/manage/' . $edit['id'] . '/fields'); + + $block_type = BlockContentType::load('foo'); + $this->assertInstanceOf(BlockContentType::class, $block_type); + + $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions('block_content', 'foo'); + $this->assertTrue(isset($field_definitions['body']), 'Body field created when using the UI to create block content types.'); + + // Check that the block type was created in site default language. + $default_langcode = \Drupal::languageManager()->getDefaultLanguage()->getId(); + $this->assertEquals($block_type->language()->getId(), $default_langcode); + + // Create block types programmatically. + $this->createBlockContentType('basic', TRUE); + $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions('block_content', 'basic'); + $this->assertTrue(isset($field_definitions['body']), "Body field for 'basic' block type created when using the testing API to create block content types."); + + $this->createBlockContentType('other'); + $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions('block_content', 'other'); + $this->assertFalse(isset($field_definitions['body']), "Body field for 'other' block type not created when using the testing API to create block content types."); + + $block_type = BlockContentType::load('other'); + $this->assertInstanceOf(BlockContentType::class, $block_type); + + $this->drupalGet('block/add/' . $block_type->id()); + $this->assertSession()->statusCodeEquals(200); + } + + /** + * Tests editing a block type using the UI. + */ + public function testBlockContentTypeEditing(): void { + $this->drupalPlaceBlock('system_breadcrumb_block'); + // Now create an initial block-type. + $this->createBlockContentType('basic', TRUE); + + $this->drupalLogin($this->adminUser); + // We need two block types to prevent /block/add redirecting. + $this->createBlockContentType('other'); + + $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions('block_content', 'other'); + $this->assertFalse(isset($field_definitions['body']), 'Body field was not created when using the API to create block content types.'); + + // Verify that title and body fields are displayed. + $this->drupalGet('block/add/basic'); + $this->assertSession()->pageTextContains('Block description'); + $this->assertNotEmpty($this->cssSelect('#edit-body-0-value'), 'Body field was found.'); + + // Change the block type name. + $edit = [ + 'label' => 'Bar', + ]; + $this->drupalGet('admin/structure/block-content/manage/basic'); + $this->assertSession()->titleEquals('Edit basic block type | Drupal'); + $this->submitForm($edit, 'Save'); + $front_page_path = Url::fromRoute('')->toString(); + $this->assertBreadcrumb('admin/structure/block-content/manage/basic/fields', [ + $front_page_path => 'Home', + 'admin/structure/block-content' => 'Block types', + 'admin/structure/block-content/manage/basic' => 'Edit Bar', + ]); + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + + $this->drupalGet('block/add'); + $this->assertSession()->pageTextContains('Bar'); + $this->clickLink('Bar'); + // Verify that the original machine name was used in the URL. + $this->assertSession()->addressEquals(Url::fromRoute('block_content.add_form', ['block_content_type' => 'basic'])); + + // Remove the body field. + $this->drupalGet('admin/structure/block-content/manage/basic/fields/block_content.basic.body/delete'); + $this->submitForm([], 'Delete'); + // Resave the settings for this type. + $this->drupalGet('admin/structure/block-content/manage/basic'); + $this->submitForm([], 'Save'); + // Check that the body field doesn't exist. + $this->drupalGet('block/add/basic'); + $this->assertEmpty($this->cssSelect('#edit-body-0-value'), 'Body field was not found.'); + } + + /** + * Tests deleting a block type that still has content. + */ + public function testBlockContentTypeDeletion(): void { + // Now create an initial block-type. + $this->createBlockContentType('basic', TRUE); + + // Create a block type programmatically. + $type = $this->createBlockContentType('foo'); + + $this->drupalLogin($this->adminUser); + + // Add a new block of this type. + $block = $this->createBlockContent(FALSE, 'foo'); + // Attempt to delete the block type, which should not be allowed. + $this->drupalGet('admin/structure/block-content/manage/' . $type->id() . '/delete'); + $this->assertSession()->pageTextContains($type->label() . ' is used by 1 content block on your site. You can not remove this block type until you have removed all of the ' . $type->label() . ' blocks.'); + $this->assertSession()->pageTextNotContains('This action cannot be undone.'); + + // Delete the block. + $block->delete(); + // Attempt to delete the block type, which should now be allowed. + $this->drupalGet('admin/structure/block-content/manage/' . $type->id() . '/delete'); + $this->assertSession()->pageTextContains('Are you sure you want to delete the block type ' . $type->id() . '?'); + $this->assertSession()->pageTextContains('This action cannot be undone.'); + } + + /** + * Tests that redirects work as expected when multiple block types exist. + */ + public function testsBlockContentAddTypes(): void { + // Now create an initial block-type. + $this->createBlockContentType('basic', TRUE); + + $this->drupalLogin($this->adminUser); + // Create two block types programmatically. + $this->createBlockContentType('foo'); + $this->createBlockContentType('bar'); + + // Get the content block storage. + $storage = $this->container + ->get('entity_type.manager') + ->getStorage('block_content'); + + // Install all themes. + $themes = ['olivero', 'stark', 'claro']; + \Drupal::service('theme_installer')->install($themes); + $theme_settings = $this->config('system.theme'); + foreach ($themes as $default_theme) { + // Change the default theme. + $theme_settings->set('default', $default_theme)->save(); + $this->drupalPlaceBlock('local_actions_block'); + + // For each installed theme, go to its block page and test the redirects. + foreach ($themes as $theme) { + // Test that adding a block from the 'place blocks' form sends you to the + // block configure form. + $path = $theme == $default_theme ? 'admin/structure/block' : "admin/structure/block/list/$theme"; + $this->drupalGet($path); + $this->clickLink('Place block'); + $this->clickLink('Add content block'); + $this->clickLink('foo'); + // Create a new block. + $edit = ['info[0][value]' => $this->randomMachineName(8)]; + $this->submitForm($edit, 'Save and configure'); + $blocks = $storage->loadByProperties(['info' => $edit['info[0][value]']]); + if (!empty($blocks)) { + $block = reset($blocks); + $this->assertSession()->addressEquals(Url::fromRoute('block.admin_add', ['plugin_id' => 'block_content:' . $block->uuid(), 'theme' => $theme])); + $this->submitForm(['region' => 'content'], 'Save block'); + $this->assertSession()->addressEquals(Url::fromRoute('block.admin_display_theme', ['theme' => $theme], ['query' => ['block-placement' => $theme . '-' . Html::getClass($edit['info[0][value]'])]])); + } + else { + $this->fail('Could not load created block.'); + } + } + } + + // Test that adding a block from the 'content blocks list' doesn't send you + // to the block configure form. + $this->drupalGet('admin/content/block'); + $this->clickLink('Add content block'); + $this->clickLink('foo'); + $edit = ['info[0][value]' => $this->randomMachineName(8)]; + $this->submitForm($edit, 'Save'); + $blocks = $storage->loadByProperties(['info' => $edit['info[0][value]']]); + if (!empty($blocks)) { + $this->assertSession()->addressEquals(Url::fromRoute('entity.block_content.collection')); + } + else { + $this->fail('Could not load created block.'); + } + } + +} diff --git a/tests/src/Functional/GenericTest.php b/tests/src/Functional/GenericTest.php new file mode 100644 index 0000000..b13ce02 --- /dev/null +++ b/tests/src/Functional/GenericTest.php @@ -0,0 +1,14 @@ +drupalPlaceBlock('page_title_block'); + $this->drupalPlaceBlock('system_breadcrumb_block'); + } + + /** + * Checks block edit functionality. + */ + public function testPageEdit(): void { + $this->drupalLogin($this->adminUser); + + $title_key = 'info[0][value]'; + $body_key = 'body[0][value]'; + // Create block to edit. + $edit = []; + $edit['info[0][value]'] = $this->randomMachineName(8); + $edit[$body_key] = $this->randomMachineName(16); + $this->drupalGet('block/add/basic'); + $this->submitForm($edit, 'Save'); + + // Check that the block exists in the database. + $blocks = \Drupal::entityQuery('block_content') + ->accessCheck(FALSE) + ->condition('info', $edit['info[0][value]']) + ->execute(); + $block = BlockContent::load(reset($blocks)); + $this->assertNotEmpty($block, 'Content block found in database.'); + + // Load the edit page. + $this->drupalGet('admin/content/block/' . $block->id()); + $this->assertSession()->fieldValueEquals($title_key, $edit[$title_key]); + $this->assertSession()->fieldValueEquals($body_key, $edit[$body_key]); + + // Edit the content of the block. + $edit = []; + $edit[$title_key] = $this->randomMachineName(8); + $edit[$body_key] = $this->randomMachineName(16); + // Stay on the current page, without reloading. + $this->submitForm($edit, 'Save'); + + // Edit the same block, creating a new revision. + $this->drupalGet("admin/content/block/" . $block->id()); + $edit = []; + $edit['info[0][value]'] = $this->randomMachineName(8); + $edit[$body_key] = $this->randomMachineName(16); + $edit['revision'] = TRUE; + $this->submitForm($edit, 'Save'); + + // Ensure that the block revision has been created. + \Drupal::entityTypeManager()->getStorage('block_content')->resetCache([$block->id()]); + $revised_block = BlockContent::load($block->id()); + $this->assertNotSame($block->getRevisionId(), $revised_block->getRevisionId(), 'A new revision has been created.'); + + // Test deleting the block. + $this->drupalGet("admin/content/block/" . $revised_block->id()); + $this->clickLink('Delete'); + $this->assertSession()->pageTextContains('Are you sure you want to delete the content block ' . $revised_block->label() . '?'); + + // Test breadcrumb. + $trail = [ + '' => 'Home', + 'admin/content/block' => 'Content blocks', + 'admin/content/block/' . $revised_block->id() => $revised_block->label(), + ]; + $this->assertBreadcrumb( + 'admin/content/block/' . $revised_block->id() . '/delete', $trail + ); + } + +} diff --git a/tests/src/Functional/Rest/BlockContentJsonAnonTest.php b/tests/src/Functional/Rest/BlockContentJsonAnonTest.php new file mode 100644 index 0000000..d20f21d --- /dev/null +++ b/tests/src/Functional/Rest/BlockContentJsonAnonTest.php @@ -0,0 +1,31 @@ + NULL, + ]; + + /** + * @var \Drupal\block_content\BlockContentInterface + */ + protected $entity; + + /** + * {@inheritdoc} + */ + protected function setUpAuthorization($method) { + switch ($method) { + case 'GET': + case 'PATCH': + $this->grantPermissionsToTestedRole(['access block library', 'edit any basic block content']); + break; + + case 'POST': + $this->grantPermissionsToTestedRole(['access block library', 'create basic block content']); + break; + + case 'DELETE': + $this->grantPermissionsToTestedRole(['delete any basic block content']); + break; + + default: + $this->grantPermissionsToTestedRole(['administer block content']); + break; + } + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + if (!BlockContentType::load('basic')) { + $block_content_type = BlockContentType::create([ + 'id' => 'basic', + 'label' => 'basic', + 'revision' => TRUE, + ]); + $block_content_type->save(); + block_content_add_body_field($block_content_type->id()); + } + + // Create a "Llama" content block. + $block_content = BlockContent::create([ + 'info' => 'Llama', + 'type' => 'basic', + 'body' => [ + 'value' => 'The name "llama" was adopted by European settlers from native Peruvians.', + 'format' => 'plain_text', + ], + ]) + ->setUnpublished(); + $block_content->save(); + return $block_content; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + return [ + 'id' => [ + [ + 'value' => 1, + ], + ], + 'uuid' => [ + [ + 'value' => $this->entity->uuid(), + ], + ], + 'langcode' => [ + [ + 'value' => 'en', + ], + ], + 'reusable' => [ + [ + 'value' => TRUE, + ], + ], + 'type' => [ + [ + 'target_id' => 'basic', + 'target_type' => 'block_content_type', + 'target_uuid' => BlockContentType::load('basic')->uuid(), + ], + ], + 'info' => [ + [ + 'value' => 'Llama', + ], + ], + 'revision_log' => [], + 'changed' => [ + [ + 'value' => (new \DateTime())->setTimestamp((int) $this->entity->getChangedTime()) + ->setTimezone(new \DateTimeZone('UTC')) + ->format(\DateTime::RFC3339), + 'format' => \DateTime::RFC3339, + ], + ], + 'revision_id' => [ + [ + 'value' => 1, + ], + ], + 'revision_created' => [ + [ + 'value' => (new \DateTime())->setTimestamp((int) $this->entity->getRevisionCreationTime()) + ->setTimezone(new \DateTimeZone('UTC')) + ->format(\DateTime::RFC3339), + 'format' => \DateTime::RFC3339, + ], + ], + 'revision_user' => [], + 'revision_translation_affected' => [ + [ + 'value' => TRUE, + ], + ], + 'default_langcode' => [ + [ + 'value' => TRUE, + ], + ], + 'body' => [ + [ + 'value' => 'The name "llama" was adopted by European settlers from native Peruvians.', + 'format' => 'plain_text', + 'summary' => NULL, + 'processed' => "

The name "llama" was adopted by European settlers from native Peruvians.

\n", + ], + ], + 'status' => [ + [ + 'value' => FALSE, + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + return [ + 'type' => [ + [ + 'target_id' => 'basic', + ], + ], + 'info' => [ + [ + 'value' => 'Drama llama', + ], + ], + ]; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedUnauthorizedAccessMessage($method) { + if (!$this->resourceConfigStorage->load(static::$resourceConfigId)) { + return match ($method) { + 'GET', 'PATCH' => "The 'edit any basic block content' permission is required.", + 'POST' => "The following permissions are required: 'create basic block content' AND 'access block library'.", + 'DELETE' => "The 'delete any basic block content' permission is required.", + default => parent::getExpectedUnauthorizedAccessMessage($method), + }; + } + return match ($method) { + 'GET' => "The 'access block library' permission is required.", + 'PATCH' => "The 'edit any basic block content' permission is required.", + 'POST' => "The following permissions are required: 'create basic block content' AND 'access block library'.", + 'DELETE' => "The 'delete any basic block content' permission is required.", + default => parent::getExpectedUnauthorizedAccessMessage($method), + }; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) { + // @see \Drupal\block_content\BlockContentAccessControlHandler() + return parent::getExpectedUnauthorizedEntityAccessCacheability($is_authenticated) + ->addCacheTags(['block_content:1']); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text']); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return Cache::mergeContexts(['url.site'], $this->container->getParameter('renderer.config')['required_cache_contexts']); + } + +} diff --git a/tests/src/Functional/Rest/BlockContentTypeJsonAnonTest.php b/tests/src/Functional/Rest/BlockContentTypeJsonAnonTest.php new file mode 100644 index 0000000..7b32797 --- /dev/null +++ b/tests/src/Functional/Rest/BlockContentTypeJsonAnonTest.php @@ -0,0 +1,31 @@ +grantPermissionsToTestedRole(['administer block types']); + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $block_content_type = BlockContentType::create([ + 'id' => 'pascal', + 'label' => 'Pascal', + 'revision' => FALSE, + 'description' => 'Provides a competitive alternative to the "basic" type', + ]); + + $block_content_type->save(); + + return $block_content_type; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedNormalizedEntity() { + return [ + 'dependencies' => [], + 'description' => 'Provides a competitive alternative to the "basic" type', + 'id' => 'pascal', + 'label' => 'Pascal', + 'langcode' => 'en', + 'revision' => FALSE, + 'status' => TRUE, + 'uuid' => $this->entity->uuid(), + ]; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + // @todo Update in https://www.drupal.org/node/2300677. + return []; + } + +} diff --git a/tests/src/Functional/Rest/BlockContentTypeXmlAnonTest.php b/tests/src/Functional/Rest/BlockContentTypeXmlAnonTest.php new file mode 100644 index 0000000..ef5687d --- /dev/null +++ b/tests/src/Functional/Rest/BlockContentTypeXmlAnonTest.php @@ -0,0 +1,33 @@ + 'Test block', + 'type' => 'basic', + ]); + $block_content->save(); + + $block = $this->placeBlock('block_content:' . $block_content->uuid()); + + $this->drupalGet(''); + $page = $this->getSession()->getPage(); + $this->assertTrue($page->has('css', '#block-' . $block->id())); + + $block_content->setUnpublished(); + $block_content->save(); + + $this->drupalGet(''); + $page = $this->getSession()->getPage(); + $this->assertFalse($page->has('css', '#block-' . $block->id())); + } + +} diff --git a/tests/src/Functional/Update/BlockContentRemoveConstraint.php b/tests/src/Functional/Update/BlockContentRemoveConstraint.php new file mode 100644 index 0000000..4f70a73 --- /dev/null +++ b/tests/src/Functional/Update/BlockContentRemoveConstraint.php @@ -0,0 +1,71 @@ +entityDefinitionUpdateManager = \Drupal::entityDefinitionUpdateManager(); + } + + /** + * {@inheritdoc} + */ + protected function setDatabaseDumpFiles() { + $this->databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.4.0.filled.standard.php.gz', + ]; + } + + /** + * Tests the upgrade path for moderation state reindexing. + */ + public function testRunUpdates(): void { + $constraint = 'UniqueField'; + $constraints = $this->getFieldInfoConstraints(); + if (!isset($constraints[$constraint])) { + $constraints[$constraint] = []; + $field_storage_definition = $this->entityDefinitionUpdateManager->getFieldStorageDefinition('info', 'block_content'); + $field_storage_definition->setConstraints($constraints); + $this->entityDefinitionUpdateManager->updateFieldStorageDefinition($field_storage_definition); + } + + $this->assertCount(2, $this->getFieldInfoConstraints()); + + $this->runUpdates(); + + $this->assertCount(1, $this->getFieldInfoConstraints()); + } + + /** + * Get constraints for info field. + * + * @return array[] + * List of constraints. + */ + protected function getFieldInfoConstraints() { + $field_storage_definition = $this->entityDefinitionUpdateManager->getFieldStorageDefinition('info', 'block_content'); + return $field_storage_definition->getConstraints(); + } + +} diff --git a/tests/src/Functional/Update/BlockContentReusableIndexUpdatePathTest.php b/tests/src/Functional/Update/BlockContentReusableIndexUpdatePathTest.php new file mode 100644 index 0000000..f97d07f --- /dev/null +++ b/tests/src/Functional/Update/BlockContentReusableIndexUpdatePathTest.php @@ -0,0 +1,36 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.4.0.bare.standard.php.gz', + ]; + } + + /** + * Tests the upgrade path for Block Content reusable index. + */ + public function testRunUpdates(): void { + $connection = Database::getConnection(); + $this->assertFalse($connection->schema()->indexExists('block_content_field_data', 'block_content_field__reusable'), 'Block Content reusable index not yet added.'); + $this->runUpdates(); + $this->assertTrue($connection->schema()->indexExists('block_content_field_data', 'block_content_field__reusable'), 'Block Content reusable index has been added.'); + } + +} diff --git a/tests/src/Functional/Update/BlockContentUpdateTest.php b/tests/src/Functional/Update/BlockContentUpdateTest.php new file mode 100644 index 0000000..4fa00b2 --- /dev/null +++ b/tests/src/Functional/Update/BlockContentUpdateTest.php @@ -0,0 +1,128 @@ +databaseDumpFiles = [ + __DIR__ . '/../../../../../system/tests/fixtures/update/drupal-9.4.0.bare.standard.php.gz', + ]; + } + + /** + * Tests converting block types' `revision` flag to boolean. + */ + public function testConvertBlockContentTypeRevisionFlagToBoolean(): void { + $no_new_revisions = BlockContentType::create([ + 'id' => 'no_new_revisions', + 'label' => 'Does not create new revisions', + 'revision' => 0, + ]); + $no_new_revisions->trustData()->save(); + $new_revisions = BlockContentType::create([ + 'id' => 'new_revisions', + 'label' => 'Creates new revisions', + 'revision' => 1, + ]); + $new_revisions->trustData()->save(); + // Ensure that an integer was stored, so we can be sure that the update + // path converts it to a boolean. + $this->assertSame(0, $no_new_revisions->get('revision')); + $this->assertSame(1, $new_revisions->get('revision')); + + $this->runUpdates(); + $this->assertFalse(BlockContentType::load('no_new_revisions')->get('revision')); + $this->assertTrue(BlockContentType::load('new_revisions')->get('revision')); + } + + /** + * Tests moving the content block library to Content. + * + * @see block_content_post_update_move_custom_block_library() + */ + public function testMoveCustomBlockLibraryToContent(): void { + $user = $this->drupalCreateUser(['administer blocks']); + $this->drupalLogin($user); + $this->drupalGet('admin/structure/block/block-content'); + $this->assertSession()->statusCodeEquals(200); + $this->assertSession()->pageTextContains('Custom blocks'); + $this->assertSession()->pageTextContains('Custom block library'); + $this->drupalGet('admin/content/block'); + $this->assertSession()->statusCodeEquals(404); + + $this->runUpdates(); + + // Load and initialize the block_content view. + $view = View::load('block_content'); + $data = $view->toArray(); + // Check that the path, description, and menu options have been updated. + $this->assertEquals('admin/content/block', $data['display']['page_1']['display_options']['path']); + $this->assertEquals('Create and edit block content.', $data['display']['page_1']['display_options']['menu']['description']); + $this->assertFalse($data['display']['page_1']['display_options']['menu']['expanded']); + $this->assertEquals('system.admin_content', $data['display']['page_1']['display_options']['menu']['parent']); + $this->assertEquals('Content blocks', $view->label()); + $this->assertEquals('Blocks', $data['display']['page_1']['display_options']['menu']['title']); + + // Check the new path is accessible. + $user = $this->drupalCreateUser(['access block library']); + $this->drupalLogin($user); + $this->drupalGet('admin/content/block'); + $this->assertSession()->statusCodeEquals(200); + } + + /** + * Tests the block_content view isn't updated if the path has been modified. + * + * @see block_content_post_update_move_custom_block_library() + */ + public function testCustomBlockLibraryPathOverridden(): void { + $view = View::load('block_content'); + $display =& $view->getDisplay('page_1'); + $display['display_options']['path'] = 'some/custom/path'; + $view->save(); + + $this->runUpdates(); + + $view = View::load('block_content'); + $data = $view->toArray(); + $this->assertEquals('some/custom/path', $data['display']['page_1']['display_options']['path']); + } + + /** + * Tests the permissions are updated for users with "administer blocks". + * + * @see block_content_post_update_sort_permissions() + */ + public function testBlockLibraryPermissionsUpdate(): void { + $user = $this->drupalCreateUser(['administer blocks']); + $this->assertTrue($user->hasPermission('administer blocks')); + $this->assertFalse($user->hasPermission('administer block content')); + $this->assertFalse($user->hasPermission('administer block types')); + $this->assertFalse($user->hasPermission('access block library')); + + $this->runUpdates(); + + $user = User::load($user->id()); + $this->assertTrue($user->hasPermission('administer blocks')); + $this->assertTrue($user->hasPermission('administer block content')); + $this->assertTrue($user->hasPermission('administer block types')); + $this->assertTrue($user->hasPermission('access block library')); + } + +} diff --git a/tests/src/Functional/Views/BlockContentFieldFilterTest.php b/tests/src/Functional/Views/BlockContentFieldFilterTest.php new file mode 100644 index 0000000..674e999 --- /dev/null +++ b/tests/src/Functional/Views/BlockContentFieldFilterTest.php @@ -0,0 +1,117 @@ +save(); + ConfigurableLanguage::createFromLangcode('es')->save(); + + // Make the body field translatable. The info is already translatable by + // definition. + $field_storage = FieldStorageConfig::loadByName('block_content', 'body'); + $field_storage->setTranslatable(TRUE); + $field_storage->save(); + + // Set up block_content infos. + $this->blockContentInfos = [ + 'en' => 'Food in Paris', + 'es' => 'Comida en Paris', + 'fr' => 'Nourriture en Paris', + ]; + + // Create block_content with translations. + $block_content = $this->createBlockContent(['info' => $this->blockContentInfos['en'], 'langcode' => 'en', 'type' => 'basic', 'body' => [['value' => $this->blockContentInfos['en']]]]); + foreach (['es', 'fr'] as $langcode) { + $translation = $block_content->addTranslation($langcode, ['info' => $this->blockContentInfos[$langcode]]); + $translation->body->value = $this->blockContentInfos[$langcode]; + } + $block_content->save(); + } + + /** + * Tests body and info filters. + */ + public function testFilters(): void { + // Test the info filter page, which filters for info contains 'Comida'. + // Should show just the Spanish translation, once. + $this->assertPageCounts('test-info-filter', ['es' => 1, 'fr' => 0, 'en' => 0], 'Comida info filter'); + + // Test the body filter page, which filters for body contains 'Comida'. + // Should show just the Spanish translation, once. + $this->assertPageCounts('test-body-filter', ['es' => 1, 'fr' => 0, 'en' => 0], 'Comida body filter'); + + // Test the info Paris filter page, which filters for info contains + // 'Paris'. Should show each translation once. + $this->assertPageCounts('test-info-paris', ['es' => 1, 'fr' => 1, 'en' => 1], 'Paris info filter'); + + // Test the body Paris filter page, which filters for body contains + // 'Paris'. Should show each translation once. + $this->assertPageCounts('test-body-paris', ['es' => 1, 'fr' => 1, 'en' => 1], 'Paris body filter'); + } + + /** + * Asserts that the given block_content translation counts are correct. + * + * @param string $path + * Path of the page to test. + * @param array $counts + * Array whose keys are languages, and values are the number of times + * that translation should be shown on the given page. + * @param string $message + * Message suffix to display. + * + * @internal + */ + protected function assertPageCounts(string $path, array $counts, string $message): void { + // Get the text of the page. + $this->drupalGet($path); + $text = $this->getTextContent(); + + foreach ($counts as $langcode => $count) { + $this->assertEquals($count, substr_count($text, $this->blockContentInfos[$langcode]), 'Translation ' . $langcode . ' has count ' . $count . ' with ' . $message); + } + } + +} diff --git a/tests/src/Functional/Views/BlockContentIntegrationTest.php b/tests/src/Functional/Views/BlockContentIntegrationTest.php new file mode 100644 index 0000000..ec22ec4 --- /dev/null +++ b/tests/src/Functional/Views/BlockContentIntegrationTest.php @@ -0,0 +1,76 @@ +createBlockContentType(); + $types[] = $type; + + for ($j = 0; $j < 5; $j++) { + // Ensure the right order of the block_contents. + $block_content = $this->createBlockContent(['type' => $type->id()]); + $block_contents[$type->id()][$block_content->id()] = $block_content; + $all_ids[] = $block_content->id(); + } + } + + $this->drupalGet('test-block_content-view'); + $this->assertSession()->statusCodeEquals(404); + + $this->drupalGet('test-block_content-view/all'); + $this->assertSession()->statusCodeEquals(200); + $this->assertIds($all_ids); + /** @var \Drupal\block_content\Entity\BlockContentType[] $types*/ + foreach ($types as $type) { + $this->drupalGet("test-block_content-view/{$type->id()}"); + $this->assertIds(array_keys($block_contents[$type->id()])); + } + } + + /** + * Ensures that a list of block_contents appear on the page. + * + * @param array $expected_ids + * An array of block_content IDs. + * + * @internal + */ + protected function assertIds(array $expected_ids = []): void { + $result = $this->xpath('//span[@class="field-content"]'); + $ids = []; + foreach ($result as $element) { + $ids[] = $element->getText(); + } + $this->assertEquals($expected_ids, $ids); + } + +} diff --git a/tests/src/Functional/Views/BlockContentRedirectTest.php b/tests/src/Functional/Views/BlockContentRedirectTest.php new file mode 100644 index 0000000..83ddb8b --- /dev/null +++ b/tests/src/Functional/Views/BlockContentRedirectTest.php @@ -0,0 +1,55 @@ +drupalLogin($this->drupalCreateUser(['access block library', 'administer block content'])); + $this->drupalGet('admin/content/block'); + + // Create a content block. + $this->clickLink('content block'); + $edit = []; + $edit['info[0][value]'] = 'Test redirect destination'; + $edit['body[0][value]'] = $this->randomMachineName(16); + $this->submitForm($edit, 'Save'); + + // Check the block content is present in the view redirect destination. + $this->drupalGet('admin/content/redirect_destination'); + $this->assertSession()->pageTextContains('Test redirect destination'); + + // Edit the created block and save. + $this->clickLink('Edit'); + $this->submitForm([], 'Save'); + $this->assertSession()->addressEquals('admin/content/redirect_destination'); + } + +} diff --git a/tests/src/Functional/Views/BlockContentTestBase.php b/tests/src/Functional/Views/BlockContentTestBase.php new file mode 100644 index 0000000..1b8af11 --- /dev/null +++ b/tests/src/Functional/Views/BlockContentTestBase.php @@ -0,0 +1,109 @@ +createBlockContentType(['id' => 'basic']); + + $this->adminUser = $this->drupalCreateUser($this->permissions); + } + + /** + * Creates a content block. + * + * @param array $values + * (optional) The values for the block_content entity. + * + * @return \Drupal\block_content\Entity\BlockContent + * Created content block. + */ + protected function createBlockContent(array $values = []) { + $status = 0; + $values += [ + 'info' => $this->randomMachineName(), + 'type' => 'basic', + 'langcode' => 'en', + ]; + if ($block_content = BlockContent::create($values)) { + $status = $block_content->save(); + } + $this->assertEquals(SAVED_NEW, $status, "Created block content {$block_content->label()}."); + return $block_content; + } + + /** + * Creates a block type (bundle). + * + * @param array $values + * An array of settings to change from the defaults. + * + * @return \Drupal\block_content\Entity\BlockContentType + * Created block type. + */ + protected function createBlockContentType(array $values = []) { + // Find a non-existent random type name. + if (!isset($values['id'])) { + do { + $id = $this->randomMachineName(8); + } while (BlockContentType::load($id)); + } + else { + $id = $values['id']; + } + $values += [ + 'id' => $id, + 'label' => $id, + 'revision' => FALSE, + ]; + $bundle = BlockContentType::create($values); + $status = $bundle->save(); + block_content_add_body_field($bundle->id()); + + $this->assertEquals(SAVED_NEW, $status, sprintf('Created block content type %s.', $bundle->id())); + return $bundle; + } + +} diff --git a/tests/src/Functional/Views/BlockContentWizardTest.php b/tests/src/Functional/Views/BlockContentWizardTest.php new file mode 100644 index 0000000..662f99d --- /dev/null +++ b/tests/src/Functional/Views/BlockContentWizardTest.php @@ -0,0 +1,59 @@ +drupalLogin($this->drupalCreateUser(['administer views'])); + } + + /** + * Tests creating a 'block_content' entity view. + */ + public function testViewAddBlockContent(): void { + $view = []; + $view['label'] = $this->randomMachineName(16); + $view['id'] = $this->randomMachineName(16); + $view['description'] = $this->randomMachineName(16); + $view['page[create]'] = FALSE; + $view['show[wizard_key]'] = 'block_content'; + $this->drupalGet('admin/structure/views/add'); + $this->submitForm($view, 'Save and edit'); + + $view_storage_controller = $this->container->get('entity_type.manager')->getStorage('view'); + /** @var \Drupal\views\Entity\View $view */ + $view = $view_storage_controller->load($view['id']); + + $display_options = $view->getDisplay('default')['display_options']; + + $this->assertEquals('block_content', $display_options['filters']['reusable']['entity_type']); + $this->assertEquals('reusable', $display_options['filters']['reusable']['entity_field']); + $this->assertEquals('boolean', $display_options['filters']['reusable']['plugin_id']); + $this->assertEquals('1', $display_options['filters']['reusable']['value']); + } + +} diff --git a/tests/src/Kernel/BlockContentAccessHandlerTest.php b/tests/src/Kernel/BlockContentAccessHandlerTest.php new file mode 100644 index 0000000..6252aa7 --- /dev/null +++ b/tests/src/Kernel/BlockContentAccessHandlerTest.php @@ -0,0 +1,621 @@ +installSchema('user', ['users_data']); + $this->installEntitySchema('user'); + $this->installEntitySchema('block_content'); + + // Create a basic block content type. + $block_content_type = BlockContentType::create([ + 'id' => 'basic', + 'label' => 'A basic block type', + 'description' => "Provides a block type that is basic.", + ]); + $block_content_type->save(); + + // Create a square block content type. + $block_content_type = BlockContentType::create([ + 'id' => 'square', + 'label' => 'A square block type', + 'description' => "Provides a block type that is square.", + ]); + $block_content_type->save(); + + $this->blockEntity = BlockContent::create([ + 'info' => 'The Block', + 'type' => 'square', + ]); + $this->blockEntity->save(); + + // Create user 1 test does not have all permissions. + User::create([ + 'name' => 'admin', + ])->save(); + + $this->role = Role::create([ + 'id' => 'test', + 'label' => 'test role', + ]); + $this->role->save(); + $this->accessControlHandler = new BlockContentAccessControlHandler(\Drupal::entityTypeManager()->getDefinition('block_content'), \Drupal::service('event_dispatcher')); + } + + /** + * Test block content entity access. + * + * @param string $operation + * The entity operation to test. + * @param bool $published + * Whether the latest revision should be published. + * @param bool $reusable + * Whether the block content should be reusable. Non-reusable blocks are + * typically used in Layout Builder. + * @param array $permissions + * Permissions to grant to the test user. + * @param bool $isLatest + * Whether the block content should be the latest revision when checking + * access. If FALSE, multiple revisions will be created, and an older + * revision will be loaded before checking access. + * @param string|null $parent_access + * Whether the test user has access to the parent entity, valid values are + * class names of classes implementing AccessResultInterface. Set to NULL to + * assert parent will not be called. + * @param string $expected_access + * The expected access for the user and block content. Valid values are + * class names of classes implementing AccessResultInterface + * @param string|null $expected_access_message + * The expected access message. + * + * @covers ::checkAccess + * + * @dataProvider providerTestAccess + * + * @phpstan-param class-string<\Drupal\Core\Access\AccessResultInterface>|null $parent_access + * @phpstan-param class-string<\Drupal\Core\Access\AccessResultInterface> $expected_access + */ + public function testAccess(string $operation, bool $published, bool $reusable, array $permissions, bool $isLatest, ?string $parent_access, string $expected_access, ?string $expected_access_message = NULL): void { + /** @var \Drupal\Core\Entity\RevisionableStorageInterface $entityStorage */ + $entityStorage = \Drupal::entityTypeManager()->getStorage('block_content'); + + $loadRevisionId = NULL; + if (!$isLatest) { + // Save a historical revision, then setup for a new revision to be saved. + $this->blockEntity->save(); + $loadRevisionId = $this->blockEntity->getRevisionId(); + $this->blockEntity = $entityStorage->createRevision($this->blockEntity); + } + + $published ? $this->blockEntity->setPublished() : $this->blockEntity->setUnpublished(); + $reusable ? $this->blockEntity->setReusable() : $this->blockEntity->setNonReusable(); + + $user = User::create([ + 'name' => 'Someone', + 'mail' => 'hi@example.com', + ]); + + if ($permissions) { + foreach ($permissions as $permission) { + $this->role->grantPermission($permission); + } + $this->role->save(); + } + $user->addRole($this->role->id())->save(); + + if ($parent_access !== NULL) { + $parent_entity = $this->prophesize(AccessibleInterface::class); + $expected_parent_result = new ($parent_access)(); + $parent_entity->access($operation, $user, TRUE) + ->willReturn($expected_parent_result) + ->shouldBeCalled(); + + $this->blockEntity->setAccessDependency($parent_entity->reveal()); + + } + $this->blockEntity->save(); + + // Reload a previous revision. + if ($loadRevisionId !== NULL) { + $this->blockEntity = $entityStorage->loadRevision($loadRevisionId); + } + + $result = $this->accessControlHandler->access($this->blockEntity, $operation, $user, TRUE); + $this->assertInstanceOf($expected_access, $result); + if ($expected_access_message !== NULL) { + $this->assertInstanceOf(AccessResultReasonInterface::class, $result); + $this->assertEquals($expected_access_message, $result->getReason()); + } + } + + /** + * Data provider for testAccess(). + */ + public static function providerTestAccess(): array { + $cases = [ + 'view:published:reusable' => [ + 'view', + TRUE, + TRUE, + [], + TRUE, + NULL, + AccessResultAllowed::class, + ], + 'view:unpublished:reusable' => [ + 'view', + FALSE, + TRUE, + [], + TRUE, + NULL, + AccessResultNeutral::class, + ], + 'view:unpublished:reusable:admin' => [ + 'view', + FALSE, + TRUE, + ['access block library'], + TRUE, + NULL, + AccessResultAllowed::class, + ], + 'view:unpublished:reusable:per-block-editor:basic' => [ + 'view', + FALSE, + TRUE, + ['edit any basic block content'], + TRUE, + NULL, + AccessResultNeutral::class, + ], + 'view:unpublished:reusable:per-block-editor:square' => [ + 'view', + FALSE, + TRUE, + ['access block library', 'edit any basic block content'], + TRUE, + NULL, + AccessResultAllowed::class, + ], + 'view:published:reusable:admin' => [ + 'view', + TRUE, + TRUE, + ['access block library'], + TRUE, + NULL, + AccessResultAllowed::class, + ], + 'view:published:reusable:per-block-editor:basic' => [ + 'view', + TRUE, + TRUE, + ['access block library', 'edit any basic block content'], + TRUE, + NULL, + AccessResultAllowed::class, + ], + 'view:published:reusable:per-block-editor:square' => [ + 'view', + TRUE, + TRUE, + ['access block library', 'edit any square block content'], + TRUE, + NULL, + AccessResultAllowed::class, + ], + 'view:published:non_reusable' => [ + 'view', + TRUE, + FALSE, + [], + TRUE, + NULL, + AccessResultForbidden::class, + ], + 'view:published:non_reusable:parent_allowed' => [ + 'view', + TRUE, + FALSE, + [], + TRUE, + AccessResultAllowed::class, + AccessResultAllowed::class, + ], + 'view:published:non_reusable:parent_neutral' => [ + 'view', + TRUE, + FALSE, + [], + TRUE, + AccessResultNeutral::class, + AccessResultNeutral::class, + ], + 'view:published:non_reusable:parent_forbidden' => [ + 'view', + TRUE, + FALSE, + [], + TRUE, + AccessResultForbidden::class, + AccessResultForbidden::class, + ], + ]; + foreach (['update', 'delete'] as $operation) { + $label = $operation === 'update' ? 'edit' : 'delete'; + $cases += [ + $operation . ':published:reusable' => [ + $operation, + TRUE, + TRUE, + [], + TRUE, + NULL, + AccessResultNeutral::class, + ], + $operation . ':unpublished:reusable' => [ + $operation, + FALSE, + TRUE, + [], + TRUE, + NULL, + AccessResultNeutral::class, + ], + $operation . ':unpublished:reusable:admin' => [ + $operation, + FALSE, + TRUE, + [$label . ' any square block content'], + TRUE, + NULL, + AccessResultAllowed::class, + ], + $operation . ':published:reusable:admin' => [ + $operation, + TRUE, + TRUE, + [$label . ' any square block content'], + TRUE, + NULL, + AccessResultAllowed::class, + ], + $operation . ':published:non_reusable' => [ + $operation, + TRUE, + FALSE, + [], + TRUE, + NULL, + AccessResultForbidden::class, + ], + $operation . ':published:non_reusable:parent_allowed' => [ + $operation, + TRUE, + FALSE, + [], + TRUE, + AccessResultAllowed::class, + AccessResultNeutral::class, + ], + $operation . ':published:non_reusable:parent_neutral' => [ + $operation, + TRUE, + FALSE, + [], + TRUE, + AccessResultNeutral::class, + AccessResultNeutral::class, + ], + $operation . ':published:non_reusable:parent_forbidden' => [ + $operation, + TRUE, + FALSE, + [], + TRUE, + AccessResultForbidden::class, + AccessResultForbidden::class, + ], + $operation . ':unpublished:reusable:per-block-editor:basic' => [ + $operation, + FALSE, + TRUE, + ['edit any basic block content'], + TRUE, + NULL, + AccessResultNeutral::class, + ], + $operation . ':published:reusable:per-block-editor:basic' => [ + $operation, + TRUE, + TRUE, + ['edit any basic block content'], + TRUE, + NULL, + AccessResultNeutral::class, + ], + ]; + } + + $cases += [ + 'update:unpublished:reusable:per-block-editor:square' => [ + 'update', + FALSE, + TRUE, + ['edit any square block content'], + TRUE, + NULL, + AccessResultAllowed::class, + ], + 'update:published:reusable:per-block-editor:square' => [ + 'update', + TRUE, + TRUE, + ['edit any square block content'], + TRUE, + NULL, + AccessResultAllowed::class, + ], + ]; + + $cases += [ + 'delete:unpublished:reusable:per-block-editor:square' => [ + 'delete', + FALSE, + TRUE, + ['edit any square block content'], + TRUE, + NULL, + AccessResultNeutral::class, + ], + 'delete:published:reusable:per-block-editor:square' => [ + 'delete', + TRUE, + TRUE, + ['edit any square block content'], + TRUE, + NULL, + AccessResultNeutral::class, + ], + ]; + + // View all revisions: + $cases['view all revisions:none'] = [ + 'view all revisions', + TRUE, + TRUE, + [], + TRUE, + NULL, + AccessResultNeutral::class, + ]; + $cases['view all revisions:view any bundle history'] = [ + 'view all revisions', + TRUE, + TRUE, + ['view any square block content history'], + TRUE, + NULL, + AccessResultAllowed::class, + ]; + $cases['view all revisions:administer block content'] = [ + 'view all revisions', + TRUE, + TRUE, + ['administer block content'], + TRUE, + NULL, + AccessResultAllowed::class, + ]; + + // Revert revisions: + $cases['revert:none:latest'] = [ + 'revert', + TRUE, + TRUE, + [], + TRUE, + NULL, + AccessResultForbidden::class, + ]; + $cases['revert:none:historical'] = [ + 'revert', + TRUE, + TRUE, + [], + FALSE, + NULL, + AccessResultNeutral::class, + ]; + $cases['revert:revert bundle:historical'] = [ + 'revert', + TRUE, + TRUE, + ['revert any square block content revisions'], + FALSE, + NULL, + AccessResultAllowed::class, + ]; + $cases['revert:administer block content:latest'] = [ + 'revert', + TRUE, + TRUE, + ['administer block content'], + TRUE, + NULL, + AccessResultForbidden::class, + ]; + $cases['revert:administer block content:historical'] = [ + 'revert', + TRUE, + TRUE, + ['administer block content'], + FALSE, + NULL, + AccessResultAllowed::class, + ]; + $cases['revert:revert bundle:historical:non reusable'] = [ + 'revert', + TRUE, + FALSE, + ['revert any square block content revisions'], + FALSE, + NULL, + AccessResultForbidden::class, + 'Block content must be reusable to use `revert` operation', + ]; + + // Delete revisions: + $cases['delete revision:none:latest'] = [ + 'delete revision', + TRUE, + TRUE, + [], + TRUE, + NULL, + AccessResultForbidden::class, + ]; + $cases['delete revision:none:historical'] = [ + 'delete revision', + TRUE, + TRUE, + [], + FALSE, + NULL, + AccessResultNeutral::class, + ]; + $cases['delete revision:administer block content:latest'] = [ + 'delete revision', + TRUE, + TRUE, + ['administer block content'], + TRUE, + NULL, + AccessResultForbidden::class, + ]; + $cases['delete revision:administer block content:historical'] = [ + 'delete revision', + TRUE, + TRUE, + ['administer block content'], + FALSE, + NULL, + AccessResultAllowed::class, + ]; + $cases['delete revision:delete bundle:latest'] = [ + 'delete revision', + TRUE, + TRUE, + ['administer block content'], + TRUE, + NULL, + AccessResultForbidden::class, + ]; + $cases['delete revision:delete bundle:historical'] = [ + 'delete revision', + TRUE, + TRUE, + ['delete any square block content revisions'], + FALSE, + NULL, + AccessResultAllowed::class, + ]; + $cases['delete revision:delete bundle:historical:non reusable'] = [ + 'delete revision', + TRUE, + FALSE, + ['delete any square block content revisions'], + FALSE, + NULL, + AccessResultForbidden::class, + 'Block content must be reusable to use `delete revision` operation', + ]; + + return $cases; + } + + /** + * Tests revision log access. + */ + public function testRevisionLogAccess(): void { + $admin = $this->createUser([ + 'administer block content', + 'access content', + ]); + $editor = $this->createUser([ + 'access content', + 'access block library', + 'view any square block content history', + ]); + $viewer = $this->createUser([ + 'access content', + ]); + + $this->assertTrue($this->blockEntity->get('revision_log')->access('view', $admin)); + $this->assertTrue($this->blockEntity->get('revision_log')->access('view', $editor)); + $this->assertFalse($this->blockEntity->get('revision_log')->access('view', $viewer)); + } + +} diff --git a/tests/src/Kernel/BlockContentDeletionTest.php b/tests/src/Kernel/BlockContentDeletionTest.php new file mode 100644 index 0000000..07d932f --- /dev/null +++ b/tests/src/Kernel/BlockContentDeletionTest.php @@ -0,0 +1,86 @@ +installEntitySchema('user'); + $this->installEntitySchema('block_content'); + $this->container->get('theme_installer')->install(['stark']); + } + + /** + * Tests deleting a block_content updates the discovered block plugin. + */ + public function testDeletingBlockContentShouldClearPluginCache(): void { + // Create a block content type. + $block_content_type = BlockContentType::create([ + 'id' => 'spiffy', + 'label' => 'Mucho spiffy', + 'description' => "Provides a block type that increases your site's spiffiness by upto 11%", + ]); + $block_content_type->save(); + // And a block content entity. + $block_content = BlockContent::create([ + 'info' => 'Spiffy prototype', + 'type' => 'spiffy', + ]); + $block_content->save(); + + // Make sure the block content provides a derivative block plugin in the + // block repository. + /** @var \Drupal\Core\Block\BlockManagerInterface $block_manager */ + $block_manager = $this->container->get('plugin.manager.block'); + $plugin_id = 'block_content' . PluginBase::DERIVATIVE_SEPARATOR . $block_content->uuid(); + $this->assertTrue($block_manager->hasDefinition($plugin_id)); + + // Now delete the block content entity. + $block_content->delete(); + // The plugin should no longer exist. + $this->assertFalse($block_manager->hasDefinition($plugin_id)); + + // Create another block content entity. + $block_content = BlockContent::create([ + 'info' => 'Spiffy prototype', + 'type' => 'spiffy', + ]); + $block_content->save(); + + $plugin_id = 'block_content' . PluginBase::DERIVATIVE_SEPARATOR . $block_content->uuid(); + $block = $this->placeBlock($plugin_id, ['region' => 'content', 'theme' => 'stark']); + + // Delete it via storage. + $storage = $this->container->get('entity_type.manager')->getStorage('block_content'); + $storage->delete([$block_content]); + // The plugin should no longer exist. + $this->assertFalse($block_manager->hasDefinition($plugin_id)); + + $this->assertNull($this->container->get('entity_type.manager')->getStorage('block')->loadUnchanged($block->id())); + } + +} diff --git a/tests/src/Kernel/BlockContentDeriverTest.php b/tests/src/Kernel/BlockContentDeriverTest.php new file mode 100644 index 0000000..edd3e0e --- /dev/null +++ b/tests/src/Kernel/BlockContentDeriverTest.php @@ -0,0 +1,124 @@ + 'block_content', + 'provider' => 'block_content', + 'class' => '\Drupal\block_content\Plugin\Block\BlockContentBlock', + 'deriver' => '\Drupal\block_content\Plugin\Derivative\BlockContent', + ]; + + /** + * The block content storage. + * + * @var \Drupal\Core\Entity\EntityStorageInterface + */ + protected $blockContentStorage; + + /** + * The tested block content derivative class. + * + * @var \Drupal\block_content\Plugin\Derivative\BlockContent + */ + protected $blockContentDerivative; + + /** + * {@inheritdoc} + */ + protected function setUp(): void { + parent::setUp(); + $this->installEntitySchema('user'); + $this->installEntitySchema('block_content'); + + $this->blockContentStorage = \Drupal::entityTypeManager()->getStorage('block_content'); + $this->blockContentDerivative = new DerivativeBlockContent($this->blockContentStorage); + } + + /** + * Tests that only reusable blocks are derived. + */ + public function testReusableBlocksOnlyAreDerived(): void { + // Create a block content type. + $block_content_type = BlockContentType::create([ + 'id' => 'spiffy', + 'label' => 'Mucho spiffy', + 'description' => "Provides a block type that increases your site's spiffiness by up to 11%", + ]); + $block_content_type->save(); + // And a block content entity. + $block_content = BlockContent::create([ + 'info' => 'Spiffy prototype', + 'type' => 'spiffy', + ]); + $block_content->save(); + + // Ensure the reusable block content is provided as a derivative block + // plugin. + /** @var \Drupal\Core\Block\BlockManagerInterface $block_manager */ + $block_manager = $this->container->get('plugin.manager.block'); + $plugin_id = 'block_content' . PluginBase::DERIVATIVE_SEPARATOR . $block_content->uuid(); + $this->assertTrue($block_manager->hasDefinition($plugin_id)); + + // Set the block not to be reusable. + $block_content->setNonReusable(); + $block_content->save(); + + // Ensure the non-reusable block content is not provided a derivative block + // plugin. + $this->assertFalse($block_manager->hasDefinition($plugin_id)); + } + + /** + * Tests the admin labels of derivative definitions. + */ + public function testGetDerivativeDefinitionsAdminLabels(): void { + $blockContentType = BlockContentType::create([ + 'id' => 'basic', + 'label' => 'Basic Block', + ]); + $blockContentType->save(); + $blockContentWithLabel = BlockContent::create([ + 'info' => 'Basic prototype', + 'type' => 'basic', + ]); + $blockContentWithLabel->save(); + $blockContentNoLabel = BlockContent::create([ + 'type' => 'basic', + ]); + $blockContentNoLabel->save(); + + $blockPluginManager = \Drupal::service('plugin.manager.block'); + $plugin = $blockPluginManager->createInstance('block_content:' . $blockContentWithLabel->uuid()); + $this->assertEquals('Basic prototype', $plugin->getPluginDefinition()['admin_label']); + + $plugin = $blockPluginManager->createInstance('block_content:' . $blockContentNoLabel->uuid()); + $this->assertEquals('Basic Block: ' . $blockContentNoLabel->id(), $plugin->getPluginDefinition()['admin_label']); + } + +} diff --git a/tests/src/Kernel/BlockContentEntityReferenceSelectionTest.php b/tests/src/Kernel/BlockContentEntityReferenceSelectionTest.php new file mode 100644 index 0000000..87592d9 --- /dev/null +++ b/tests/src/Kernel/BlockContentEntityReferenceSelectionTest.php @@ -0,0 +1,195 @@ +installEntitySchema('user'); + $this->installEntitySchema('block_content'); + + // Create a block content type. + $block_content_type = BlockContentType::create([ + 'id' => 'spiffy', + 'label' => 'Mucho spiffy', + 'description' => "Provides a block type that increases your site's spiffiness by up to 11%", + ]); + $block_content_type->save(); + $this->entityTypeManager = $this->container->get('entity_type.manager'); + + // And reusable block content entities. + $this->blockReusable = BlockContent::create([ + 'info' => 'Reusable Block', + 'type' => 'spiffy', + ]); + $this->blockReusable->save(); + $this->blockNonReusable = BlockContent::create([ + 'info' => 'Non-reusable Block', + 'type' => 'spiffy', + 'reusable' => FALSE, + ]); + $this->blockNonReusable->save(); + + $configuration = [ + 'target_type' => 'block_content', + 'target_bundles' => ['spiffy' => 'spiffy'], + 'sort' => ['field' => '_none'], + ]; + $this->selectionHandler = new TestSelection($configuration, '', '', $this->container->get('entity_type.manager'), $this->container->get('module_handler'), \Drupal::currentUser(), \Drupal::service('entity_field.manager'), \Drupal::service('entity_type.bundle.info'), \Drupal::service('entity.repository')); + + // Setup the 3 expectation cases. + $this->expectations = [ + 'both_blocks' => [ + 'spiffy' => [ + $this->blockReusable->id() => $this->blockReusable->label(), + $this->blockNonReusable->id() => $this->blockNonReusable->label(), + ], + ], + 'block_reusable' => ['spiffy' => [$this->blockReusable->id() => $this->blockReusable->label()]], + 'block_non_reusable' => ['spiffy' => [$this->blockNonReusable->id() => $this->blockNonReusable->label()]], + ]; + } + + /** + * Tests to make sure queries without the expected tags are not altered. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function testQueriesNotAltered(): void { + // Ensure that queries without all the tags are not altered. + $query = $this->entityTypeManager->getStorage('block_content') + ->getQuery() + ->accessCheck(FALSE); + $this->assertCount(2, $query->execute()); + + $query = $this->entityTypeManager->getStorage('block_content') + ->getQuery() + ->accessCheck(FALSE); + $query->addTag('block_content_access'); + $this->assertCount(2, $query->execute()); + + $query = $this->entityTypeManager->getStorage('block_content') + ->getQuery() + ->accessCheck(FALSE); + $query->addTag('entity_query_block_content'); + $this->assertCount(2, $query->execute()); + } + + /** + * Tests with no conditions set. + * + * @throws \Drupal\Core\Entity\EntityStorageException + */ + public function testNoConditions(): void { + $this->assertEquals( + $this->expectations['block_reusable'], + $this->selectionHandler->getReferenceableEntities() + ); + + $this->blockNonReusable->setReusable(); + $this->blockNonReusable->save(); + + // Ensure that the block is now returned as a referenceable entity. + $this->assertEquals( + $this->expectations['both_blocks'], + $this->selectionHandler->getReferenceableEntities() + ); + } + + /** + * Tests setting 'reusable' condition on different levels. + * + * @dataProvider fieldConditionProvider + * + * @throws \Exception + */ + public function testFieldConditions($condition_type, $is_reusable): void { + $this->selectionHandler->setTestMode($condition_type, $is_reusable); + $this->assertEquals( + $is_reusable ? $this->expectations['block_reusable'] : $this->expectations['block_non_reusable'], + $this->selectionHandler->getReferenceableEntities() + ); + } + + /** + * Provides possible fields and condition types. + */ + public static function fieldConditionProvider() { + $cases = []; + foreach (['base', 'group', 'nested_group'] as $condition_type) { + foreach ([TRUE, FALSE] as $reusable) { + $cases["$condition_type:" . ($reusable ? 'reusable' : 'non-reusable')] = [ + $condition_type, + $reusable, + ]; + } + } + return $cases; + } + +} diff --git a/tests/src/Kernel/BlockContentPermissionsTest.php b/tests/src/Kernel/BlockContentPermissionsTest.php new file mode 100644 index 0000000..e10b4c9 --- /dev/null +++ b/tests/src/Kernel/BlockContentPermissionsTest.php @@ -0,0 +1,87 @@ +installEntitySchema('user'); + $this->installEntitySchema('block_content'); + + $this->permissionHandler = $this->container->get('user.permissions'); + } + + /** + * @covers ::blockTypePermissions + */ + public function testDynamicPermissions(): void { + $permissions = $this->permissionHandler->getPermissions(); + $this->assertArrayNotHasKey('edit any basic block content', $permissions, 'The per-block-type permission does not exist.'); + $this->assertArrayNotHasKey('edit any square block content', $permissions, 'The per-block-type permission does not exist.'); + + // Create a basic block content type. + BlockContentType::create([ + 'id' => 'basic', + 'label' => 'A basic block type', + 'description' => 'Provides a basic block type', + ])->save(); + + // Create a square block content type. + BlockContentType::create([ + 'id' => 'square', + 'label' => 'A square block type', + 'description' => 'Provides a block type that is square', + ])->save(); + + $permissions = $this->permissionHandler->getPermissions(); + + // Assert the basic permission has been created. + $this->assertArrayHasKey('edit any basic block content', $permissions, 'The per-block-type permission exists.'); + $this->assertEquals( + 'A basic block type: Edit content block', + $permissions['edit any basic block content']['title']->render() + ); + + // Assert the square permission has been created. + $this->assertArrayHasKey('edit any square block content', $permissions, 'The per-block-type permission exists.'); + $this->assertEquals( + 'A square block type: Edit content block', + $permissions['edit any square block content']['title']->render() + ); + } + +} diff --git a/tests/src/Kernel/BlockContentRevisionsTest.php b/tests/src/Kernel/BlockContentRevisionsTest.php new file mode 100644 index 0000000..42763d5 --- /dev/null +++ b/tests/src/Kernel/BlockContentRevisionsTest.php @@ -0,0 +1,55 @@ +installEntitySchema('user'); + $this->installEntitySchema('block_content'); + } + + /** + * Tests block content revision user id doesn't throw error with null field. + */ + public function testNullRevisionUser(): void { + BlockContentType::create([ + 'id' => 'basic', + 'label' => 'A basic block type', + ])->save(); + + $block = BlockContent::create([ + 'info' => 'Test', + 'type' => 'basic', + 'revision_user' => NULL, + ]); + $block->save(); + $this->assertNull($block->getRevisionUserId()); + } + +} diff --git a/tests/src/Kernel/BlockContentTest.php b/tests/src/Kernel/BlockContentTest.php new file mode 100644 index 0000000..8cc3f2b --- /dev/null +++ b/tests/src/Kernel/BlockContentTest.php @@ -0,0 +1,77 @@ +installEntitySchema('user'); + $this->installEntitySchema('block_content'); + } + + /** + * Tests the editing links for BlockContentBlock. + */ + public function testOperationLinks(): void { + // Create a block content type. + BlockContentType::create([ + 'id' => 'spiffy', + 'label' => 'Mucho spiffy', + 'description' => "Provides a block type that increases your site's spiffiness by up to 11%", + ])->save(); + // And a block content entity. + $block_content = BlockContent::create([ + 'info' => 'Spiffy prototype', + 'type' => 'spiffy', + ]); + $block_content->save(); + $block = Block::create([ + 'plugin' => 'block_content' . PluginBase::DERIVATIVE_SEPARATOR . $block_content->uuid(), + 'region' => 'content', + 'id' => 'machine_name', + 'theme' => 'stark', + ]); + + // The anonymous user doesn't have the "administer block" permission. + $this->assertEmpty(block_content_entity_operation($block)); + + $this->setUpCurrentUser(['uid' => 1], ['edit any spiffy block content', 'administer blocks']); + + // The admin user does have the "administer block" permission. + $this->assertEquals([ + 'block-edit' => [ + 'title' => $this->t('Edit block'), + 'url' => $block_content->toUrl('edit-form')->setOptions([]), + 'weight' => 50, + ], + ], block_content_entity_operation($block)); + } + +} diff --git a/tests/src/Kernel/BlockContentTypeValidationTest.php b/tests/src/Kernel/BlockContentTypeValidationTest.php new file mode 100644 index 0000000..1a61c18 --- /dev/null +++ b/tests/src/Kernel/BlockContentTypeValidationTest.php @@ -0,0 +1,41 @@ +entity = BlockContentType::create([ + 'id' => 'test', + 'label' => 'Test', + ]); + $this->entity->save(); + } + +} diff --git a/tests/src/Kernel/BlockTemplateSuggestionsTest.php b/tests/src/Kernel/BlockTemplateSuggestionsTest.php new file mode 100644 index 0000000..426f9c3 --- /dev/null +++ b/tests/src/Kernel/BlockTemplateSuggestionsTest.php @@ -0,0 +1,88 @@ +installEntitySchema('block_content'); + + // Create a block content type. + $block_content_type = BlockContentType::create([ + 'id' => 'test_block', + 'label' => 'A test block type', + 'description' => "Provides a test block type.", + ]); + $block_content_type->save(); + + $this->blockContent = BlockContent::create([ + 'info' => 'The Test Block', + 'type' => 'test_block', + ]); + $this->blockContent->save(); + } + + /** + * Tests template suggestions from block_content_theme_suggestions_block(). + */ + public function testBlockThemeHookSuggestions(): void { + // Create a block using a block_content plugin. + $block = Block::create([ + 'plugin' => 'block_content:' . $this->blockContent->uuid(), + 'region' => 'footer', + 'id' => 'machine_name', + ]); + + $variables['elements']['#id'] = $block->id(); + $variables['elements']['#configuration']['provider'] = 'block_content'; + $variables['elements']['#configuration']['view_mode'] = 'full'; + $variables['elements']['content']['#block_content'] = $this->blockContent; + $suggestions_empty = []; + $suggestions_empty[] = 'block__block_content__' . $block->uuid(); + $suggestions = block_content_theme_suggestions_block_alter($suggestions_empty, $variables); + + $this->assertSame([ + 'block__block_content__' . $block->uuid(), + 'block__block_content__view__full', + 'block__block_content__type__test_block', + 'block__block_content__view_type__test_block__full', + 'block__block_content__id__machine_name', + 'block__block_content__id_view__machine_name__full', + ], $suggestions); + } + +} diff --git a/tests/src/Kernel/Migrate/MigrateBlockContentBodyFieldTest.php b/tests/src/Kernel/Migrate/MigrateBlockContentBodyFieldTest.php new file mode 100644 index 0000000..09bdc51 --- /dev/null +++ b/tests/src/Kernel/Migrate/MigrateBlockContentBodyFieldTest.php @@ -0,0 +1,58 @@ +installEntitySchema('block_content'); + $this->installConfig(['block_content']); + $this->executeMigrations([ + 'block_content_type', + 'block_content_body_field', + ]); + } + + /** + * Tests the block content body field migration. + */ + public function testBlockContentBodyFieldMigration(): void { + /** @var \Drupal\field\FieldStorageConfigInterface $storage */ + $storage = FieldStorageConfig::load('block_content.body'); + $this->assertInstanceOf(FieldStorageConfigInterface::class, $storage); + $this->assertSame('block_content', $storage->getTargetEntityTypeId()); + $this->assertSame(['basic'], array_values($storage->getBundles())); + $this->assertSame('body', $storage->getName()); + + /** @var \Drupal\field\FieldConfigInterface $field */ + $field = FieldConfig::load('block_content.basic.body'); + $this->assertInstanceOf(FieldConfigInterface::class, $field); + $this->assertSame('block_content', $field->getTargetEntityTypeId()); + $this->assertSame('basic', $field->getTargetBundle()); + $this->assertSame('body', $field->getName()); + $this->assertSame('Body', $field->getLabel()); + } + +} diff --git a/tests/src/Kernel/Migrate/MigrateBlockContentEntityDisplayTest.php b/tests/src/Kernel/Migrate/MigrateBlockContentEntityDisplayTest.php new file mode 100644 index 0000000..8aabd89 --- /dev/null +++ b/tests/src/Kernel/Migrate/MigrateBlockContentEntityDisplayTest.php @@ -0,0 +1,59 @@ +installEntitySchema('block_content'); + $this->installConfig(static::$modules); + $this->executeMigrations([ + 'block_content_type', + 'block_content_body_field', + 'block_content_entity_display', + ]); + } + + /** + * Asserts a display entity. + * + * @param string $id + * The entity ID. + * @param string $component_id + * The ID of the display component. + * + * @internal + */ + protected function assertDisplay(string $id, string $component_id): void { + $component = EntityViewDisplay::load($id)->getComponent($component_id); + $this->assertIsArray($component); + $this->assertSame('hidden', $component['label']); + } + + /** + * Tests the migrated display configuration. + */ + public function testMigration(): void { + $this->assertDisplay('block_content.basic.default', 'body'); + } + +} diff --git a/tests/src/Kernel/Migrate/MigrateBlockContentEntityFormDisplayTest.php b/tests/src/Kernel/Migrate/MigrateBlockContentEntityFormDisplayTest.php new file mode 100644 index 0000000..edbe0ca --- /dev/null +++ b/tests/src/Kernel/Migrate/MigrateBlockContentEntityFormDisplayTest.php @@ -0,0 +1,59 @@ +installEntitySchema('block_content'); + $this->installConfig(static::$modules); + $this->executeMigrations([ + 'block_content_type', + 'block_content_body_field', + 'block_content_entity_form_display', + ]); + } + + /** + * Asserts a display entity. + * + * @param string $id + * The entity ID. + * @param string $component_id + * The ID of the form component. + * + * @internal + */ + protected function assertDisplay(string $id, string $component_id): void { + $component = EntityFormDisplay::load($id)->getComponent($component_id); + $this->assertIsArray($component); + $this->assertSame('text_textarea_with_summary', $component['type']); + } + + /** + * Tests the migrated display configuration. + */ + public function testMigration(): void { + $this->assertDisplay('block_content.basic.default', 'body'); + } + +} diff --git a/tests/src/Kernel/Migrate/MigrateBlockContentStubTest.php b/tests/src/Kernel/Migrate/MigrateBlockContentStubTest.php new file mode 100644 index 0000000..c903935 --- /dev/null +++ b/tests/src/Kernel/Migrate/MigrateBlockContentStubTest.php @@ -0,0 +1,55 @@ +installEntitySchema('block_content'); + } + + /** + * Tests creation of block content stubs with no block_content_type available. + */ + public function testStubFailure(): void { + // Expected MigrateException thrown when no bundles exist. + $this->expectException(MigrateException::class); + $this->expectExceptionMessage('Stubbing failed, no bundles available for entity type: block_content'); + $this->createEntityStub('block_content'); + } + + /** + * Tests creation of block content stubs when there is a block_content_type. + */ + public function testStubSuccess(): void { + BlockContentType::create([ + 'id' => 'test_block_content_type', + 'label' => 'Test block content type', + ])->save(); + $this->performStubTest('block_content'); + } + +} diff --git a/tests/src/Kernel/Migrate/MigrateBlockContentTypeTest.php b/tests/src/Kernel/Migrate/MigrateBlockContentTypeTest.php new file mode 100644 index 0000000..c9a677f --- /dev/null +++ b/tests/src/Kernel/Migrate/MigrateBlockContentTypeTest.php @@ -0,0 +1,43 @@ +installEntitySchema('block_content'); + $this->installConfig(['block_content']); + $this->executeMigration('block_content_type'); + } + + /** + * Tests the block content type migration. + */ + public function testBlockContentTypeMigration(): void { + /** @var \Drupal\block_content\BlockContentTypeInterface $entity */ + $entity = BlockContentType::load('basic'); + $this->assertInstanceOf(BlockContentTypeInterface::class, $entity); + $this->assertSame('Basic', $entity->label()); + } + +} diff --git a/tests/src/Kernel/Migrate/d6/MigrateBlockContentTest.php b/tests/src/Kernel/Migrate/d6/MigrateBlockContentTest.php new file mode 100644 index 0000000..cd90204 --- /dev/null +++ b/tests/src/Kernel/Migrate/d6/MigrateBlockContentTest.php @@ -0,0 +1,62 @@ +installEntitySchema('block_content'); + $this->installConfig(['block_content']); + + $this->executeMigrations([ + 'd6_filter_format', + 'block_content_type', + 'block_content_body_field', + 'd6_custom_block', + ]); + } + + /** + * Tests the Drupal 6 content block to Drupal 8 migration. + */ + public function testBlockMigration(): void { + /** @var \Drupal\block_content\Entity\BlockContent $block */ + $block = BlockContent::load(1); + $this->assertSame('My block 1', $block->label()); + $requestTime = \Drupal::time()->getRequestTime(); + $this->assertGreaterThanOrEqual($requestTime, (int) $block->getChangedTime()); + $this->assertLessThanOrEqual(time(), $block->getChangedTime()); + $this->assertSame('en', $block->language()->getId()); + $this->assertSame('

My first content block body

', $block->body->value); + $this->assertSame('full_html', $block->body->format); + + $block = BlockContent::load(2); + $this->assertSame('My block 2', $block->label()); + $this->assertGreaterThanOrEqual($requestTime, (int) $block->getChangedTime()); + $this->assertGreaterThanOrEqual($requestTime, (int) $block->getChangedTime()); + $this->assertLessThanOrEqual(time(), $block->getChangedTime()); + $this->assertSame('en', $block->language()->getId()); + $this->assertSame('

My second content block body

', $block->body->value); + $this->assertSame('full_html', $block->body->format); + } + +} diff --git a/tests/src/Kernel/Migrate/d6/MigrateCustomBlockContentTranslationTest.php b/tests/src/Kernel/Migrate/d6/MigrateCustomBlockContentTranslationTest.php new file mode 100644 index 0000000..bc042de --- /dev/null +++ b/tests/src/Kernel/Migrate/d6/MigrateCustomBlockContentTranslationTest.php @@ -0,0 +1,73 @@ +installEntitySchema('block_content'); + $this->installConfig(['block_content']); + $this->executeMigrations([ + 'language', + 'd6_filter_format', + 'block_content_type', + 'block_content_body_field', + 'd6_custom_block', + 'd6_custom_block_translation', + ]); + } + + /** + * Tests the Drupal 6 i18n content block strings to Drupal 8 migration. + */ + public function testCustomBlockContentTranslation(): void { + /** @var \Drupal\block_content\Entity\BlockContent $block */ + $block = BlockContent::load(1)->getTranslation('fr'); + $this->assertSame('fr - Static Block', $block->label()); + $this->assertGreaterThanOrEqual(\Drupal::time()->getRequestTime(), $block->getChangedTime()); + $this->assertLessThanOrEqual(time(), $block->getChangedTime()); + $this->assertSame('fr', $block->language()->getId()); + $this->assertSame('

fr - My first content block body

', $block->body->value); + $this->assertSame('full_html', $block->body->format); + + $block = $block->getTranslation('zu'); + $this->assertSame('My block 1', $block->label()); + $this->assertGreaterThanOrEqual(\Drupal::time()->getRequestTime(), $block->getChangedTime()); + $this->assertLessThanOrEqual(time(), $block->getChangedTime()); + $this->assertSame('zu', $block->language()->getId()); + $this->assertSame('

zu - My first content block body

', $block->body->value); + $this->assertSame('full_html', $block->body->format); + + $block = BlockContent::load(2)->getTranslation('fr'); + $this->assertSame('Encore un bloc statique', $block->label()); + $this->assertGreaterThanOrEqual(\Drupal::time()->getRequestTime(), $block->getChangedTime()); + $this->assertLessThanOrEqual(time(), $block->getChangedTime()); + $this->assertSame('fr', $block->language()->getId()); + $this->assertSame('Nom de vocabulaire beaucoup plus long que trente-deux caractères', $block->body->value); + $this->assertSame('full_html', $block->body->format); + } + +} diff --git a/tests/src/Kernel/Migrate/d7/MigrateCustomBlockContentTranslationTest.php b/tests/src/Kernel/Migrate/d7/MigrateCustomBlockContentTranslationTest.php new file mode 100644 index 0000000..da086a4 --- /dev/null +++ b/tests/src/Kernel/Migrate/d7/MigrateCustomBlockContentTranslationTest.php @@ -0,0 +1,69 @@ +installEntitySchema('block_content'); + $this->installConfig(['block_content']); + $this->executeMigrations([ + 'language', + 'd7_filter_format', + 'block_content_type', + 'block_content_body_field', + 'd7_custom_block', + 'd7_custom_block_translation', + ]); + } + + /** + * Tests the Drupal 7 i18n content block strings to Drupal 8 migration. + */ + public function testCustomBlockContentTranslation(): void { + /** @var \Drupal\block_content\Entity\BlockContent $block */ + $block = BlockContent::load(1)->getTranslation('fr'); + $this->assertSame('fr - Mildly amusing limerick of the day', $block->label()); + $this->assertGreaterThanOrEqual($block->getChangedTime(), \Drupal::time()->getRequestTime()); + $this->assertLessThanOrEqual(time(), $block->getChangedTime()); + $this->assertSame('fr', $block->language()->getId()); + $translation = "fr - A fellow jumped off a high wall\r\nAnd had a most terrible fall\r\nHe went back to bed\r\nWith a bump on his head\r\nThat's why you don't jump off a wall"; + $this->assertSame($translation, $block->body->value); + $this->assertSame('filtered_html', $block->body->format); + + $block = $block->getTranslation('is'); + $this->assertSame('is - Mildly amusing limerick of the day', $block->label()); + $this->assertGreaterThanOrEqual($block->getChangedTime(), \Drupal::time()->getRequestTime()); + $this->assertLessThanOrEqual(time(), $block->getChangedTime()); + $this->assertSame('is', $block->language()->getId()); + $text = "A fellow jumped off a high wall\r\nAnd had a most terrible fall\r\nHe went back to bed\r\nWith a bump on his head\r\nThat's why you don't jump off a wall"; + $this->assertSame($text, $block->body->value); + $this->assertSame('filtered_html', $block->body->format); + } + +} diff --git a/tests/src/Kernel/Migrate/d7/MigrateCustomBlockTest.php b/tests/src/Kernel/Migrate/d7/MigrateCustomBlockTest.php new file mode 100644 index 0000000..cb2f5e5 --- /dev/null +++ b/tests/src/Kernel/Migrate/d7/MigrateCustomBlockTest.php @@ -0,0 +1,57 @@ +installEntitySchema('block_content'); + $this->installConfig(static::$modules); + + $this->executeMigrations([ + 'd7_filter_format', + 'block_content_type', + 'block_content_body_field', + 'd7_custom_block', + ]); + } + + /** + * Tests migration of content blocks from Drupal 7 to Drupal 8. + */ + public function testCustomBlockMigration(): void { + $block = BlockContent::load(1); + $this->assertInstanceOf(BlockContentInterface::class, $block); + /** @var \Drupal\block_content\BlockContentInterface $block */ + $this->assertSame('Limerick', $block->label()); + + $expected_body = "A fellow jumped off a high wall\r\nAnd had a most terrible fall\r\nHe went back to bed\r\nWith a bump on his head\r\nThat's why you don't jump off a wall"; + $this->assertSame($expected_body, $block->body->value); + $this->assertSame('filtered_html', $block->body->format); + } + +} diff --git a/tests/src/Kernel/Plugin/migrate/source/d6/BoxTest.php b/tests/src/Kernel/Plugin/migrate/source/d6/BoxTest.php new file mode 100644 index 0000000..4c7b0fd --- /dev/null +++ b/tests/src/Kernel/Plugin/migrate/source/d6/BoxTest.php @@ -0,0 +1,48 @@ + 1, + 'body' => '

I made some custom content.

', + 'info' => 'Static Block', + 'format' => 1, + ], + [ + 'bid' => 2, + 'body' => '

I made some more custom content.

', + 'info' => 'Test Content', + 'format' => 1, + ], + ]; + // The expected results are identical to the source data. + $tests[0]['expected_data'] = $tests[0]['source_data']['boxes']; + + return $tests; + } + +} diff --git a/tests/src/Kernel/Plugin/migrate/source/d6/BoxTranslationTest.php b/tests/src/Kernel/Plugin/migrate/source/d6/BoxTranslationTest.php new file mode 100644 index 0000000..337698c --- /dev/null +++ b/tests/src/Kernel/Plugin/migrate/source/d6/BoxTranslationTest.php @@ -0,0 +1,143 @@ + 1, + 'body' => 'box 1 body', + 'info' => 'box 1 title', + 'format' => '2', + ], + [ + 'bid' => 2, + 'body' => 'box 2 body', + 'info' => 'box 2 title', + 'format' => '2', + ], + ]; + + $tests[0]['source_data']['i18n_strings'] = [ + [ + 'lid' => 1, + 'objectid' => 1, + 'type' => 'block', + 'property' => 'title', + 'objectindex' => 1, + 'format' => 0, + ], + [ + 'lid' => 2, + 'objectid' => 1, + 'type' => 'block', + 'property' => 'body', + 'objectindex' => 1, + 'format' => 0, + ], + [ + 'lid' => 3, + 'objectid' => 2, + 'type' => 'block', + 'property' => 'body', + 'objectindex' => 2, + 'format' => 2, + ], + ]; + + $tests[0]['source_data']['locales_target'] = [ + [ + 'lid' => 1, + 'language' => 'fr', + 'translation' => 'fr - title translation', + 'plid' => 0, + 'plural' => 0, + 'i18n_status' => 0, + ], + [ + 'lid' => 2, + 'language' => 'fr', + 'translation' => 'fr - body translation', + 'plid' => 0, + 'plural' => 0, + 'i18n_status' => 0, + ], + [ + 'lid' => 3, + 'language' => 'zu', + 'translation' => 'zu - body translation', + 'plid' => 0, + 'plural' => 0, + 'i18n_status' => 0, + ], + ]; + + $tests[0]['expected_data'] = [ + [ + 'lid' => '1', + 'property' => 'title', + 'language' => 'fr', + 'translation' => 'fr - title translation', + 'bid' => '1', + 'format' => '2', + 'title_translated' => 'fr - title translation', + 'body_translated' => 'fr - body translation', + 'title' => 'box 1 title', + 'body' => 'box 1 body', + ], + [ + 'lid' => '2', + 'property' => 'body', + 'language' => 'fr', + 'translation' => 'fr - body translation', + 'bid' => '1', + 'format' => '2', + 'title_translated' => 'fr - title translation', + 'body_translated' => 'fr - body translation', + 'title' => 'box 1 title', + 'body' => 'box 1 body', + ], + [ + 'lid' => '3', + 'property' => 'body', + 'language' => 'zu', + 'translation' => 'zu - body translation', + 'bid' => '2', + 'format' => '2', + 'title_translated' => NULL, + 'body_translated' => 'zu - body translation', + 'title' => 'box 2 title', + 'body' => 'box 2 body', + ], + ]; + + return $tests; + } + +} diff --git a/tests/src/Kernel/Plugin/migrate/source/d7/BlockCustomTest.php b/tests/src/Kernel/Plugin/migrate/source/d7/BlockCustomTest.php new file mode 100644 index 0000000..bea4529 --- /dev/null +++ b/tests/src/Kernel/Plugin/migrate/source/d7/BlockCustomTest.php @@ -0,0 +1,42 @@ + '1', + 'body' => "I don't feel creative enough to write anything clever here.", + 'info' => 'Meh', + 'format' => 'filtered_html', + ], + ]; + // The expected results are identical to the source data. + $tests[0]['expected_data'] = $tests[0]['source_data']['block_custom']; + + return $tests; + } + +} diff --git a/tests/src/Kernel/Plugin/migrate/source/d7/BlockCustomTranslationTest.php b/tests/src/Kernel/Plugin/migrate/source/d7/BlockCustomTranslationTest.php new file mode 100644 index 0000000..6794975 --- /dev/null +++ b/tests/src/Kernel/Plugin/migrate/source/d7/BlockCustomTranslationTest.php @@ -0,0 +1,166 @@ + 1, + 'body' => 'box 1 body', + 'info' => 'box 1 title', + 'format' => '2', + ], + [ + 'bid' => 2, + 'body' => 'box 2 body', + 'info' => 'box 2 title', + 'format' => '2', + ], + [ + 'bid' => 4, + 'body' => 'box 2 body', + 'info' => 'box 2 title', + 'format' => '2', + ], + ]; + + $tests[0]['source_data']['i18n_string'] = [ + [ + 'lid' => 1, + 'objectid' => 1, + 'type' => 'block', + 'property' => 'title', + 'objectindex' => 1, + 'format' => 0, + ], + [ + 'lid' => 2, + 'objectid' => 1, + 'type' => 'block', + 'property' => 'body', + 'objectindex' => 1, + 'format' => 0, + ], + [ + 'lid' => 3, + 'objectid' => 2, + 'type' => 'block', + 'property' => 'body', + 'objectindex' => 2, + 'format' => 2, + ], + [ + 'lid' => 4, + 'objectid' => 4, + 'type' => 'block', + 'property' => 'body', + 'objectindex' => 4, + 'format' => 2, + ], + ]; + + $tests[0]['source_data']['locales_target'] = [ + [ + 'lid' => 1, + 'language' => 'fr', + 'translation' => 'fr - title translation', + 'plid' => 0, + 'plural' => 0, + 'i18n_status' => 0, + ], + [ + 'lid' => 2, + 'language' => 'fr', + 'translation' => 'fr - body translation', + 'plid' => 0, + 'plural' => 0, + 'i18n_status' => 0, + ], + [ + 'lid' => 3, + 'language' => 'zu', + 'translation' => 'zu - body translation', + 'plid' => 0, + 'plural' => 0, + 'i18n_status' => 0, + ], + ]; + + $tests[0]['source_data']['system'] = [ + [ + 'type' => 'module', + 'name' => 'system', + 'schema_version' => '7001', + 'status' => '1', + ], + ]; + + $tests[0]['expected_data'] = [ + [ + 'lid' => '1', + 'property' => 'title', + 'language' => 'fr', + 'translation' => 'fr - title translation', + 'bid' => '1', + 'format' => '2', + 'title_translated' => 'fr - title translation', + 'body_translated' => 'fr - body translation', + 'title' => 'box 1 title', + 'body' => 'box 1 body', + ], + [ + 'lid' => '2', + 'property' => 'body', + 'language' => 'fr', + 'translation' => 'fr - body translation', + 'bid' => '1', + 'format' => '2', + 'title_translated' => 'fr - title translation', + 'body_translated' => 'fr - body translation', + 'title' => 'box 1 title', + 'body' => 'box 1 body', + ], + [ + 'lid' => '3', + 'property' => 'body', + 'language' => 'zu', + 'translation' => 'zu - body translation', + 'bid' => '2', + 'format' => '2', + 'title_translated' => NULL, + 'body_translated' => 'zu - body translation', + 'title' => 'box 2 title', + 'body' => 'box 2 body', + ], + ]; + + return $tests; + } + +} diff --git a/tests/src/Kernel/Views/FieldTypeTest.php b/tests/src/Kernel/Views/FieldTypeTest.php new file mode 100644 index 0000000..156a957 --- /dev/null +++ b/tests/src/Kernel/Views/FieldTypeTest.php @@ -0,0 +1,77 @@ +installEntitySchema('block_content'); + BlockContentType::create([ + 'id' => 'basic', + 'label' => 'basic', + 'revision' => FALSE, + ]); + $block_content = BlockContent::create([ + 'info' => $this->randomMachineName(), + 'type' => 'basic', + 'langcode' => 'en', + ]); + $block_content->save(); + + $expected_result[] = [ + 'id' => $block_content->id(), + 'type' => $block_content->bundle(), + ]; + $column_map = [ + 'id' => 'id', + 'type:target_id' => 'type', + ]; + + $view = Views::getView('test_field_type'); + $this->executeView($view); + $this->assertIdenticalResultset($view, $expected_result, $column_map, 'The correct block_content type was displayed.'); + } + +} diff --git a/tests/src/Kernel/Views/RevisionRelationshipsTest.php b/tests/src/Kernel/Views/RevisionRelationshipsTest.php new file mode 100644 index 0000000..05d56f1 --- /dev/null +++ b/tests/src/Kernel/Views/RevisionRelationshipsTest.php @@ -0,0 +1,102 @@ +installEntitySchema('block_content'); + ViewTestData::createTestViews(static::class, ['block_content_test_views']); + + BlockContentType::create([ + 'id' => 'basic', + 'label' => 'basic', + 'revision' => TRUE, + ]); + $block_content = BlockContent::create([ + 'info' => $this->randomMachineName(), + 'type' => 'basic', + 'langcode' => 'en', + ]); + $block_content->save(); + // Create revision of the block_content. + $block_content_revision = clone $block_content; + $block_content_revision->setNewRevision(); + $block_content_revision->save(); + $column_map = [ + 'revision_id' => 'revision_id', + 'id_1' => 'id_1', + 'block_content_field_data_block_content_field_revision_id' => 'block_content_field_data_block_content_field_revision_id', + ]; + + // Here should be two rows. + $view = Views::getView('test_block_content_revision_id'); + $view->preview(NULL, [$block_content->id()]); + $resultset_id = [ + [ + 'revision_id' => '1', + 'id_1' => '1', + 'block_content_field_data_block_content_field_revision_id' => '1', + ], + [ + 'revision_id' => '2', + 'id_1' => '1', + 'block_content_field_data_block_content_field_revision_id' => '1', + ], + ]; + $this->assertIdenticalResultset($view, $resultset_id, $column_map); + + // There should be only one row with active revision 2. + $view_revision = Views::getView('test_block_content_revision_revision_id'); + $view_revision->preview(NULL, [$block_content->id()]); + $resultset_revision_id = [ + [ + 'revision_id' => '2', + 'id_1' => '1', + 'block_content_field_data_block_content_field_revision_id' => '1', + ], + ]; + $this->assertIdenticalResultset($view_revision, $resultset_revision_id, $column_map); + } + +} diff --git a/tests/src/Kernel/Views/RevisionUserTest.php b/tests/src/Kernel/Views/RevisionUserTest.php new file mode 100644 index 0000000..a72e079 --- /dev/null +++ b/tests/src/Kernel/Views/RevisionUserTest.php @@ -0,0 +1,153 @@ + 'id', + 'revision_id' => 'revision_id', + 'revision_user' => 'revision_user', + ]; + + /** + * {@inheritdoc} + */ + protected function setUp($import_test_views = TRUE): void { + parent::setUp($import_test_views); + + $this->installEntitySchema('block_content'); + $this->installEntitySchema('user'); + + if ($import_test_views) { + ViewTestData::createTestViews(get_class($this), ['block_content_test_views']); + } + } + + /** + * Tests the block_content_revision_user relationship. + */ + public function testRevisionUser(): void { + $primary_author = $this->createUser(); + $secondary_author = $this->createUser(); + + $block_content_type = BlockContentType::create([ + 'id' => 'basic', + 'label' => 'basic block', + ]); + $block_content_type->save(); + + $block_content = BlockContent::create([ + 'info' => 'Test block content', + 'type' => 'basic', + ]); + $block_content->setRevisionUserId($primary_author->id()); + $block_content->save(); + + $view = Views::getView('test_block_content_revision_user'); + $this->executeView($view); + $this->assertIdenticalResultset($view, [ + [ + 'id' => 1, + 'revision_id' => 1, + 'revision_user' => $primary_author->id(), + ], + ], static::$columnMap); + + // Test results shows the revision author. + $block_content->setRevisionUser($secondary_author); + $block_content->setNewRevision(); + $block_content->save(); + + $view = Views::getView('test_block_content_revision_user'); + $this->executeView($view); + $this->assertIdenticalResultset($view, [ + [ + 'id' => 1, + 'revision_id' => 2, + 'revision_user' => $secondary_author->id(), + ], + ], static::$columnMap); + + // Build a larger dataset to allow filtering. + $block_content2_title = $this->randomString(); + $block_content2 = BlockContent::create([ + 'info' => $block_content2_title, + 'type' => 'basic', + ]); + $block_content2->save(); + $block_content2->setRevisionUser($primary_author); + $block_content2->setNewRevision(); + $block_content2->save(); + + $view = Views::getView('test_block_content_revision_user'); + $this->executeView($view); + $this->assertIdenticalResultset($view, [ + [ + 'id' => 1, + 'revision_id' => 2, + 'revision_user' => $secondary_author->id(), + ], + [ + 'id' => 2, + 'revision_id' => 4, + 'revision_user' => $primary_author->id(), + ], + ], static::$columnMap); + + // Test filter by revision_author. + $view = Views::getView('test_block_content_revision_user'); + $view->initHandlers(); + $view->filter['revision_user']->value = [$secondary_author->id()]; + $this->executeView($view); + $this->assertIdenticalResultset($view, [ + [ + 'id' => 1, + 'revision_id' => 2, + 'revision_user' => $secondary_author->id(), + ], + ], static::$columnMap); + } + +} diff --git a/tests/src/Unit/Access/AccessGroupAndTest.php b/tests/src/Unit/Access/AccessGroupAndTest.php new file mode 100644 index 0000000..b9fd73b --- /dev/null +++ b/tests/src/Unit/Access/AccessGroupAndTest.php @@ -0,0 +1,57 @@ +account = $this->prophesize(AccountInterface::class)->reveal(); + } + + /** + * @covers \Drupal\block_content\Access\AccessGroupAnd + */ + public function testGroups(): void { + $allowedAccessible = $this->createAccessibleDouble(AccessResult::allowed()); + $forbiddenAccessible = $this->createAccessibleDouble(AccessResult::forbidden()); + $neutralAccessible = $this->createAccessibleDouble(AccessResult::neutral()); + + // Ensure that groups with no dependencies return a neutral access result. + $this->assertTrue((new AccessGroupAnd())->access('view', $this->account, TRUE)->isNeutral()); + + $andNeutral = new AccessGroupAnd(); + $andNeutral->addDependency($allowedAccessible)->addDependency($neutralAccessible); + $this->assertTrue($andNeutral->access('view', $this->account, TRUE)->isNeutral()); + + $andForbidden = $andNeutral; + $andForbidden->addDependency($forbiddenAccessible); + $this->assertTrue($andForbidden->access('view', $this->account, TRUE)->isForbidden()); + + // Ensure that groups added to other groups works. + $andGroupsForbidden = new AccessGroupAnd(); + $andGroupsForbidden->addDependency($andNeutral)->addDependency($andForbidden); + $this->assertTrue($andGroupsForbidden->access('view', $this->account, TRUE)->isForbidden()); + // Ensure you can add a non-group accessible object. + $andGroupsForbidden->addDependency($allowedAccessible); + $this->assertTrue($andGroupsForbidden->access('view', $this->account, TRUE)->isForbidden()); + } + +} diff --git a/tests/src/Unit/Access/AccessibleTestingTrait.php b/tests/src/Unit/Access/AccessibleTestingTrait.php new file mode 100644 index 0000000..a407e23 --- /dev/null +++ b/tests/src/Unit/Access/AccessibleTestingTrait.php @@ -0,0 +1,38 @@ +prophesize(AccessibleInterface::class); + $accessible->access('view', $this->account, TRUE) + ->willReturn($accessResult); + return $accessible->reveal(); + } + +} diff --git a/tests/src/Unit/Access/DependentAccessTest.php b/tests/src/Unit/Access/DependentAccessTest.php new file mode 100644 index 0000000..55c9a89 --- /dev/null +++ b/tests/src/Unit/Access/DependentAccessTest.php @@ -0,0 +1,161 @@ +account = $this->prophesize(AccountInterface::class)->reveal(); + $this->forbidden = $this->createAccessibleDouble(AccessResult::forbidden('Because I said so')); + $this->neutral = $this->createAccessibleDouble(AccessResult::neutral('I have no opinion')); + } + + /** + * Tests that the previous dependency is replaced when using set. + * + * @covers ::setAccessDependency + * + * @dataProvider providerTestSetFirst + */ + public function testSetAccessDependency($use_set_first): void { + $testRefinable = new RefinableDependentAccessTraitTestClass(); + + if ($use_set_first) { + $testRefinable->setAccessDependency($this->forbidden); + } + else { + $testRefinable->addAccessDependency($this->forbidden); + } + $accessResult = $testRefinable->getAccessDependency()->access('view', $this->account, TRUE); + $this->assertTrue($accessResult->isForbidden()); + $this->assertEquals('Because I said so', $accessResult->getReason()); + + // Calling setAccessDependency() replaces the existing dependency. + $testRefinable->setAccessDependency($this->neutral); + $dependency = $testRefinable->getAccessDependency(); + $this->assertNotInstanceOf(AccessGroupAnd::class, $dependency); + $accessResult = $dependency->access('view', $this->account, TRUE); + $this->assertTrue($accessResult->isNeutral()); + $this->assertEquals('I have no opinion', $accessResult->getReason()); + } + + /** + * Tests merging a new dependency with existing non-group access dependency. + * + * @dataProvider providerTestSetFirst + */ + public function testMergeNonGroup($use_set_first): void { + $testRefinable = new RefinableDependentAccessTraitTestClass(); + if ($use_set_first) { + $testRefinable->setAccessDependency($this->forbidden); + } + else { + $testRefinable->addAccessDependency($this->forbidden); + } + + $accessResult = $testRefinable->getAccessDependency()->access('view', $this->account, TRUE); + $this->assertTrue($accessResult->isForbidden()); + $this->assertEquals('Because I said so', $accessResult->getReason()); + + $testRefinable->addAccessDependency($this->neutral); + /** @var \Drupal\block_content\Access\AccessGroupAnd $dependency */ + $dependency = $testRefinable->getAccessDependency(); + // Ensure the new dependency create a new AND group when merged. + $this->assertInstanceOf(AccessGroupAnd::class, $dependency); + $dependencies = $dependency->getDependencies(); + $accessResultForbidden = $dependencies[0]->access('view', $this->account, TRUE); + $this->assertTrue($accessResultForbidden->isForbidden()); + $this->assertEquals('Because I said so', $accessResultForbidden->getReason()); + $accessResultNeutral = $dependencies[1]->access('view', $this->account, TRUE); + $this->assertTrue($accessResultNeutral->isNeutral()); + $this->assertEquals('I have no opinion', $accessResultNeutral->getReason()); + } + + /** + * Tests merging a new dependency with an existing access group dependency. + * + * @dataProvider providerTestSetFirst + */ + public function testMergeGroup($use_set_first): void { + $andGroup = new AccessGroupAnd(); + $andGroup->addDependency($this->forbidden); + $testRefinable = new RefinableDependentAccessTraitTestClass(); + if ($use_set_first) { + $testRefinable->setAccessDependency($andGroup); + } + else { + $testRefinable->addAccessDependency($andGroup); + } + + $testRefinable->addAccessDependency($this->neutral); + /** @var \Drupal\block_content\Access\AccessGroupAnd $dependency */ + $dependency = $testRefinable->getAccessDependency(); + + // Ensure the new dependency is merged with the existing group. + $this->assertInstanceOf(AccessGroupAnd::class, $dependency); + $dependencies = $dependency->getDependencies(); + $accessResultForbidden = $dependencies[0]->access('view', $this->account, TRUE); + $this->assertTrue($accessResultForbidden->isForbidden()); + $this->assertEquals('Because I said so', $accessResultForbidden->getReason()); + $accessResultNeutral = $dependencies[1]->access('view', $this->account, TRUE); + $this->assertTrue($accessResultNeutral->isNeutral()); + $this->assertEquals('I have no opinion', $accessResultNeutral->getReason()); + } + + /** + * Data provider for all test methods. + * + * Provides test cases for calling setAccessDependency() or + * mergeAccessDependency() first. A call to either should behave the same on a + * new RefinableDependentAccessInterface object. + */ + public static function providerTestSetFirst() { + return [ + [TRUE], + [FALSE], + ]; + } + +} + +/** + * Test class that implements RefinableDependentAccessInterface. + */ +class RefinableDependentAccessTraitTestClass implements RefinableDependentAccessInterface { + + use RefinableDependentAccessTrait; + +} diff --git a/tests/src/Unit/Menu/BlockContentLocalTasksTest.php b/tests/src/Unit/Menu/BlockContentLocalTasksTest.php new file mode 100644 index 0000000..f38e6d6 --- /dev/null +++ b/tests/src/Unit/Menu/BlockContentLocalTasksTest.php @@ -0,0 +1,89 @@ +directoryList = [ + 'system' => 'core/modules/system', + 'block_content' => 'core/modules/block_content', + ]; + parent::setUp(); + + $config_factory = $this->getConfigFactoryStub([ + 'system.theme' => ['default' => 'test_c'], + ]); + + $themes = []; + $themes['test_a'] = (object) [ + 'status' => 0, + ]; + $themes['test_b'] = (object) [ + 'status' => 1, + 'info' => [ + 'name' => 'test_b', + ], + ]; + $themes['test_c'] = (object) [ + 'status' => 1, + 'info' => [ + 'name' => 'test_c', + ], + ]; + $theme_handler = $this->createMock('Drupal\Core\Extension\ThemeHandlerInterface'); + $theme_handler->expects($this->any()) + ->method('listInfo') + ->willReturn($themes); + + // Add services required for block local tasks. + $entity_type_manager = $this->createMock(EntityTypeManagerInterface::class); + $entity_type_manager->expects($this->any()) + ->method('getDefinitions') + ->willReturn([]); + + $container = new ContainerBuilder(); + $container->set('config.factory', $config_factory); + $container->set('theme_handler', $theme_handler); + $container->set('entity_type.manager', $entity_type_manager); + \Drupal::setContainer($container); + } + + /** + * Checks block_content listing local tasks. + * + * @dataProvider getBlockContentListingRoutes + */ + public function testBlockContentListLocalTasks($route): void { + $this->assertLocalTasks($route, [ + 0 => [ + 'system.admin_content', + 'entity.block_content.collection', + ], + ]); + } + + /** + * Provides a list of routes to test. + */ + public static function getBlockContentListingRoutes() { + return [ + ['entity.block_content.collection', 'system.admin_content'], + ]; + } + +}