diff --git a/core/modules/forum/src/Form/Overview.php b/core/modules/forum/src/Form/Overview.php index df6f94725a..801888aabf 100644 --- a/core/modules/forum/src/Form/Overview.php +++ b/core/modules/forum/src/Form/Overview.php @@ -58,21 +58,30 @@ 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); + $title = $this->t('edit container'); + $url = Url::fromRoute('entity.taxonomy_term.forum_edit_container_form', ['taxonomy_term' => $term->id()]); } 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); + $title = $this->t('edit forum'); + $url = Url::fromRoute('entity.taxonomy_term.forum_edit_form', ['taxonomy_term' => $term->id()]); } - // 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']); + + // Re-create the operations column and add only the edit link. + $form['terms'][$key]['operations'] = [ + '#type' => 'operations', + '#links' => [ + 'edit' => [ + 'title' => $title, + 'url' => $url, + ], + ], + ]; + } } diff --git a/core/modules/forum/tests/src/Functional/ForumIndexTest.php b/core/modules/forum/tests/src/Functional/ForumIndexTest.php index 38adb72ea8..e3d904d368 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')); + $tid_child = $tid + 1; // Verify that the node appears on the index. 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..97599774b3 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php @@ -41,16 +41,23 @@ protected function setUpAuthorization($method) { case 'GET': $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 +175,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..2e70a1cf7d 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\Core\Entity\EntityListBuilder", * "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..d73598d672 100644 --- a/core/modules/taxonomy/src/Form/OverviewTerms.php +++ b/core/modules/taxonomy/src/Form/OverviewTerms.php @@ -2,10 +2,13 @@ namespace Drupal\taxonomy\Form; +use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\EntityManagerInterface; use Drupal\Core\Form\FormBase; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Render\RendererInterface; +use Drupal\Core\Url; use Drupal\taxonomy\VocabularyInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -36,17 +39,35 @@ class OverviewTerms extends FormBase { protected $storageController; /** + * The term list builder. + * + * @var \Drupal\Core\Entity\EntityListBuilderInterface + */ + protected $termListBuilder; + + /** + * The renderer service. + * + * @var \Drupal\Core\Render\RendererInterface + */ + protected $renderer; + + /** * Constructs an OverviewTerms object. * * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler * The module handler service. * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager * The entity manager service. + * @param \Drupal\Core\Render\RendererInterface $renderer + * The renderer service. */ - public function __construct(ModuleHandlerInterface $module_handler, EntityManagerInterface $entity_manager) { + public function __construct(ModuleHandlerInterface $module_handler, EntityManagerInterface $entity_manager, RendererInterface $renderer = NULL) { $this->moduleHandler = $module_handler; $this->entityManager = $entity_manager; $this->storageController = $entity_manager->getStorage('taxonomy_term'); + $this->termListBuilder = $entity_manager->getListBuilder('taxonomy_term'); + $this->renderer = $renderer ?: \Drupal::service('renderer'); } /** @@ -55,7 +76,8 @@ public function __construct(ModuleHandlerInterface $module_handler, EntityManage public static function create(ContainerInterface $container) { return new static( $container->get('module_handler'), - $container->get('entity.manager') + $container->get('entity.manager'), + $container->get('renderer') ); } @@ -204,17 +226,28 @@ 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'); + $create_access = $access_control_handler->createAccess($taxonomy_vocabulary->id(), NULL, [], TRUE); + if ($create_access->isAllowed()) { + $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', ], ]; + $this->renderer->addCacheableDependency($form['terms'], $create_access); + + // Only allow access to changing weights if the user has update access for + // all terms. + $change_weight_access = AccessResult::allowed(); foreach ($current_page as $key => $term) { /** @var $term \Drupal\Core\Entity\EntityInterface */ $term = $this->entityManager->getTranslationFromContext($term); @@ -260,39 +293,26 @@ 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'), + $update_access = $term->access('update', NULL, TRUE); + $change_weight_access = $change_weight_access->andIf($update_access); + + if ($update_access->isAllowed()) { + $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']], + ]; + } + + if ($operations = $this->termListBuilder->getOperations($term)) { + $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,34 +342,42 @@ public function buildForm(array $form, FormStateInterface $form_state, Vocabular $row_position++; } - if ($parent_fields) { + $form['terms']['#header'] = [$this->t('Name')]; + + $this->renderer->addCacheableDependency($form['terms'], $change_weight_access); + if ($change_weight_access->isAllowed()) { + $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) { + $form['terms']['#header'][] = $this->t('Operations'); + + if (($taxonomy_vocabulary->getHierarchy() !== VocabularyInterface::HIERARCHY_MULTIPLE && count($tree) > 1) && $change_weight_access->isAllowed()) { $form['actions'] = ['#type' => 'actions', '#tree' => FALSE]; $form['actions']['submit'] = [ '#type' => 'submit', diff --git a/core/modules/taxonomy/src/TaxonomyPermissions.php b/core/modules/taxonomy/src/TaxonomyPermissions.php index 196c5a5258..1772a34f97 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\VocabularyInterface $vocabulary + * The vocabulary. + * + * @return array + * An array of permission names and descriptions. + */ + protected function buildPermissions(VocabularyInterface $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/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; + $this->renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('current_user'), + $container->get('entity_type.manager'), + $container->get('renderer') + ); + } + + /** * {@inheritdoc} */ public function getFormId() { @@ -36,16 +94,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 +122,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 +150,25 @@ 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')]); + + // If the weight key was unset then the table is in the 'table' key, + // otherwise in vocabularies. The empty message is only needed if the table + // is possibly empty, so there is no need to support the vocabularies key + // here. + if (isset($build['table'])) { + $access_control_handler = $this->entityTypeManager->getAccessControlHandler('taxonomy_vocabulary'); + $create_access = $access_control_handler->createAccess(NULL, NULL, [], TRUE); + $this->renderer->addCacheableDependency($build['table'], $create_access); + if ($create_access->isAllowed()) { + $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..c165855c28 100644 --- a/core/modules/taxonomy/taxonomy.module +++ b/core/modules/taxonomy/taxonomy.module @@ -75,13 +75,25 @@ function taxonomy_help($route_name, RouteMatchInterface $route_match) { case 'entity.taxonomy_vocabulary.overview_form': $vocabulary = $route_match->getParameter('taxonomy_vocabulary'); - 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()]) . '

'; - case VocabularyInterface::HIERARCHY_SINGLE: - return '

' . t('%capital_name contains terms grouped under parent terms. You can reorganize the terms in %capital_name using their drag-and-drop handles.', ['%capital_name' => Unicode::ucfirst($vocabulary->label()), '%name' => $vocabulary->label()]) . '

'; - case VocabularyInterface::HIERARCHY_MULTIPLE: - return '

' . t('%capital_name contains terms with multiple parents. Drag and drop of terms with multiple parents is not supported, but you can re-enable drag-and-drop support by editing each term to include only a single parent.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '

'; + if (\Drupal::currentUser()->hasPermission('administer taxonomy') || \Drupal::currentUser()->hasPermission('edit terms in ' . $vocabulary->id())) { + 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()]) . '

'; + case VocabularyInterface::HIERARCHY_SINGLE: + return '

' . t('%capital_name contains terms grouped under parent terms. You can reorganize the terms in %capital_name using their drag-and-drop handles.', ['%capital_name' => Unicode::ucfirst($vocabulary->label()), '%name' => $vocabulary->label()]) . '

'; + case VocabularyInterface::HIERARCHY_MULTIPLE: + return '

' . t('%capital_name contains terms with multiple parents. Drag and drop of terms with multiple parents is not supported, but you can re-enable drag-and-drop support by editing each term to include only a single parent.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '

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

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

'; + case VocabularyInterface::HIERARCHY_SINGLE: + return '

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

'; + case VocabularyInterface::HIERARCHY_MULTIPLE: + return '

' . t('%capital_name contains terms with multiple parents.', ['%capital_name' => Unicode::ucfirst($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..989398e62f 100644 --- a/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php +++ b/core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php @@ -2,6 +2,8 @@ namespace Drupal\Tests\taxonomy\Functional; +use Drupal\Component\Utility\Unicode; + /** * Tests the taxonomy vocabulary permissions. * @@ -9,10 +11,204 @@ */ class VocabularyPermissionsTest extends TaxonomyTestBase { + /** + * Modules to enable. + * + * @var array + */ + public static $modules = ['help']; + protected function setUp() { parent::setUp(); $this->drupalPlaceBlock('page_title_block'); + $this->drupalPlaceBlock('local_actions_block'); + $this->drupalPlaceBlock('help_block'); + } + + /** + * 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); + + $assert_session = $this->assertSession(); + + // Visit the main taxonomy administration page. + $this->drupalGet('admin/structure/taxonomy'); + $assert_session->statusCodeEquals(403); + + // 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'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('Vocabulary name'); + $assert_session->linkNotExists('Add vocabulary'); + } + + /** + * Test the vocabulary overview permission. + */ + public function testTaxonomyVocabularyOverviewPermissions() { + // Create two vocabularies, one with two terms, the other without any term. + /** @var \Drupal\taxonomy\Entity\Vocabulary $vocabulary1 , $vocabulary2 */ + $vocabulary1 = $this->createVocabulary(); + $vocabulary2 = $this->createVocabulary(); + $vocabulary1_id = $vocabulary1->id(); + $vocabulary2_id = $vocabulary2->id(); + $this->createTerm($vocabulary1); + $this->createTerm($vocabulary1); + + // Assert expected help texts on first vocabulary. + $edit_help_text = 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($vocabulary1->label())]); + $no_edit_help_text = t('@capital_name contains the following terms.', ['@capital_name' => Unicode::ucfirst($vocabulary1->label())]); + + $assert_session = $this->assertSession(); + + // Logged in as admin user with 'administer taxonomy' permission. + $admin_user = $this->drupalCreateUser(['administer taxonomy']); + $this->drupalLogin($admin_user); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary1_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->linkExists('Edit'); + $assert_session->linkExists('Delete'); + $assert_session->linkExists('Add term'); + $assert_session->buttonExists('Save'); + $assert_session->pageTextContains('Weight'); + $assert_session->pageTextContains($edit_help_text); + + // Visit vocabulary overview without terms. 'Add term' should be shown. + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary2_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('No terms available'); + $assert_session->linkExists('Add term'); + + // Login as a user without any of the required permissions. + $no_permission_user = $this->drupalCreateUser(); + $this->drupalLogin($no_permission_user); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary1_id . '/overview'); + $assert_session->statusCodeEquals(403); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary2_id . '/overview'); + $assert_session->statusCodeEquals(403); + + // Log in as a user with only the overview permission, neither edit nor + // delete operations must be available and no Save button. + $overview_only_user = $this->drupalCreateUser(['access taxonomy overview']); + $this->drupalLogin($overview_only_user); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary1_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->linkNotExists('Edit'); + $assert_session->linkNotExists('Delete'); + $assert_session->buttonNotExists('Save'); + $assert_session->pageTextNotContains('Weight'); + $assert_session->linkNotExists('Add term'); + $assert_session->pageTextContains($no_edit_help_text); + + // Visit vocabulary overview without terms. 'Add term' should not be shown. + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary2_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('No terms available'); + $assert_session->linkNotExists('Add term'); + + // Login as a user with permission to edit terms, only edit link should be + // visible. + $edit_user = $this->createUser([ + 'access taxonomy overview', + 'edit terms in ' . $vocabulary1_id, + 'edit terms in ' . $vocabulary2_id, + ]); + $this->drupalLogin($edit_user); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary1_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->linkExists('Edit'); + $assert_session->linkNotExists('Delete'); + $assert_session->buttonExists('Save'); + $assert_session->pageTextContains('Weight'); + $assert_session->linkNotExists('Add term'); + $assert_session->pageTextContains($edit_help_text); + + // Visit vocabulary overview without terms. 'Add term' should not be shown. + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary2_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('No terms available'); + $assert_session->linkNotExists('Add term'); + + // Login as a user with permission only to delete terms. + $edit_delete_user = $this->createUser([ + 'access taxonomy overview', + 'delete terms in ' . $vocabulary1_id, + 'delete terms in ' . $vocabulary2_id, + ]); + $this->drupalLogin($edit_delete_user); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary1_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->linkNotExists('Edit'); + $assert_session->linkExists('Delete'); + $assert_session->linkNotExists('Add term'); + $assert_session->buttonNotExists('Save'); + $assert_session->pageTextNotContains('Weight'); + $assert_session->pageTextContains($no_edit_help_text); + + // Visit vocabulary overview without terms. 'Add term' should not be shown. + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary2_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('No terms available'); + $assert_session->linkNotExists('Add term'); + + // Login as a user with permission to edit and delete terms. + $edit_delete_user = $this->createUser([ + 'access taxonomy overview', + 'edit terms in ' . $vocabulary1_id, + 'delete terms in ' . $vocabulary1_id, + 'edit terms in ' . $vocabulary2_id, + 'delete terms in ' . $vocabulary2_id, + ]); + $this->drupalLogin($edit_delete_user); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary1_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->linkExists('Edit'); + $assert_session->linkExists('Delete'); + $assert_session->linkNotExists('Add term'); + $assert_session->buttonExists('Save'); + $assert_session->pageTextContains('Weight'); + $assert_session->pageTextContains($edit_help_text); + + // Visit vocabulary overview without terms. 'Add term' should not be shown. + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary2_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('No terms available'); + $assert_session->linkNotExists('Add term'); + + // Login as a user with permission to create new terms, only add new term + // link should be visible. + $edit_user = $this->createUser([ + 'access taxonomy overview', + 'create terms in ' . $vocabulary1_id, + 'create terms in ' . $vocabulary2_id, + ]); + $this->drupalLogin($edit_user); + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary1_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->linkNotExists('Edit'); + $assert_session->linkNotExists('Delete'); + $assert_session->linkExists('Add term'); + $assert_session->buttonNotExists('Save'); + $assert_session->pageTextNotContains('Weight'); + $assert_session->pageTextContains($no_edit_help_text); + + // Visit vocabulary overview without terms. 'Add term' should not be shown. + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary2_id . '/overview'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('No terms available'); + $assert_session->linkExists('Add term'); } /** @@ -42,7 +238,9 @@ public function testVocabularyPermissionsTaxonomyTerm() { $view_link = $this->xpath('//div[@class="messages"]//a[contains(@href, :href)]', [':href' => 'term/']); $this->assert(isset($view_link), 'The message area contains a link to a term'); - $terms = taxonomy_term_load_multiple_by_name($edit['name[0][value]']); + $terms = \Drupal::entityTypeManager() + ->getStorage('taxonomy_term') + ->loadByProperties(['name' => $edit['name[0][value]']]); $term = reset($terms); // Edit the term. @@ -62,6 +260,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(["create terms in {$vocabulary->id()}"]); + $this->drupalLogin($user); + + $assert_session = $this->assertSession(); + + // Create a new term. + $this->drupalGet('admin/structure/taxonomy/manage/' . $vocabulary->id() . '/add'); + $assert_session->statusCodeEquals(200); + $assert_session->fieldExists('name[0][value]'); + + // Submit the term. + $edit = []; + $edit['name[0][value]'] = $this->randomMachineName(); + + $this->drupalPostForm(NULL, $edit, t('Save')); + $assert_session->pageTextContains(t('Created new term @name.', ['@name' => $edit['name[0][value]']])); + + $terms = \Drupal::entityTypeManager() + ->getStorage('taxonomy_term') + ->loadByProperties(['name' => $edit['name[0][value]']]); + $term = reset($terms); + + // Ensure that edit and delete access is denied. + $this->drupalGet('taxonomy/term/' . $term->id() . '/edit'); + $assert_session->statusCodeEquals(403); + $this->drupalGet('taxonomy/term/' . $term->id() . '/delete'); + $assert_session->statusCodeEquals(403); + // Test as user with "edit" permissions. $user = $this->drupalCreateUser(["edit terms in {$vocabulary->id()}"]); $this->drupalLogin($user);