diff --git a/core/modules/forum/forum.module b/core/modules/forum/forum.module index 51472cf8c5..92037d98d6 100644 --- a/core/modules/forum/forum.module +++ b/core/modules/forum/forum.module @@ -7,11 +7,14 @@ use Drupal\comment\CommentInterface; use Drupal\comment\Plugin\Field\FieldType\CommentItemInterface; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\taxonomy\TermInterface; use Drupal\taxonomy\VocabularyInterface; use Drupal\user\Entity\User; @@ -349,6 +352,22 @@ function forum_form_node_form_alter(&$form, FormStateInterface $form_state, $for } /** + * Implements hook_ENTITY_TYPE_access() for taxonomy_term. + */ +function forum_taxonomy_term_access(TermInterface $term, $operation, AccountInterface $account) { + // Allow update access to terms in the forum vocabulary with the + // administer forums permission. + $forum_vid = \Drupal::config('forum.settings')->get('vocabulary'); + if ($operation === 'update' && $term->bundle() === $forum_vid) { + return AccessResult::allowedIfHasPermission($account, 'administer forums'); + } + + // No opinion. + return AccessResult::neutral(); +} + + +/** * Implements hook_preprocess_HOOK() for block templates. */ function forum_preprocess_block(&$variables) { diff --git a/core/modules/forum/src/Form/Overview.php b/core/modules/forum/src/Form/Overview.php index df6f94725a..8c374b5842 100644 --- a/core/modules/forum/src/Form/Overview.php +++ b/core/modules/forum/src/Form/Overview.php @@ -58,21 +58,27 @@ public function buildForm(array $form, FormStateInterface $form_state) { foreach (Element::children($form['terms']) as $key) { if (isset($form['terms'][$key]['#term'])) { + /** @var \Drupal\taxonomy\TermInterface $term */ $term = $form['terms'][$key]['#term']; $form['terms'][$key]['term']['#url'] = Url::fromRoute('forum.page', ['taxonomy_term' => $term->id()]); - unset($form['terms'][$key]['operations']['#links']['delete']); - $route_parameters = $form['terms'][$key]['operations']['#links']['edit']['url']->getRouteParameters(); - if (!empty($term->forum_container->value)) { - $form['terms'][$key]['operations']['#links']['edit']['title'] = $this->t('edit container'); - $form['terms'][$key]['operations']['#links']['edit']['url'] = Url::fromRoute('entity.taxonomy_term.forum_edit_container_form', $route_parameters); + if ($term->access('delete')) { + unset($form['terms'][$key]['operations']['#links']['delete']); } - else { - $form['terms'][$key]['operations']['#links']['edit']['title'] = $this->t('edit forum'); - $form['terms'][$key]['operations']['#links']['edit']['url'] = Url::fromRoute('entity.taxonomy_term.forum_edit_form', $route_parameters); + + if ($term->access('update')) { + $route_parameters = $form['terms'][$key]['operations']['#links']['edit']['url']->getRouteParameters(); + if (!empty($term->forum_container->value)) { + $form['terms'][$key]['operations']['#links']['edit']['title'] = $this->t('edit container'); + $form['terms'][$key]['operations']['#links']['edit']['url'] = Url::fromRoute('entity.taxonomy_term.forum_edit_container_form', $route_parameters); + } + else { + $form['terms'][$key]['operations']['#links']['edit']['title'] = $this->t('edit forum'); + $form['terms'][$key]['operations']['#links']['edit']['url'] = Url::fromRoute('entity.taxonomy_term.forum_edit_form', $route_parameters); + } + // We don't want the redirect from the link so we can redirect the + // delete action. + unset($form['terms'][$key]['operations']['#links']['edit']['query']['destination']); } - // We don't want the redirect from the link so we can redirect the - // delete action. - unset($form['terms'][$key]['operations']['#links']['edit']['query']['destination']); } } diff --git a/core/modules/forum/tests/src/Functional/ForumIndexTest.php b/core/modules/forum/tests/src/Functional/ForumIndexTest.php index 38adb72ea8..4b048e2647 100644 --- a/core/modules/forum/tests/src/Functional/ForumIndexTest.php +++ b/core/modules/forum/tests/src/Functional/ForumIndexTest.php @@ -57,6 +57,8 @@ public function testForumIndexStatus() { 'parent[0]' => $tid, ]; $this->drupalPostForm('admin/structure/forum/add/forum', $edit, t('Save')); + $this->assertSession()->linkExists(t('edit forum'), 0, 'edit forum link is not accessible to user with administer forums permission'); + $tid_child = $tid + 1; // Verify that the node appears on the index. diff --git a/core/modules/node/tests/src/Functional/NodeAccessBaseTableTest.php b/core/modules/node/tests/src/Functional/NodeAccessBaseTableTest.php index 5b99ccf68e..ea0dde664f 100644 --- a/core/modules/node/tests/src/Functional/NodeAccessBaseTableTest.php +++ b/core/modules/node/tests/src/Functional/NodeAccessBaseTableTest.php @@ -94,7 +94,7 @@ public function testNodeAccessBasic() { // Array of nids marked private. $private_nodes = []; for ($i = 0; $i < $num_simple_users; $i++) { - $simple_users[$i] = $this->drupalCreateUser(['access content', 'create article content']); + $simple_users[$i] = $this->drupalCreateUser(['access content', 'create article content', 'create terms in tags']); } foreach ($simple_users as $this->webUser) { $this->drupalLogin($this->webUser); diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php index 44a4e83033..b13250faaa 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php @@ -42,14 +42,17 @@ protected function setUpAuthorization($method) { $this->grantPermissionsToTestedRole(['access content']); break; case 'POST': + $this->grantPermissionsToTestedRole(['create terms in camelids']); + break; case 'PATCH': - case 'DELETE': // Grant the 'create url aliases' permission to test the case when // the path field is accessible, see // \Drupal\Tests\rest\Functional\EntityResource\Node\NodeResourceTestBase // for a negative test. - // @todo Update once https://www.drupal.org/node/2824408 lands. - $this->grantPermissionsToTestedRole(['administer taxonomy', 'create url aliases']); + $this->grantPermissionsToTestedRole(['edit terms in camelids', 'create url aliases']); + break; + case 'DELETE': + $this->grantPermissionsToTestedRole(['delete terms in camelids']); break; } } @@ -168,7 +171,7 @@ protected function getExpectedUnauthorizedAccessMessage($method) { case 'GET': return "The 'access content' permission is required."; case 'POST': - return "The 'administer taxonomy' permission is required."; + return "The following permissions are required: 'create terms in camelids' OR 'administer taxonomy'."; case 'PATCH': return "The following permissions are required: 'edit terms in camelids' OR 'administer taxonomy'."; case 'DELETE': diff --git a/core/modules/taxonomy/src/Entity/Term.php b/core/modules/taxonomy/src/Entity/Term.php index 03491ab962..c28a379d09 100644 --- a/core/modules/taxonomy/src/Entity/Term.php +++ b/core/modules/taxonomy/src/Entity/Term.php @@ -20,6 +20,7 @@ * "storage" = "Drupal\taxonomy\TermStorage", * "storage_schema" = "Drupal\taxonomy\TermStorageSchema", * "view_builder" = "Drupal\taxonomy\TermViewBuilder", + * "list_builder" = "Drupal\taxonomy\TermListBuilder", * "access" = "Drupal\taxonomy\TermAccessControlHandler", * "views_data" = "Drupal\taxonomy\TermViewsData", * "form" = { diff --git a/core/modules/taxonomy/src/Entity/Vocabulary.php b/core/modules/taxonomy/src/Entity/Vocabulary.php index b0d1ac13d2..d61294b4e3 100644 --- a/core/modules/taxonomy/src/Entity/Vocabulary.php +++ b/core/modules/taxonomy/src/Entity/Vocabulary.php @@ -15,6 +15,7 @@ * handlers = { * "storage" = "Drupal\taxonomy\VocabularyStorage", * "list_builder" = "Drupal\taxonomy\VocabularyListBuilder", + * "access" = "Drupal\taxonomy\VocabularyAccessControlHandler", * "form" = { * "default" = "Drupal\taxonomy\VocabularyForm", * "reset" = "Drupal\taxonomy\Form\VocabularyResetForm", diff --git a/core/modules/taxonomy/src/Form/OverviewTerms.php b/core/modules/taxonomy/src/Form/OverviewTerms.php index 3811ee5fd8..a986c683c8 100644 --- a/core/modules/taxonomy/src/Form/OverviewTerms.php +++ b/core/modules/taxonomy/src/Form/OverviewTerms.php @@ -6,6 +6,7 @@ use Drupal\Core\Form\FormBase; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Url; use Drupal\taxonomy\VocabularyInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -36,6 +37,13 @@ class OverviewTerms extends FormBase { protected $storageController; /** + * The term list builder. + * + * @var \Drupal\Core\Entity\EntityListBuilderInterface + */ + protected $termListBuilder; + + /** * Constructs an OverviewTerms object. * * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler @@ -47,6 +55,7 @@ public function __construct(ModuleHandlerInterface $module_handler, EntityManage $this->moduleHandler = $module_handler; $this->entityManager = $entity_manager; $this->storageController = $entity_manager->getStorage('taxonomy_term'); + $this->termListBuilder = $entity_manager->getListBuilder('taxonomy_term'); } /** @@ -204,17 +213,24 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular } $errors = $form_state->getErrors(); - $destination = $this->getDestinationArray(); $row_position = 0; // Build the actual form. + $access_control_handler = $this->entityManager->getAccessControlHandler('taxonomy_term'); + if ($access_control_handler->createAccess($taxonomy_vocabulary->id())) { + $empty = $this->t('No terms available. Add term.', ['@link' => Url::fromRoute('entity.taxonomy_term.add_form', ['taxonomy_vocabulary' => $taxonomy_vocabulary->id()])->toString()]); + } + else { + $empty = $this->t('No terms available.'); + } $form['terms'] = [ '#type' => 'table', - '#header' => [$this->t('Name'), $this->t('Weight'), $this->t('Operations')], - '#empty' => $this->t('No terms available. Add term.', [':link' => $this->url('entity.taxonomy_term.add_form', ['taxonomy_vocabulary' => $taxonomy_vocabulary->id()])]), + '#empty' => $empty, '#attributes' => [ 'id' => 'taxonomy', ], ]; + $change_weight_access = TRUE; + $operations_access = FALSE; foreach ($current_page as $key => $term) { /** @var $term \Drupal\Core\Entity\EntityInterface */ $term = $this->entityManager->getTranslationFromContext($term); @@ -260,39 +276,30 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular ], ]; } - $form['terms'][$key]['weight'] = [ - '#type' => 'weight', - '#delta' => $delta, - '#title' => $this->t('Weight for added term'), - '#title_display' => 'invisible', - '#default_value' => $term->getWeight(), - '#attributes' => [ - 'class' => ['term-weight'], - ], - ]; - $operations = [ - 'edit' => [ - 'title' => $this->t('Edit'), - 'query' => $destination, - 'url' => $term->urlInfo('edit-form'), - ], - 'delete' => [ - 'title' => $this->t('Delete'), - 'query' => $destination, - 'url' => $term->urlInfo('delete-form'), - ], - ]; - if ($this->moduleHandler->moduleExists('content_translation') && content_translation_translate_access($term)->isAllowed()) { - $operations['translate'] = [ - 'title' => $this->t('Translate'), - 'query' => $destination, - 'url' => $term->urlInfo('drupal:content-translation-overview'), + $edit_access = $term->access('update'); + $change_weight_access &= $edit_access; + + if ($edit_access) { + $form['terms'][$key]['weight'] = [ + '#type' => 'weight', + '#delta' => $delta, + '#title' => $this->t('Weight for added term'), + '#title_display' => 'invisible', + '#default_value' => $term->getWeight(), + '#attributes' => ['class' => ['term-weight']], + '#access' => $edit_access, + ]; + } + + if ($operations = $this->termListBuilder->getOperations($term)) { + // Allow access to operations if there is at least one term with + // operations. + $operations_access = TRUE; + $form['terms'][$key]['operations'] = [ + '#type' => 'operations', + '#links' => $operations, ]; } - $form['terms'][$key]['operations'] = [ - '#type' => 'operations', - '#links' => $operations, - ]; $form['terms'][$key]['#attributes']['class'] = []; if ($parent_fields) { @@ -322,44 +329,54 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular $row_position++; } - if ($parent_fields) { + $form['terms']['#header'] = [$this->t('Name')]; + if ($change_weight_access) { + $form['terms']['#header'][] = $this->t('Weight'); + if ($parent_fields) { + $form['terms']['#tabledrag'][] = [ + 'action' => 'match', + 'relationship' => 'parent', + 'group' => 'term-parent', + 'subgroup' => 'term-parent', + 'source' => 'term-id', + 'hidden' => FALSE, + ]; + $form['terms']['#tabledrag'][] = [ + 'action' => 'depth', + 'relationship' => 'group', + 'group' => 'term-depth', + 'hidden' => FALSE, + ]; + $form['terms']['#attached']['library'][] = 'taxonomy/drupal.taxonomy'; + $form['terms']['#attached']['drupalSettings']['taxonomy'] = [ + 'backStep' => $back_step, + 'forwardStep' => $forward_step, + ]; + } $form['terms']['#tabledrag'][] = [ - 'action' => 'match', - 'relationship' => 'parent', - 'group' => 'term-parent', - 'subgroup' => 'term-parent', - 'source' => 'term-id', - 'hidden' => FALSE, - ]; - $form['terms']['#tabledrag'][] = [ - 'action' => 'depth', - 'relationship' => 'group', - 'group' => 'term-depth', - 'hidden' => FALSE, - ]; - $form['terms']['#attached']['library'][] = 'taxonomy/drupal.taxonomy'; - $form['terms']['#attached']['drupalSettings']['taxonomy'] = [ - 'backStep' => $back_step, - 'forwardStep' => $forward_step, + 'action' => 'order', + 'relationship' => 'sibling', + 'group' => 'term-weight', ]; } - $form['terms']['#tabledrag'][] = [ - 'action' => 'order', - 'relationship' => 'sibling', - 'group' => 'term-weight', - ]; - if ($taxonomy_vocabulary->getHierarchy() != VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) { + if ($operations_access) { + $form['terms']['#header'][] = $this->t('Operations'); + } + + if (($taxonomy_vocabulary->getHierarchy() != VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) && $change_weight_access) { $form['actions'] = ['#type' => 'actions', '#tree' => FALSE]; $form['actions']['submit'] = [ '#type' => 'submit', '#value' => $this->t('Save'), '#button_type' => 'primary', + '#access' => $change_weight_access, ]; $form['actions']['reset_alphabetical'] = [ '#type' => 'submit', '#submit' => ['::submitReset'], '#value' => $this->t('Reset to alphabetical'), + '#access' => $change_weight_access, ]; } diff --git a/core/modules/taxonomy/src/TaxonomyPermissions.php b/core/modules/taxonomy/src/TaxonomyPermissions.php index 196c5a5258..c3c43bc504 100644 --- a/core/modules/taxonomy/src/TaxonomyPermissions.php +++ b/core/modules/taxonomy/src/TaxonomyPermissions.php @@ -5,6 +5,7 @@ use Drupal\Core\DependencyInjection\ContainerInjectionInterface; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\taxonomy\Entity\Vocabulary; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -48,19 +49,30 @@ public static function create(ContainerInterface $container) { */ public function permissions() { $permissions = []; - foreach ($this->entityManager->getStorage('taxonomy_vocabulary')->loadMultiple() as $vocabulary) { - $permissions += [ - 'edit terms in ' . $vocabulary->id() => [ - 'title' => $this->t('Edit terms in %vocabulary', ['%vocabulary' => $vocabulary->label()]), - ], - ]; - $permissions += [ - 'delete terms in ' . $vocabulary->id() => [ - 'title' => $this->t('Delete terms from %vocabulary', ['%vocabulary' => $vocabulary->label()]), - ], - ]; + foreach (Vocabulary::loadMultiple() as $vocabulary) { + $permissions += $this->buildPermissions($vocabulary); } return $permissions; } + /** + * Builds a standard list of taxonomy term permissions for a given vocabulary. + * + * @param \Drupal\taxonomy\Entity\Vocabulary $vocabulary + * The vocabulary. + * + * @return array + * An array of permission names and descriptions. + */ + protected function buildPermissions(Vocabulary $vocabulary) { + $id = $vocabulary->id(); + $args = ['%vocabulary' => $vocabulary->label()]; + + return [ + "create terms in $id" => ['title' => $this->t('%vocabulary: Create terms', $args)], + "delete terms in $id" => ['title' => $this->t('%vocabulary: Delete terms', $args)], + "edit terms in $id" => ['title' => $this->t('%vocabulary: Edit terms', $args)], + ]; + } + } diff --git a/core/modules/taxonomy/src/TermAccessControlHandler.php b/core/modules/taxonomy/src/TermAccessControlHandler.php index 04c2c4f3fb..1d48463666 100644 --- a/core/modules/taxonomy/src/TermAccessControlHandler.php +++ b/core/modules/taxonomy/src/TermAccessControlHandler.php @@ -38,7 +38,7 @@ protected function checkAccess(EntityInterface $entity, $operation, AccountInter * {@inheritdoc} */ protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { - return AccessResult::allowedIfHasPermission($account, 'administer taxonomy'); + return AccessResult::allowedIfHasPermissions($account, ["create terms in $entity_bundle", 'administer taxonomy'], 'OR'); } } diff --git a/core/modules/taxonomy/src/TermListBuilder.php b/core/modules/taxonomy/src/TermListBuilder.php new file mode 100644 index 0000000000..81d859120e --- /dev/null +++ b/core/modules/taxonomy/src/TermListBuilder.php @@ -0,0 +1,65 @@ +get('entity_type.manager')->getStorage($entity_type->id()), + $container->get('redirect.destination') + ); + } + + /** + * Constructs a new instance. + * + * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type + * The entity type definition. + * @param \Drupal\Core\Entity\EntityStorageInterface $storage + * The entity storage class. + * @param \Drupal\Core\Routing\RedirectDestinationInterface $redirect_destination + * The redirect destination. + */ + public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, RedirectDestinationInterface $redirect_destination) { + parent::__construct($entity_type, $storage); + + $this->redirectDestination = $redirect_destination; + } + + /** + * {@inheritdoc} + */ + public function getDefaultOperations(EntityInterface $entity) { + $operations = parent::getDefaultOperations($entity); + + $destination = $this->redirectDestination->get(); + foreach ($operations as $key => $operation) { + $operations[$key]['query']['destination'] = $destination; + } + + return $operations; + } + +} diff --git a/core/modules/taxonomy/src/VocabularyAccessControlHandler.php b/core/modules/taxonomy/src/VocabularyAccessControlHandler.php new file mode 100644 index 0000000000..befc8a5fdf --- /dev/null +++ b/core/modules/taxonomy/src/VocabularyAccessControlHandler.php @@ -0,0 +1,30 @@ +getStorage($entity_type->id())); + + $this->currentUser = $current_user; + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('current_user'), + $container->get('entity_type.manager') + ); + } + + /** * {@inheritdoc} */ public function getFormId() { @@ -36,16 +82,23 @@ public function getDefaultOperations(EntityInterface $entity) { $operations['edit']['title'] = t('Edit vocabulary'); } - $operations['list'] = [ - 'title' => t('List terms'), - 'weight' => 0, - 'url' => $entity->urlInfo('overview-form'), - ]; - $operations['add'] = [ - 'title' => t('Add terms'), - 'weight' => 10, - 'url' => Url::fromRoute('entity.taxonomy_term.add_form', ['taxonomy_vocabulary' => $entity->id()]), - ]; + if ($entity->access('access taxonomy overview')) { + $operations['list'] = [ + 'title' => t('List terms'), + 'weight' => 0, + 'url' => $entity->toUrl('overview-form'), + ]; + } + + $taxonomy_term_access_control_handler = $this->entityTypeManager->getAccessControlHandler('taxonomy_term'); + if ($taxonomy_term_access_control_handler->createAccess($entity->id())) { + $operations['add'] = [ + 'title' => t('Add terms'), + 'weight' => 10, + 'url' => Url::fromRoute('entity.taxonomy_term.add_form', ['taxonomy_vocabulary' => $entity->id()]), + ]; + } + unset($operations['delete']); return $operations; @@ -57,6 +110,11 @@ public function getDefaultOperations(EntityInterface $entity) { public function buildHeader() { $header['label'] = t('Vocabulary name'); $header['description'] = t('Description'); + + if ($this->currentUser->hasPermission('administer vocabularies')) { + $header['weight'] = t('Weight'); + } + return $header + parent::buildHeader(); } @@ -80,7 +138,13 @@ public function render() { unset($this->weightKey); } $build = parent::render(); - $build['table']['#empty'] = t('No vocabularies available. Add vocabulary.', [':link' => \Drupal::url('entity.taxonomy_vocabulary.add_form')]); + $create_access = $this->entityTypeManager->getAccessControlHandler('taxonomy_vocabulary')->createAccess(); + if ($create_access) { + $build['table']['#empty'] = t('No vocabularies available. Add vocabulary.', ['@link' => Url::fromRoute('entity.taxonomy_vocabulary.add_form')->toString()]); + } + else { + $build['table']['#empty'] = t('No vocabularies available.'); + } return $build; } diff --git a/core/modules/taxonomy/taxonomy.module b/core/modules/taxonomy/taxonomy.module index 4a93989ad5..eb887274a7 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -75,6 +75,17 @@ function taxonomy_help($route_name, RouteMatchInterface $route_match) { case 'entity.taxonomy_vocabulary.overview_form': $vocabulary = $route_match->getParameter('taxonomy_vocabulary'); + if (!\Drupal::currentUser()->hasPermission('administer taxonomy')) { + // There is no drag-and-drop handles. + switch ($vocabulary->getHierarchy()) { + case TAXONOMY_HIERARCHY_DISABLED: + return '

' . t('%capital_name contains the following terms.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '

'; + case TAXONOMY_HIERARCHY_SINGLE: + return '

' . t('%capital_name contains terms grouped under parent terms', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '

'; + case TAXONOMY_HIERARCHY_MULTIPLE: + return '

' . t('%capital_name contains terms with multiple parents.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '

'; + } + } switch ($vocabulary->getHierarchy()) { case VocabularyInterface::HIERARCHY_DISABLED: return '

' . t('You can reorganize the terms in %capital_name using their drag-and-drop handles, and group terms under a parent term by sliding them under and to the right of the parent.', ['%capital_name' => Unicode::ucfirst($vocabulary->label()), '%name' => $vocabulary->label()]) . '

'; diff --git a/core/modules/taxonomy/taxonomy.permissions.yml b/core/modules/taxonomy/taxonomy.permissions.yml index d4859492af..bb71e93c12 100644 --- a/core/modules/taxonomy/taxonomy.permissions.yml +++ b/core/modules/taxonomy/taxonomy.permissions.yml @@ -1,5 +1,9 @@ administer taxonomy: title: 'Administer vocabularies and terms' +access taxonomy overview: + title: 'Access the taxonomy vocabulary overview page' + description: 'Get an overview of all taxonomy vocabularies.' + permission_callbacks: - Drupal\taxonomy\TaxonomyPermissions::permissions diff --git a/core/modules/taxonomy/taxonomy.routing.yml b/core/modules/taxonomy/taxonomy.routing.yml index 8a3bd1a58d..19989241e4 100644 --- a/core/modules/taxonomy/taxonomy.routing.yml +++ b/core/modules/taxonomy/taxonomy.routing.yml @@ -4,7 +4,7 @@ entity.taxonomy_vocabulary.collection: _entity_list: 'taxonomy_vocabulary' _title: 'Taxonomy' requirements: - _permission: 'administer taxonomy' + _permission: 'access taxonomy overview+administer taxonomy' entity.taxonomy_term.add_form: path: '/admin/structure/taxonomy/manage/{taxonomy_vocabulary}/add' @@ -74,7 +74,7 @@ entity.taxonomy_vocabulary.overview_form: _form: 'Drupal\taxonomy\Form\OverviewTerms' _title_callback: 'Drupal\taxonomy\Controller\TaxonomyController::vocabularyTitle' requirements: - _entity_access: 'taxonomy_vocabulary.view' + _entity_access: 'taxonomy_vocabulary.access taxonomy overview' entity.taxonomy_term.canonical: path: '/taxonomy/term/{taxonomy_term}' diff --git a/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php b/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php index 3ba8868f54..1cb7dc9a2f 100644 --- a/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php +++ b/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php @@ -16,6 +16,34 @@ protected function setUp() { } /** + * Create, edit and delete a vocabulary via the user interface. + */ + public function testVocabularyPermissionsVocabulary() { + // VocabularyTest.php already tests for user with "administer taxonomy" + // permission. + + // Test as user without proper permissions. + $authenticated_user = $this->drupalCreateUser([]); + $this->drupalLogin($authenticated_user); + + // Visit the main taxonomy administration page. + $this->drupalGet('admin/structure/taxonomy'); + $this->assertResponse(403, 'Vocabulary overview open failed.'); + + // Test as user with "access taxonomy overview" permissions. + $proper_user = $this->drupalCreateUser(['access taxonomy overview']); + $this->drupalLogin($proper_user); + + // Visit the main taxonomy administration page. + $this->drupalGet('admin/structure/taxonomy'); + $this->assertResponse(200); + $this->assertText(t('Vocabulary name'), 'Vocabulary overview opened successfully.'); + + // Create a new vocabulary. + $this->assertNoLink(t('Add vocabulary')); + } + + /** * Create, edit and delete a taxonomy term via the user interface. */ public function testVocabularyPermissionsTaxonomyTerm() { @@ -62,8 +90,35 @@ public function testVocabularyPermissionsTaxonomyTerm() { $this->drupalPostForm(NULL, NULL, t('Delete')); $this->assertRaw(t('Deleted term %name.', ['%name' => $edit['name[0][value]']]), 'Term deleted.'); + // Test as user with "create" permissions. + $user = $this->drupalCreateUser(["access taxonomy overview", "create terms in {$vocabulary->id()}"]); + $this->drupalLogin($user); + + // Visit the main taxonomy administration page. + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary->id() . '/add'); + $this->assertResponse(200); + $this->assertFieldByName('name[0][value]', NULL, 'Add taxonomy term form opened successfully.'); + + // Submit the term. + $edit = []; + $edit['name[0][value]'] = $this->randomMachineName(); + + $this->drupalPostForm(NULL, $edit, t('Save')); + $this->assertText(t('Created new term @name.', ['@name' => $edit['name[0][value]']]), 'Term created successfully.'); + + $terms = taxonomy_term_load_multiple_by_name($edit['name[0][value]']); + $term = reset($terms); + + // Edit the term. + $this->drupalGet('taxonomy/term/' . $term->id() . '/edit'); + $this->assertResponse(403, 'Edit taxonomy term form open failed.'); + + // Delete the vocabulary. + $this->drupalGet('taxonomy/term/' . $term->id() . '/delete'); + $this->assertResponse(403, 'Delete taxonomy term form open failed.'); + // Test as user with "edit" permissions. - $user = $this->drupalCreateUser(["edit terms in {$vocabulary->id()}"]); + $user = $this->drupalCreateUser(["access taxonomy overview", "edit terms in {$vocabulary->id()}"]); $this->drupalLogin($user); // Visit the main taxonomy administration page. @@ -91,7 +146,7 @@ public function testVocabularyPermissionsTaxonomyTerm() { $this->assertResponse(403, 'Delete taxonomy term form open failed.'); // Test as user with "delete" permissions. - $user = $this->drupalCreateUser(["delete terms in {$vocabulary->id()}"]); + $user = $this->drupalCreateUser(["access taxonomy overview", "delete terms in {$vocabulary->id()}"]); $this->drupalLogin($user); // Visit the main taxonomy administration page.