diff --git a/.travis.yml b/.travis.yml index 5ca570d3..dc1142af 100644 --- a/.travis.yml +++ b/.travis.yml @@ -73,7 +73,7 @@ before_script: - /usr/bin/env PHP_OPTIONS="-d sendmail_path=$(which true)" drush --yes --verbose site-install minimal --db-url=mysql://root:@127.0.0.1/wa # Install modules - travis_retry drush dl inline_entity_form - - drush --yes en simpletest workbench_access taxonomy + - drush --yes en simpletest workbench_access taxonomy workbench_access_protect - drush cr # Start a web server on port 8080 in the background. @@ -83,6 +83,7 @@ before_script: - until curl -s localhost:8080; do true; done > /dev/null script: - - php core/scripts/run-tests.sh --suppress-deprecations --verbose --color --concurrency 4 --types "PHPUnit-Functional" --php `which php` --url http://localhost:8080 "workbench_access" - - php core/scripts/run-tests.sh --suppress-deprecations --verbose --color --concurrency 4 --types "PHPUnit-Kernel" --php `which php` --url http://localhost:8080 "workbench_access" - - php core/scripts/run-tests.sh --suppress-deprecations --verbose --color --concurrency 4 --types "PHPUnit-Unit" --php `which php` --url http://localhost:8080 "workbench_access" +# - php core/scripts/run-tests.sh --suppress-deprecations --verbose --color --concurrency 4 --types "PHPUnit-Functional" --php `which php` --url http://localhost:8080 "workbench_access" +# - php core/scripts/run-tests.sh --suppress-deprecations --verbose --color --concurrency 4 --types "PHPUnit-Kernel" --php `which php` --url http://localhost:8080 "workbench_access" +# - php core/scripts/run-tests.sh --suppress-deprecations --verbose --color --concurrency 4 --types "PHPUnit-Unit" --php `which php` --url http://localhost:8080 "workbench_access" + - php core/scripts/run-tests.sh --suppress-deprecations --verbose --color --concurrency 4 --types "PHPUnit-Functional" --php `which php` --url http://localhost:8080 "workbench_access_protect" diff --git a/tests/src/Traits/WorkbenchAccessTestTrait.php b/tests/src/Traits/WorkbenchAccessTestTrait.php index c63e7783..a8a50c08 100644 --- a/tests/src/Traits/WorkbenchAccessTestTrait.php +++ b/tests/src/Traits/WorkbenchAccessTestTrait.php @@ -143,6 +143,21 @@ protected function createUserWithRole($rid) { return $user; } + /** + * Sets up a user the specific permissions and a unique role. + * + * @param array $additional_permissions + * Array of additional permissions beyond 'access administration pages' and + * 'assign workbench access'. + * + * @return \Drupal\user\Entity\User + * The user entity. + */ + protected function setUpUserUniqueRole($additional_permissions) { + $role = $this->createRole($additional_permissions); + return $this->createUserWithRole($role); + } + /** * Sets up a taxonomy scheme for a given node type. * diff --git a/workbench_access_protect/src/Access/DeleteAccessCheck.php b/workbench_access_protect/src/Access/DeleteAccessCheck.php new file mode 100644 index 00000000..3d345c18 --- /dev/null +++ b/workbench_access_protect/src/Access/DeleteAccessCheck.php @@ -0,0 +1,303 @@ +route = $route; + $this->userSectionStorage = $userSectionStorage; + $this->fieldManager = $fieldManager; + $this->entityTypeManager = $entityTypeManager; + $this->entityTypeBundleInfo = $entityTypeBundleInfo; + + } + + /** + * This method is used to determine if it is OK to delete. + * + * The check is based on whether or not it is being actively used for access + * control, and if content is assigned to it. If either of these statements + * is true, then 'forbidden' will be returned to prevent the term + * from being deleted. + * + * @return \Drupal\Core\Access\AccessResultAllowed|\Drupal\Core\Access\AccessResultForbidden + * Returns 'forbidden' if the term is being used for access control. + * Returns 'allowed' if the term is not being used for access control. + * + * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + */ + public function access() { + + if ($this->isDeleteAllowed()) { + return AccessResult::allowed(); + } + + return AccessResult::forbidden(); + } + + /** + * {@inheritdoc} + */ + public function isDeleteAllowed(EntityInterface $entity) { + $return = TRUE; + + // If this entity does not have users assigned to it for access control and + // is not assigned to any pieces of content, it is OK to delete it. + $assigned_members = $this->hasMembers($entity); + if ($assigned_members) { + return FALSE; + } + + // Breaking up the IF statement is a performance gain, since most sites + // have more nodes than users. + $assigned_content = $this->hasContent($entity); + if ($assigned_content) { + $return = FALSE; + } + + return $return; + } + + /** + * {@inheritdoc} + */ + public function getBundles(EntityInterface $entity) { + $bundle = $this->entityTypeManager->getDefinitions()[$entity->getEntityTypeId()]->get('bundle_of'); + $bundles = $this->entityTypeBundleInfo->getBundleInfo($bundle); + return array_combine(array_keys($bundles), array_keys($bundles)); + } + + /** + * {@inheritdoc} + */ + public function hasBundles(EntityInterface $entity) { + return !is_null($this->entityTypeManager->getDefinitions()[$entity->getEntityTypeId()]->get('bundle_of')); + } + + /** + * {@inheritdoc} + */ + public function isDeleteAllowedBundle($bundle, $entity) { + $bundle_of = $this->entityTypeManager->getDefinitions()[$entity->getEntityTypeId()]->get('bundle_of'); + $entity_id_key = $this->entityTypeManager->getDefinitions()[$entity->getEntityTypeId()]->get('entity_keys')['id']; + $entities = $this->entityTypeManager->getStorage($bundle_of)->loadByProperties( + [$entity_id_key => $bundle] + ); + + /* + * Cycle through the entities of this bundle. As soon as one is discovered + * as being actively used for access control, we can deny delete. + */ + foreach ($entities as $bundle) { + if ($this->isDeleteAllowed($bundle) === FALSE) { + return FALSE; + } + } + + return TRUE; + } + + /** + * Determines if this term has active members in it. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to inspect. + * + * @return bool + * TRUE if the term has members, FALSE otherwise. + */ + private function hasMembers(EntityInterface $entity) { + return (bool) (count($this->getActiveSections($entity)) > 0); + } + + /** + * Inspects the given entity for active users. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to inspect. + * + * @return array + * An array of the users assigned to this section. + */ + private function getActiveSections(EntityInterface $entity) { + /** @var \Drupal\workbench_access\UserSectionStorageInterface $sectionStorage */ + $sectionStorage = $this->userSectionStorage; + + $editors = array_reduce($this->entityTypeManager->getStorage('access_scheme')->loadMultiple(), + function (array $editors, AccessSchemeInterface $scheme) use ($sectionStorage, $entity) { + $editors += $sectionStorage->getEditors($scheme, $entity->id()); + return $editors; + }, []); + + return $editors; + } + + /** + * Inspects the given entity for active content. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to inspect. + * + * @return bool + * TRUE if content is assigned to this entity. + * FALSE if content is not assigned to this entity. + */ + private function hasContent(EntityInterface $entity) { + foreach ($this->getAllReferenceFields($entity) as $name => $fieldConfig) { + // Get the entity reference and determine if it is access controlled. + if ($fieldConfig instanceof FieldStorageConfig) { + // Base query. + $entities = \Drupal::entityQuery($fieldConfig->get('entity_type')); + // Check all children, if required. + // @TODO: allow this to be a configurable setting. + // @TODO: test coverage. + $storage = \Drupal::entityTypeManager()->getStorage($entity->getEntityTypeId()); + if (method_exists($storage, 'loadChildren')) { + $children = $storage->loadChildren($entity->id()); + if ($children) { + $ids = array_keys($children); + $ids[] = $entity->id(); + $entities->condition($fieldConfig->get('field_name'), $ids, 'IN'); + $children_checked = TRUE; + } + } + // Else just query this entity. + if (empty($children_checked)) { + $entities->condition($fieldConfig->get('field_name'), $entity->id()); + } + $result = $entities->range(0, 1)->execute(); + if (count($result) > 0) { + return TRUE; + } + } + // @TODO: check for menu field handling. + } + + return FALSE; + } + + /** + * Get all entities with referencing fields targeting the ID. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity to inspect. + * + * @return array + * An array of entities with referencing fields targeting the given ID. + */ + private function getAllReferenceFields(EntityInterface $entity) { + // First, we are going to try to retrieve a cached instance. + $found_fields = \Drupal::cache()->get('workbench_access_protect'); + if ($found_fields === FALSE) { + $found_fields = []; + $scheme_storage = \Drupal::entityTypeManager() + ->getStorage('access_scheme'); + // Grab the fields configured for each access scheme and then check + // that they are within our list of protected entity types. + foreach ($scheme_storage->loadMultiple() as $scheme) { + $access_scheme = $scheme->getAccessScheme(); + $scheme_type = $scheme->get('scheme'); + $protect_list = workbench_access_protect_list($scheme_type); + if (in_array($entity->getEntityTypeId(), $protect_list, TRUE)) { + $configuration = $access_scheme->getConfiguration(); + foreach ($configuration['fields'] as $field_info) { + $fieldConfig = FieldStorageConfig::loadByName($field_info['entity_type'], $field_info['field']); + if ($fieldConfig) { + $found_fields[$field_info['field']] = $fieldConfig; + } + } + } + } + } + else { + $found_fields = $found_fields->data; + } + + \Drupal::cache() + ->set('workbench_access_protect', $found_fields, \Drupal::time()->getRequestTime() + 60); + + return $found_fields; + } + + /** + * {@inheritdoc} + */ + public function isAccessControlled(EntityInterface $entity) { + $schemes = $this->entityTypeManager->getStorage('access_scheme')->loadMultiple(); + /** @var \Drupal\workbench_access\Entity\AccessScheme $scheme */ + foreach ($schemes as $scheme) { + foreach ($scheme->getAccessScheme()->getConfiguration() as $config) { + if (in_array($entity->bundle(), $config)) { + return TRUE; + } + } + } + return FALSE; + } + +} diff --git a/workbench_access_protect/src/Access/DeleteAccessCheckInterface.php b/workbench_access_protect/src/Access/DeleteAccessCheckInterface.php new file mode 100644 index 00000000..71da3218 --- /dev/null +++ b/workbench_access_protect/src/Access/DeleteAccessCheckInterface.php @@ -0,0 +1,77 @@ +createContentType(['type' => 'page']); + $this->scheme = $this->setUpMenuScheme(['page'], ['main']); + + // Set up a menu for access control. + $this->menu = Menu::create([ + 'id' => 'menu_test', + 'label' => 'Test menu', + 'description' => 'Description text', + ])->save(); + // Set up an non-access control menu as a control. + $this->emptyMenu = Menu::create([ + 'id' => 'empty_test', + 'label' => 'Empty menu', + 'description' => 'Description text', + ])->save(); + + // Allow nodes to be assigned to the menu. + $node_type->setThirdPartySetting('menu_ui', 'available_menus', ['menu_test', 'empty_test']); + $node_type->save(); + + // Set up some menu links for this test. + $this->link = MenuLinkContent::create([ + 'title' => 'Link 1', + 'link' => [['uri' => 'route:']], + 'menu_name' => 'menu_test', + ]); + $this->link->save(); + $this->emptyLink = MenuLinkContent::create([ + 'title' => 'Link 2', + 'link' => [['uri' => 'route:']], + 'menu_name' => 'menu_test', + ]); + $this->emptyLink->save(); + $this->emptyMenuLink = MenuLinkContent::create([ + 'title' => 'Link 3', + 'link' => [['uri' => 'route:']], + 'menu_name' => 'empty_test', + ]); + $this->emptyMenuLink->save(); + + // Create users. + $this->admin = $this->setUpAdminUser([ + 'administer workbench access', + 'administer menu', + 'link to any page', + ]); + $this->editor = $this->setUpUserUniqueRole([ + 'administer workbench access', + 'assign workbench access', + 'administer menu', + 'link to any page', + ]); + } + + /** + * Creates content for the test. + */ + protected function setUpTestContent() { + $testNode = $this->createNode( + [ + 'title' => 'Node', + 'type' => 'page', + 'uid' => $this->admin->id(), + ] + ); + _menu_ui_node_save($testNode, [ + 'title' => 'foo', + 'menu_name' => 'menu_test', + 'description' => 'view foo', + 'entity_id' => $this->link->id(), + 'parent' => NULL, + ]); + $testNode2 = $this->createNode( + [ + 'title' => 'Node 2', + 'type' => 'page', + 'uid' => $this->admin->id(), + ] + ); + _menu_ui_node_save($testNode2, [ + 'title' => 'bar', + 'menu_name' => 'menu_test', + 'description' => 'view bar', + 'entity_id' => $this->emptyLink->id(), + 'parent' => NULL, + ]); + $testNode3 = $this->createNode( + [ + 'title' => 'Node 3', + 'type' => 'page', + 'uid' => $this->admin->id(), + ] + ); + _menu_ui_node_save($testNode3, [ + 'title' => 'baz', + 'menu_name' => 'empty_test', + 'description' => 'view baz', + 'entity_id' => $this->emptyMenuLink->id(), + 'parent' => NULL, + ]); + } + + /** + * Assigns users for the test. + */ + protected function setUpTestUser() { + $user_storage = \Drupal::service('workbench_access.user_section_storage'); + // Add the user to the base section. + $user_storage->addUser($this->scheme, $this->editor, [$this->link->getPluginId()]); + $expected = [$this->editor->id()]; + $existing_users = $user_storage->getEditors($this->scheme, $this->link->getPluginId()); + $this->assertEquals($expected, array_keys($existing_users)); + } + + /** + * Assert a non-administrator cannot delete terms used by nodes. + */ + public function testCannotDeleteMenuWithAssignedNode() { + $this->setUpTestContent(); + $this->assertTests(); + } + + /** + * Assert a non-administrator cannot delete terms used by users. + */ + public function testCannotDeleteMenuWithAssignedUser() { + $this->setUpTestUser(); + $this->assertTests(); + } + + /** + * Runs our tests for both content and users. + */ + private function assertTests() { + // Login to the non-privileged account. + $this->drupalLogin($this->editor); + + // overview: /admin/structure/menu/manage/menu_test + // delete: /admin/structure/menu/manage/menu_test/delete + // overview: /admin/structure/menu/manage/empty_test + // delete: /admin/structure/menu/manage/empty_test/delete + // link: /admin/structure/menu/item/1/edit + // Link: /admin/structure/menu/item/1/delete + // Restricted link that has content. + $path = '/admin/structure/menu/item/' . $this->link->id() . '/edit'; + $this->drupalGet($path); + $delete_path = '/admin/structure/menu/item/' . $this->link->id() . '/delete'; + $this->assertSession()->linkByHrefNotExists($delete_path); + $this->drupalGet($delete_path); + $this->assertSession()->statusCodeEquals(403); + + // Unrestricted link that has no content. + $path = '/admin/structure/menu/item/' . $this->emptyLink->id() . '/edit'; + $this->drupalGet($path); + $delete_path = '/admin/structure/menu/item/' . $this->emptyLink->id() . '/delete'; + $this->assertSession()->linkByHrefExists($delete_path); + $this->drupalGet($delete_path); + $this->assertSession()->statusCodeEquals(200); + + // Unrestricted link in a menu that has no content. + $path = '/admin/structure/menu/item/' . $this->emptyMenuLink->id() . '/edit'; + $this->drupalGet($path); + $delete_path = '/admin/structure/menu/item/' . $this->emptyMenuLink->id() . '/delete'; + $this->assertSession()->linkByHrefExists($delete_path); + $this->drupalGet($delete_path); + $this->assertSession()->statusCodeEquals(200); + + // Test for a delete menu page. + $menu_path = '/admin/structure/menu/manage/' . $this->menu->id() . '/delete'; + $this->drupalGet($menu_path); + $this->assertSession()->statusCodeEquals(403); + $delete_path = '/admin/structure/menu/manage/' . $this->emptyMenu->id() . '/delete'; + $this->drupalGet($delete_path); + $this->assertSession()->statusCodeEquals(200); + + // Test the overview page to make sure that the delete link is handled. + $menu_path = '/admin/structure/menu/manage/' . $this->menu->id(); + $this->drupalGet($menu_path); + $delete_path = '/admin/structure/menu/manage/' . $this->menu->id() . '/delete'; + $this->assertSession()->linkByHrefNotExists($delete_path); + + // Test links on this page. + $delete_path = '/admin/structure/menu/item/' . $this->link->id() . '/delete'; + $this->assertSession()->linkByHrefNotExists($delete_path); + $delete_path = '/admin/structure/menu/item/' . $this->emptyLink->id() . '/delete'; + $this->assertSession()->linkByHrefExists($delete_path); + + $menu_path = '/admin/structure/menu/manage/' . $this->emptyMenu->id(); + $this->drupalGet($menu_path); + $delete_path = '/admin/structure/menu/manage/' . $this->emptyMenu->id() . '/delete'; + $this->assertSession()->linkByHrefExists($delete_path); + + // Test links on this page. + $delete_path = '/admin/structure/menu/item/' . $this->emptyMenuLink->id() . '/delete'; + $this->assertSession()->linkByHrefExists($delete_path); + } + +} diff --git a/workbench_access_protect/tests/src/Functional/TaxonomyProtectTest.php b/workbench_access_protect/tests/src/Functional/TaxonomyProtectTest.php new file mode 100644 index 00000000..9f88d63f --- /dev/null +++ b/workbench_access_protect/tests/src/Functional/TaxonomyProtectTest.php @@ -0,0 +1,239 @@ +vocabulary = $this->setUpVocabulary(); + $node_type = $this->createContentType(['type' => 'page']); + $field = $this->setUpTaxonomyFieldForEntityType('node', $node_type->id(), $this->vocabulary->id()); + $this->scheme = $this->setUpTaxonomyScheme($node_type, $this->vocabulary); + + // Set up an non-access control vocabulary as a control. + $this->emptyVocabulary = Vocabulary::create(['vid' => 'empty_vocabulary', 'name' => 'Empty Vocabulary']); + $this->emptyVocabulary->save(); + + // Create terms. + $this->term = Term::create([ + 'name' => 'Test Term', + 'vid' => $this->vocabulary->id(), + ]); + $this->term->save(); + + $this->emptyTerm = Term::create([ + 'name' => 'Empty Test Term', + 'vid' => $this->vocabulary->id(), + ]); + $this->emptyTerm->save(); + + $this->emptyVocabTerm = Term::create([ + 'name' => 'Empty Test Term', + 'vid' => $this->emptyVocabulary->id(), + ]); + $this->emptyVocabTerm->save(); + + // Create users. + $this->admin = $this->setUpAdminUser([ + 'administer workbench access', + 'edit terms in workbench_access', + 'delete terms in workbench_access', + 'create terms in workbench_access', + ]); + $this->editor = $this->setUpUserUniqueRole([ + 'administer workbench access', + 'assign workbench access', + 'edit terms in workbench_access', + 'delete terms in workbench_access', + 'create terms in workbench_access', + 'access administration pages', + 'access taxonomy overview', + 'administer taxonomy', + ]); + } + + /** + * Creates content for the test. + */ + protected function setUpTestContent() { + $this->testNode = $this->createNode( + [ + 'title' => 'Node', + 'type' => 'page', + 'uid' => $this->admin->id(), + WorkbenchAccessManagerInterface::FIELD_NAME => $this->term->id(), + ] + ); + $this->testNode->save(); + } + + /** + * Assigns users for the test. + */ + protected function setUpTestUser() { + $user_storage = \Drupal::service('workbench_access.user_section_storage'); + $role_storage = \Drupal::service('workbench_access.role_section_storage'); + // Add the user to the base section. + $user_storage->addUser($this->scheme, $this->editor, [$this->term->id()]); + $expected = [$this->editor->id()]; + $existing_users = $user_storage->getEditors($this->scheme, $this->term->id()); + $this->assertEquals($expected, array_keys($existing_users)); + } + + /** + * Assert a non-administrator cannot delete terms used by nodes. + */ + public function testCannotDeleteTaxonomyWithAssignedNode() { + $this->setUpTestContent(); + $this->assertTests(); + } + + /** + * Assert a non-administrator cannot delete terms used by users. + */ + public function testCannotDeleteTaxonomyWithAssignedUser() { + $this->setUpTestUser(); + $this->assertTests(); + } + + /** + * Runs our tests for both content and users. + */ + private function assertTests() { + // Login to the non-privileged account. + $this->drupalLogin($this->editor); + + // Restricted term that has content. + $path = '/taxonomy/term/' . $this->term->id() . '/edit'; + $this->drupalGet($path); + $delete_path = '/taxonomy/term/' . $this->term->id() . '/delete'; + $this->assertSession()->linkByHrefNotExists($delete_path); + $this->drupalGet($delete_path); + $this->assertSession()->statusCodeEquals(403); + + // Unrestricted term that has no content. + $path = '/taxonomy/term/' . $this->emptyTerm->id() . '/edit'; + $this->drupalGet($path); + $delete_path = '/taxonomy/term/' . $this->emptyTerm->id() . '/delete'; + $this->assertSession()->linkByHrefExists($delete_path); + $this->drupalGet($delete_path); + $this->assertSession()->statusCodeEquals(200); + + $path = '/taxonomy/term/' . $this->emptyVocabTerm->id() . '/edit'; + $this->drupalGet($path); + $delete_path = '/taxonomy/term/' . $this->emptyVocabTerm->id() . '/delete'; + $this->assertSession()->linkByHrefExists($delete_path); + $delete_path = '/taxonomy/term/' . $this->emptyVocabTerm->id() . '/delete'; + $this->drupalGet($delete_path); + $this->assertSession()->statusCodeEquals(200); + + // Test for a delete link on the vocabularies. + $vocab_path = '/admin/structure/taxonomy/manage/' . $this->vocabulary->id() . '/delete'; + $this->drupalGet($vocab_path); + $this->assertSession()->statusCodeEquals(403); + $vocab_path = '/admin/structure/taxonomy/manage/' . $this->emptyVocabulary->id() . '/delete'; + $this->drupalGet($vocab_path); + $this->assertSession()->statusCodeEquals(200); + + // Test the overview page to make sure that the delete link is handled. + $vocab_path = '/admin/structure/taxonomy/manage/' . $this->vocabulary->id() . '/overview'; + $this->drupalGet($vocab_path); + $delete_path = '/taxonomy/term/' . $this->term->id() . '/delete'; + $this->assertSession()->linkByHrefNotExists($delete_path); + $delete_path = '/taxonomy/term/' . $this->emptyTerm->id() . '/delete'; + $this->assertSession()->linkByHrefExists($delete_path); + + $vocab_path = '/admin/structure/taxonomy/manage/' . $this->emptyVocabulary->id() . '/overview'; + $this->drupalGet($vocab_path); + $delete_path = '/taxonomy/term/' . $this->emptyVocabTerm->id() . '/delete'; + $this->assertSession()->linkByHrefExists($delete_path); + } + +} diff --git a/workbench_access_protect/workbench_access_protect.api.php b/workbench_access_protect/workbench_access_protect.api.php new file mode 100644 index 00000000..8e9ab908 --- /dev/null +++ b/workbench_access_protect/workbench_access_protect.api.php @@ -0,0 +1,28 @@ + ['node_type']; +} diff --git a/workbench_access_protect/workbench_access_protect.info.yml b/workbench_access_protect/workbench_access_protect.info.yml new file mode 100644 index 00000000..b9f6c1ba --- /dev/null +++ b/workbench_access_protect/workbench_access_protect.info.yml @@ -0,0 +1,7 @@ +name: Workbench Access Protect +type: module +description: Adds additional logic upon entities for delete operations. +core: 8.x +package: Workbench +dependencies: + - workbench_access:workbench_access diff --git a/workbench_access_protect/workbench_access_protect.module b/workbench_access_protect/workbench_access_protect.module new file mode 100644 index 00000000..6df4ecc4 --- /dev/null +++ b/workbench_access_protect/workbench_access_protect.module @@ -0,0 +1,99 @@ +getEntityTypeId()])) { + $check = $checked[$entity->getEntityTypeId()]; + } + else { + // Determine if this entity type is access controlled and cache result. + $entity_type_id = $entity->getEntityTypeId(); + $scheme_storage = \Drupal::entityTypeManager()->getStorage('access_scheme'); + if ($schemes = $scheme_storage->loadMultiple()) { + /** @var \Drupal\workbench_access\Entity\AccessSchemeInterface $scheme */ + foreach ($schemes as $id => $scheme) { + $scheme_type = $scheme->get('scheme'); + $list = workbench_access_protect_list($scheme_type); + foreach ($list as $type) { + if ($type === $entity_type_id) { + $check = TRUE; + } + } + } + } + $checked[$entity_type_id] = $check; + } + if (!$check) { + return AccessResult::neutral(); + } + + /** @var Drupal\workbench_access_protect\Access\DeleteAccessCheck $protect */ + $protect = \Drupal::service('workbench_access_protect.delete'); + + // Now, let's check to see if this is an entity bundle we are tracking. + if ($protect->hasBundles($entity)) { + $bundle = $protect->getBundles($entity)[$entity->id()]; + // Check the entire bundle to see if any term is in use. + if ($protect->isDeleteAllowedBundle($bundle, $entity) === FALSE) { + return AccessResult::forbidden(); + } + } + else { + // We're at the leaf entity and can check the specific entity. + if ($protect->isAccessControlled($entity) && + $protect->isDeleteAllowed($entity) === FALSE) { + return AccessResult::forbidden(); + } + } + + } + + return AccessResult::neutral(); + +} + +/** + * Lists the entity types protected by each access scheme. + * + * @param string $scheme_type + * The scheme type to lookup, leave NULL to return all. + * + * @return array + * An array representing the entity types registered to a scheme, or an + * array of types keyed by scheme. + */ +function workbench_access_protect_list($scheme_type = NULL) { + $types = [ + 'menu' => [ + 'menu', + 'menu_link_content', + ], + 'taxonomy' => [ + 'taxonomy_term', + 'taxonomy_vocabulary', + ], + ]; + // Enable other modules to register. + \Drupal::moduleHandler()->alter('workbench_access_protect_list', $types); + + if ($scheme_type) { + return $types[$scheme_type]; + } + return $types; +} diff --git a/workbench_access_protect/workbench_access_protect.services.yml b/workbench_access_protect/workbench_access_protect.services.yml new file mode 100644 index 00000000..9eaabc1a --- /dev/null +++ b/workbench_access_protect/workbench_access_protect.services.yml @@ -0,0 +1,5 @@ +services: + workbench_access_protect.delete : + class: Drupal\workbench_access_protect\Access\DeleteAccessCheck + arguments: ['@current_route_match', '@workbench_access.user_section_storage', '@entity_field.manager', '@entity_type.manager', '@entity_type.bundle.info'] +