summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorNathaniel Catchpole2017-10-03 10:22:50 (GMT)
committerNathaniel Catchpole2017-10-03 10:22:50 (GMT)
commit3529e208aa33d539c98efab9faf112f8f3b4b7c0 (patch)
treedd0f9224e7e806146e853075e837b28e22077ed6
parent5f9fe909884efcd38b2e0b199b1d9dfe0f19cfa6 (diff)
Issue #1848686 by Johnny vd Laar, Berdir, JeroenT, geertvd, mogtofu33, hauruck, Manuel Garcia, pfrenssen, Bambell, realityloop, ParisLiakos, webflo, cyborg_572, Algeron, zniki.ru, xjm, Wim Leers, dawehner, yoroy: Add a dedicated permission to access the term overview page (without 'administer taxonomy' permission)
-rw-r--r--core/modules/forum/src/Form/Overview.php27
-rw-r--r--core/modules/forum/tests/src/Functional/ForumIndexTest.php2
-rw-r--r--core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php15
-rw-r--r--core/modules/taxonomy/src/Entity/Term.php1
-rw-r--r--core/modules/taxonomy/src/Entity/Vocabulary.php1
-rw-r--r--core/modules/taxonomy/src/Form/OverviewTerms.php148
-rw-r--r--core/modules/taxonomy/src/TaxonomyPermissions.php34
-rw-r--r--core/modules/taxonomy/src/TermAccessControlHandler.php2
-rw-r--r--core/modules/taxonomy/src/VocabularyAccessControlHandler.php30
-rw-r--r--core/modules/taxonomy/src/VocabularyListBuilder.php110
-rw-r--r--core/modules/taxonomy/taxonomy.module26
-rw-r--r--core/modules/taxonomy/taxonomy.permissions.yml4
-rw-r--r--core/modules/taxonomy/taxonomy.routing.yml4
-rw-r--r--core/modules/taxonomy/tests/src/Functional/VocabularyPermissionsTest.php229
14 files changed, 527 insertions, 106 deletions
diff --git a/core/modules/forum/src/Form/Overview.php b/core/modules/forum/src/Form/Overview.php
index df6f947..801888a 100644
--- a/core/modules/forum/src/Form/Overview.php
+++ b/core/modules/forum/src/Form/Overview.php
@@ -58,21 +58,30 @@ class Overview extends OverviewTerms {
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 38adb72..e3d904d 100644
--- a/core/modules/forum/tests/src/Functional/ForumIndexTest.php
+++ b/core/modules/forum/tests/src/Functional/ForumIndexTest.php
@@ -57,6 +57,8 @@ class ForumIndexTest extends BrowserTestBase {
'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 44a4e83..9759977 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 @@ abstract class TermResourceTestBase extends EntityResourceTestBase {
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 @@ abstract class TermResourceTestBase extends EntityResourceTestBase {
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 03491ab..2e70a1c 100644
--- a/core/modules/taxonomy/src/Entity/Term.php
+++ b/core/modules/taxonomy/src/Entity/Term.php
@@ -20,6 +20,7 @@ use Drupal\taxonomy\TermInterface;
* "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 b0d1ac1..d61294b 100644
--- a/core/modules/taxonomy/src/Entity/Vocabulary.php
+++ b/core/modules/taxonomy/src/Entity/Vocabulary.php
@@ -15,6 +15,7 @@ use Drupal\taxonomy\VocabularyInterface;
* 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 3811ee5..d73598d 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 @@ class OverviewTerms extends FormBase {
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 @@ class OverviewTerms extends FormBase {
}
$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. <a href=":link">Add term</a>.', [':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. <a href=":link">Add term</a>.', [':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 @@ class OverviewTerms extends FormBase {
],
];
}
- $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 @@ class OverviewTerms extends FormBase {
$row_position++;
}
- if ($parent_fields) {
- $form['terms']['#tabledrag'][] = [
- 'action' => 'match',
- 'relationship' => 'parent',
- 'group' => 'term-parent',
- 'subgroup' => 'term-parent',
- 'source' => 'term-id',
- 'hidden' => FALSE,
- ];
+ $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' => '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 196c5a5..1772a34 100644
--- a/core/modules/taxonomy/src/TaxonomyPermissions.php
+++ b/core/modules/taxonomy/src/TaxonomyPermissions.php
@@ -5,6 +5,7 @@ namespace Drupal\taxonomy;
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 @@ class TaxonomyPermissions implements ContainerInjectionInterface {
*/
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 04c2c4f..1d48463 100644
--- a/core/modules/taxonomy/src/TermAccessControlHandler.php
+++ b/core/modules/taxonomy/src/TermAccessControlHandler.php
@@ -38,7 +38,7 @@ class TermAccessControlHandler extends EntityAccessControlHandler {
* {@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 0000000..befc8a5
--- /dev/null
+++ b/core/modules/taxonomy/src/VocabularyAccessControlHandler.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\taxonomy;
+
+use Drupal\Core\Access\AccessResult;
+use Drupal\Core\Entity\EntityAccessControlHandler;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+
+/**
+ * Defines the access control handler for the taxonomy vocabulary entity type.
+ *
+ * @see \Drupal\taxonomy\Entity\Vocabulary
+ */
+class VocabularyAccessControlHandler extends EntityAccessControlHandler {
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
+ switch ($operation) {
+ case 'access taxonomy overview':
+ return AccessResult::allowedIfHasPermissions($account, ['access taxonomy overview', 'administer taxonomy'], 'OR');
+
+ default:
+ return parent::checkAccess($entity, $operation, $account);
+ }
+ }
+
+}
diff --git a/core/modules/taxonomy/src/VocabularyListBuilder.php b/core/modules/taxonomy/src/VocabularyListBuilder.php
index 8503c28..e7c021b 100644
--- a/core/modules/taxonomy/src/VocabularyListBuilder.php
+++ b/core/modules/taxonomy/src/VocabularyListBuilder.php
@@ -4,8 +4,13 @@ namespace Drupal\taxonomy;
use Drupal\Core\Config\Entity\DraggableListBuilder;
use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\EntityTypeInterface;
+use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Form\FormStateInterface;
+use Drupal\Core\Render\RendererInterface;
+use Drupal\Core\Session\AccountInterface;
use Drupal\Core\Url;
+use Symfony\Component\DependencyInjection\ContainerInterface;
/**
* Defines a class to build a listing of taxonomy vocabulary entities.
@@ -20,6 +25,59 @@ class VocabularyListBuilder extends DraggableListBuilder {
protected $entitiesKey = 'vocabularies';
/**
+ * The current user.
+ *
+ * @var \Drupal\Core\Session\AccountInterface
+ */
+ protected $currentUser;
+
+ /**
+ * The entity manager.
+ *
+ * @var \Drupal\Core\Entity\EntityTypeManagerInterface
+ */
+ protected $entityTypeManager;
+
+ /**
+ * The renderer service.
+ *
+ * @var \Drupal\Core\Render\RendererInterface
+ */
+ protected $renderer;
+
+ /**
+ * Constructs a new VocabularyListBuilder object.
+ *
+ * @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
+ * The entity type definition.
+ * @param \Drupal\Core\Session\AccountInterface $current_user
+ * The current user.
+ * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
+ * The entity manager service.
+ * @param \Drupal\Core\Render\RendererInterface $renderer
+ * The renderer service.
+ */
+ public function __construct(EntityTypeInterface $entity_type, AccountInterface $current_user, EntityTypeManagerInterface $entity_type_manager, RendererInterface $renderer = NULL) {
+ parent::__construct($entity_type, $entity_type_manager->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 @@ class VocabularyListBuilder extends DraggableListBuilder {
$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 @@ class VocabularyListBuilder extends DraggableListBuilder {
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 @@ class VocabularyListBuilder extends DraggableListBuilder {
unset($this->weightKey);
}
$build = parent::render();
- $build['table']['#empty'] = t('No vocabularies available. <a href=":link">Add vocabulary</a>.', [':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. <a href=":link">Add vocabulary</a>.', [
+ ':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 11c959f..c6dd721 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 '<p>' . 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()]) . '</p>';
- case VocabularyInterface::HIERARCHY_SINGLE:
- return '<p>' . 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()]) . '</p>';
- case VocabularyInterface::HIERARCHY_MULTIPLE:
- return '<p>' . 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())]) . '</p>';
+ if (\Drupal::currentUser()->hasPermission('administer taxonomy') || \Drupal::currentUser()->hasPermission('edit terms in ' . $vocabulary->id())) {
+ switch ($vocabulary->getHierarchy()) {
+ case VocabularyInterface::HIERARCHY_DISABLED:
+ return '<p>' . 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()]) . '</p>';
+ case VocabularyInterface::HIERARCHY_SINGLE:
+ return '<p>' . 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()]) . '</p>';
+ case VocabularyInterface::HIERARCHY_MULTIPLE:
+ return '<p>' . 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())]) . '</p>';
+ }
+ }
+ else {
+ switch ($vocabulary->getHierarchy()) {
+ case VocabularyInterface::HIERARCHY_DISABLED:
+ return '<p>' . t('%capital_name contains the following terms.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '</p>';
+ case VocabularyInterface::HIERARCHY_SINGLE:
+ return '<p>' . t('%capital_name contains terms grouped under parent terms', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '</p>';
+ case VocabularyInterface::HIERARCHY_MULTIPLE:
+ return '<p>' . t('%capital_name contains terms with multiple parents.', ['%capital_name' => Unicode::ucfirst($vocabulary->label())]) . '</p>';
+ }
}
}
}
diff --git a/core/modules/taxonomy/taxonomy.permissions.yml b/core/modules/taxonomy/taxonomy.permissions.yml
index d485949..bb71e93 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 8a3bd1a..1998924 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 3ba8868..989398e6 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 @@ namespace Drupal\Tests\taxonomy\Functional;
*/
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 @@ class VocabularyPermissionsTest extends TaxonomyTestBase {
$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 @@ class VocabularyPermissionsTest extends TaxonomyTestBase {
$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);