diff --git a/core/modules/node/node.module b/core/modules/node/node.module
index da392615a65a0e597edc7a6adaa4035e5ecb97b2..99f3aee9f568f030d35aae82b0dbf5817ec45cc0 100644
--- a/core/modules/node/node.module
+++ b/core/modules/node/node.module
@@ -671,56 +671,6 @@ function template_preprocess_node(&$variables) {
}
}
-/**
- * Implements hook_permission().
- */
-function node_permission() {
- $perms = array(
- 'bypass node access' => array(
- 'title' => t('Bypass content access control'),
- 'description' => t('View, edit and delete all content regardless of permission restrictions.'),
- 'restrict access' => TRUE,
- ),
- 'administer content types' => array(
- 'title' => t('Administer content types'),
- 'description' => t('Promote, change ownership, edit revisions, and perform other tasks across all content types.'),
- 'restrict access' => TRUE,
- ),
- 'administer nodes' => array(
- 'title' => t('Administer content'),
- 'restrict access' => TRUE,
- ),
- 'access content overview' => array(
- 'title' => t('Access the Content overview page'),
- 'description' => t('Get an overview of all content.', array('!url' => \Drupal::url('system.admin_content'))),
- ),
- 'access content' => array(
- 'title' => t('View published content'),
- ),
- 'view own unpublished content' => array(
- 'title' => t('View own unpublished content'),
- ),
- 'view all revisions' => array(
- 'title' => t('View all revisions'),
- ),
- 'revert all revisions' => array(
- 'title' => t('Revert all revisions'),
- 'description' => t('Role requires permission view revisions and edit rights for nodes in question, or administer nodes.'),
- ),
- 'delete all revisions' => array(
- 'title' => t('Delete all revisions'),
- 'description' => t('Role requires permission to view revisions and delete rights for nodes in question, or administer nodes.'),
- ),
- );
-
- // Generate node permissions for all node types.
- foreach (NodeType::loadMultiple() as $type) {
- $perms += node_list_permissions($type);
- }
-
- return $perms;
-}
-
/**
* Implements hook_ranking().
*/
@@ -1193,48 +1143,6 @@ function node_node_access(NodeInterface $node, $op, $account) {
return NODE_ACCESS_IGNORE;
}
-/**
- * Helper function to generate standard node permission list for a given type.
- *
- * @param $name
- * The machine name of the node type.
- *
- * @return array
- * An array of permission names and descriptions.
- */
-function node_list_permissions($type) {
- // Build standard list of node permissions for this type.
- $perms = array(
- "create $type->type content" => array(
- 'title' => t('%type_name: Create new content', array('%type_name' => $type->name)),
- ),
- "edit own $type->type content" => array(
- 'title' => t('%type_name: Edit own content', array('%type_name' => $type->name)),
- ),
- "edit any $type->type content" => array(
- 'title' => t('%type_name: Edit any content', array('%type_name' => $type->name)),
- ),
- "delete own $type->type content" => array(
- 'title' => t('%type_name: Delete own content', array('%type_name' => $type->name)),
- ),
- "delete any $type->type content" => array(
- 'title' => t('%type_name: Delete any content', array('%type_name' => $type->name)),
- ),
- "view $type->type revisions" => array(
- 'title' => t('%type_name: View revisions', array('%type_name' => $type->name)),
- ),
- "revert $type->type revisions" => array(
- 'title' => t('%type_name: Revert revisions', array('%type_name' => $type->name)),
- 'description' => t('Role requires permission view revisions and edit rights for nodes in question, or administer nodes.'),
- ),
- "delete $type->type revisions" => array(
- 'title' => t('%type_name: Delete revisions', array('%type_name' => $type->name)),
- 'description' => t('Role requires permission to view revisions and delete rights for nodes in question, or administer nodes.'),
- ),
- );
- return $perms;
-}
-
/**
* Fetches an array of permission IDs granted to the given user ID.
*
diff --git a/core/modules/node/node.permissions.yml b/core/modules/node/node.permissions.yml
new file mode 100644
index 0000000000000000000000000000000000000000..31a49927f6f0d2f7b6a805814cdb01fd8a7b8324
--- /dev/null
+++ b/core/modules/node/node.permissions.yml
@@ -0,0 +1,27 @@
+bypass node access:
+ title: 'Bypass content access control'
+ description: 'View edit and delete all content regardless of permission restrictions.'
+ 'restrict access': TRUE
+administer content types:
+ title: 'Administer content types'
+ description: 'Promote change ownership edit revisions and perform other tasks across all content types.'
+ 'restrict access': TRUE
+administer nodes:
+ title: 'Administer content'
+ 'restrict access': TRUE
+access content:
+ title: 'View published content'
+view own unpublished content:
+ title: 'View own unpublished content'
+view all revisions:
+ title: 'View all revisions'
+revert all revisions:
+ title: 'Revert all revisions'
+ description: 'Role requires permission view revisions and edit rights for nodes in question or administer nodes.'
+delete all revisions:
+ title: 'Delete all revisions'
+ description: 'Role requires permission to view revisions and delete rights for nodes in question or administer nodes.'
+
+permission_callbacks:
+ - \Drupal\node\NodePermissions::nodeTypePermissions
+ - \Drupal\node\NodePermissions::contentPermissions
diff --git a/core/modules/node/src/NodePermissions.php b/core/modules/node/src/NodePermissions.php
new file mode 100644
index 0000000000000000000000000000000000000000..5f25c64938c9195b5c5f947eca2db65159929d7e
--- /dev/null
+++ b/core/modules/node/src/NodePermissions.php
@@ -0,0 +1,94 @@
+ array(
+ 'title' => $this->t('Access the Content overview page'),
+ 'description' => $this->t('Get an overview of all content.', array('!url' => $this->url('system.admin_content'))),
+ ),
+ );
+ }
+
+ /**
+ * Returns an array of node type permissions.
+ *
+ * @return array
+ */
+ public function nodeTypePermissions() {
+ $perms = array();
+ // Generate node permissions for all node types.
+ foreach (NodeType::loadMultiple() as $type) {
+ $perms += $this->buildPermissions($type);
+ }
+
+ return $perms;
+ }
+
+ /**
+ * Builds a standard list of node permissions for a given type.
+ *
+ * @param \Drupal\node\Entity\NodeType $type
+ * The machine name of the node type.
+ *
+ * @return array
+ * An array of permission names and descriptions.
+ */
+ protected function buildPermissions(NodeType $type) {
+ $type_id = $type->id();
+ $type_params = array('%type_name' => $type->label());
+
+ return array(
+ "create $type_id content" => array(
+ 'title' => $this->t('%type_name: Create new content', $type_params),
+ ),
+ "edit own $type_id content" => array(
+ 'title' => $this->t('%type_name: Edit own content', $type_params),
+ ),
+ "edit any $type_id content" => array(
+ 'title' => $this->t('%type_name: Edit any content', $type_params),
+ ),
+ "delete own $type_id content" => array(
+ 'title' => $this->t('%type_name: Delete own content', $type_params),
+ ),
+ "delete any $type_id content" => array(
+ 'title' => $this->t('%type_name: Delete any content', $type_params),
+ ),
+ "view $type_id revisions" => array(
+ 'title' => $this->t('%type_name: View revisions', $type_params),
+ ),
+ "revert $type_id revisions" => array(
+ 'title' => $this->t('%type_name: Revert revisions', $type_params),
+ 'description' => t('Role requires permission view revisions and edit rights for nodes in question, or administer nodes.'),
+ ),
+ "delete $type_id revisions" => array(
+ 'title' => $this->t('%type_name: Delete revisions', $type_params),
+ 'description' => $this->t('Role requires permission to view revisions and delete rights for nodes in question, or administer nodes.'),
+ ),
+ );
+ }
+
+}
diff --git a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php
index adf573e1865163dc011e1afc0c7eb4ec7d12ea42..c8a8987211583a4806cc0475039991416d26f35b 100644
--- a/core/modules/simpletest/src/Tests/KernelTestBaseTest.php
+++ b/core/modules/simpletest/src/Tests/KernelTestBaseTest.php
@@ -77,8 +77,8 @@ function testEnableModulesLoad() {
* Tests expected installation behavior of enableModules().
*/
function testEnableModulesInstall() {
- $module = 'node';
- $table = 'node_access';
+ $module = 'module_test';
+ $table = 'module_test';
// Verify that the module does not exist yet.
$this->assertFalse(\Drupal::moduleHandler()->moduleExists($module), "$module module not found.");
diff --git a/core/modules/user/src/PermissionHandler.php b/core/modules/user/src/PermissionHandler.php
index 2336593c33f09c2da356b9a43c6f0c2bb13ed72c..1cbe222fdd1b333c12582746ccab7d4fc0bbc79f 100644
--- a/core/modules/user/src/PermissionHandler.php
+++ b/core/modules/user/src/PermissionHandler.php
@@ -8,6 +8,7 @@
namespace Drupal\user;
use Drupal\Component\Discovery\YamlDiscovery;
+use Drupal\Core\Controller\ControllerResolverInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
@@ -44,6 +45,13 @@ class PermissionHandler implements PermissionHandlerInterface {
*/
protected $yamlDiscovery;
+ /**
+ * The controller resolver.
+ *
+ * @var \Drupal\Core\Controller\ControllerResolverInterface
+ */
+ protected $controllerResolver;
+
/**
* Constructs a new PermissionHandler.
*
@@ -51,12 +59,15 @@ class PermissionHandler implements PermissionHandlerInterface {
* The module handler.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation.
+ * @param \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver
+ * The controller resolver.
*/
- public function __construct(ModuleHandlerInterface $module_handler, TranslationInterface $string_translation) {
+ public function __construct(ModuleHandlerInterface $module_handler, TranslationInterface $string_translation, ControllerResolverInterface $controller_resolver) {
// @todo It would be nice if you could pull all module directories from the
// container.
$this->moduleHandler = $module_handler;
$this->stringTranslation = $string_translation;
+ $this->controllerResolver = $controller_resolver;
}
/**
@@ -94,7 +105,38 @@ public function getPermissions() {
*/
protected function buildPermissionsYaml() {
$all_permissions = array();
+ $all_callback_permissions = array();
+
foreach ($this->getYamlDiscovery()->findAll() as $provider => $permissions) {
+ // The top-level 'permissions_callback' is a list of methods in controller
+ // syntax, see \Drupal\Core\Controller\ControllerResolver. These methods
+ // should return an array of permissions in the same structure.
+ if (isset($permissions['permission_callbacks'])) {
+ foreach ($permissions['permission_callbacks'] as $permission_callback) {
+ $callback = $this->controllerResolver->getControllerFromDefinition($permission_callback);
+ if ($callback_permissions = call_user_func($callback)) {
+ // Add any callback permissions to the array of permissions. Any
+ // defaults can then get processed below.
+ foreach ($callback_permissions as $name => $callback_permission) {
+ if (!is_array($callback_permission)) {
+ $callback_permission = array(
+ 'title' => $callback_permission,
+ );
+ }
+
+ $callback_permission += array(
+ 'description' => NULL,
+ );
+ $callback_permission['provider'] = $provider;
+
+ $all_callback_permissions[$name] = $callback_permission;
+ }
+ }
+ }
+
+ unset($permissions['permission_callbacks']);
+ }
+
foreach ($permissions as &$permission) {
if (!is_array($permission)) {
$permission = array(
@@ -105,9 +147,11 @@ protected function buildPermissionsYaml() {
$permission['description'] = isset($permission['description']) ? $this->t($permission['description']) : NULL;
$permission['provider'] = $provider;
}
+
$all_permissions += $permissions;
}
- return $all_permissions;
+
+ return $all_permissions + $all_callback_permissions;
}
/**
diff --git a/core/modules/user/tests/src/Unit/PermissionHandlerTest.php b/core/modules/user/tests/src/Unit/PermissionHandlerTest.php
index 9312544a3d6f6571b9c84d4004cbc3da7e80b21c..a25bbce71333d8f0d9db0f7feb40ea5115a2cfd3 100644
--- a/core/modules/user/tests/src/Unit/PermissionHandlerTest.php
+++ b/core/modules/user/tests/src/Unit/PermissionHandlerTest.php
@@ -44,11 +44,19 @@ class PermissionHandlerTest extends UnitTestCase {
*/
protected $stringTranslation;
+ /**
+ * The mocked controller resolver.
+ *
+ * @var \Drupal\Core\Controller\ControllerResolverInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $controllerResolver;
+
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->stringTranslation = $this->getStringTranslationStub();
+ $this->controllerResolver = $this->getMock('Drupal\Core\Controller\ControllerResolverInterface');
}
/**
@@ -112,7 +120,7 @@ public function testBuildPermissionsModules() {
->method('getModuleList')
->willReturn(array_flip($modules));
- $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation);
+ $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver);
// Setup system_rebuild_module_data().
$this->permissionHandler->setSystemRebuildModuleData($extensions);
@@ -180,7 +188,10 @@ public function testBuildPermissionsYaml() {
->method('getModuleList')
->willReturn(array_flip($modules));
- $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation);
+ $this->controllerResolver->expects($this->never())
+ ->method('getControllerFromDefinition');
+
+ $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver);
// Setup system_rebuild_module_data().
$this->permissionHandler->setSystemRebuildModuleData($extensions);
@@ -229,7 +240,7 @@ public function testBuildPermissionsSortPerModule() {
->method('getModuleList')
->willReturn(array_flip($modules));
- $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation);
+ $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver);
// Setup system_rebuild_module_data().
$this->permissionHandler->setSystemRebuildModuleData($extensions);
@@ -239,6 +250,143 @@ public function testBuildPermissionsSortPerModule() {
$this->assertEquals(['access_module_a1', 'access_module_a2'], array_keys($actual_permissions));
}
+ /**
+ * Tests dynamic callback permissions provided by YML files.
+ *
+ * @covers ::__construct
+ * @covers ::getPermissions
+ * @covers ::buildPermissions
+ * @covers ::buildPermissionsYaml
+ */
+ public function testBuildPermissionsYamlCallback() {
+ vfsStreamWrapper::register();
+ $root = new vfsStreamDirectory('modules');
+ vfsStreamWrapper::setRoot($root);
+
+ $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+ $this->moduleHandler->expects($this->once())
+ ->method('getModuleDirectories')
+ ->willReturn(array(
+ 'module_a' => vfsStream::url('modules/module_a'),
+ 'module_b' => vfsStream::url('modules/module_b'),
+ 'module_c' => vfsStream::url('modules/module_c'),
+ ));
+
+ $url = vfsStream::url('modules');
+ mkdir($url . '/module_a');
+ file_put_contents($url . '/module_a/module_a.permissions.yml',
+"permission_callbacks:
+ - 'Drupal\\user\\Tests\\TestPermissionCallbacks::singleDescription'
+");
+ mkdir($url . '/module_b');
+ file_put_contents($url . '/module_b/module_b.permissions.yml',
+"permission_callbacks:
+ - 'Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescription'
+");
+ mkdir($url . '/module_c');
+ file_put_contents($url . '/module_c/module_c.permissions.yml',
+"permission_callbacks:
+ - 'Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescriptionRestrictAccess'
+");
+
+ $modules = array('module_a', 'module_b', 'module_c');
+ $extensions = array(
+ 'module_a' => $this->mockModuleExtension('module_a', 'Module a'),
+ 'module_b' => $this->mockModuleExtension('module_b', 'Module b'),
+ 'module_c' => $this->mockModuleExtension('module_c', 'Module c'),
+ );
+
+ $this->moduleHandler->expects($this->any())
+ ->method('getImplementations')
+ ->with('permission')
+ ->willReturn(array());
+
+ $this->moduleHandler->expects($this->any())
+ ->method('getModuleList')
+ ->willReturn(array_flip($modules));
+
+ $this->controllerResolver->expects($this->at(0))
+ ->method('getControllerFromDefinition')
+ ->with('Drupal\\user\\Tests\\TestPermissionCallbacks::singleDescription')
+ ->willReturn(array(new TestPermissionCallbacks(), 'singleDescription'));
+ $this->controllerResolver->expects($this->at(1))
+ ->method('getControllerFromDefinition')
+ ->with('Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescription')
+ ->willReturn(array(new TestPermissionCallbacks(), 'titleDescription'));
+ $this->controllerResolver->expects($this->at(2))
+ ->method('getControllerFromDefinition')
+ ->with('Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescriptionRestrictAccess')
+ ->willReturn(array(new TestPermissionCallbacks(), 'titleDescriptionRestrictAccess'));
+
+ $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver);
+
+ // Setup system_rebuild_module_data().
+ $this->permissionHandler->setSystemRebuildModuleData($extensions);
+
+ $actual_permissions = $this->permissionHandler->getPermissions();
+ $this->assertPermissions($actual_permissions);
+ }
+
+ /**
+ * Tests a YAML file containing both static permissions and a callback.
+ */
+ public function testPermissionsYamlStaticAndCallback() {
+ vfsStreamWrapper::register();
+ $root = new vfsStreamDirectory('modules');
+ vfsStreamWrapper::setRoot($root);
+
+ $this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
+ $this->moduleHandler->expects($this->once())
+ ->method('getModuleDirectories')
+ ->willReturn(array(
+ 'module_a' => vfsStream::url('modules/module_a'),
+ ));
+
+ $url = vfsStream::url('modules');
+ mkdir($url . '/module_a');
+ file_put_contents($url . '/module_a/module_a.permissions.yml',
+"'access module a':
+ title: 'Access A'
+ description: 'bla bla'
+permission_callbacks:
+ - 'Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescription'
+");
+
+ $modules = array('module_a');
+ $extensions = array(
+ 'module_a' => $this->mockModuleExtension('module_a', 'Module a'),
+ );
+
+ $this->moduleHandler->expects($this->any())
+ ->method('getImplementations')
+ ->with('permission')
+ ->willReturn(array());
+
+ $this->moduleHandler->expects($this->any())
+ ->method('getModuleList')
+ ->willReturn(array_flip($modules));
+
+ $this->controllerResolver->expects($this->once())
+ ->method('getControllerFromDefinition')
+ ->with('Drupal\\user\\Tests\\TestPermissionCallbacks::titleDescription')
+ ->willReturn(array(new TestPermissionCallbacks(), 'titleDescription'));
+
+ $this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation, $this->controllerResolver);
+
+ // Setup system_rebuild_module_data().
+ $this->permissionHandler->setSystemRebuildModuleData($extensions);
+
+ $actual_permissions = $this->permissionHandler->getPermissions();
+
+ $this->assertCount(2, $actual_permissions);
+ $this->assertEquals($actual_permissions['access module a']['title'], 'Access A');
+ $this->assertEquals($actual_permissions['access module a']['provider'], 'module_a');
+ $this->assertEquals($actual_permissions['access module a']['description'], 'bla bla');
+ $this->assertEquals($actual_permissions['access module b']['title'], 'Access B');
+ $this->assertEquals($actual_permissions['access module b']['provider'], 'module_a');
+ $this->assertEquals($actual_permissions['access module b']['description'], 'bla bla');
+ }
+
/**
* Checks that the permissions are like expected.
*
@@ -276,3 +424,32 @@ public function setSystemRebuildModuleData(array $extensions) {
}
}
+
+class TestPermissionCallbacks {
+
+ public function singleDescription() {
+ return array(
+ 'access_module_a' => 'single_description'
+ );
+ }
+
+ public function titleDescription() {
+ return array(
+ 'access module b' => array(
+ 'title' => 'Access B',
+ 'description' => 'bla bla',
+ ),
+ );
+ }
+
+ public function titleDescriptionRestrictAccess() {
+ return array(
+ 'access_module_c' => array(
+ 'title' => 'Access C',
+ 'description' => 'bla bla',
+ 'restrict access' => TRUE,
+ ),
+ );
+ }
+
+}
diff --git a/core/modules/user/user.services.yml b/core/modules/user/user.services.yml
index 7ccd7beaedcd8a01c3a3eaebfe24862e876e913e..7ffcc65f4fc187abccef1ddd7a6251dd5a51e99e 100644
--- a/core/modules/user/user.services.yml
+++ b/core/modules/user/user.services.yml
@@ -61,7 +61,7 @@ services:
- { name: backend_overridable }
user.permissions:
class: Drupal\user\PermissionHandler
- arguments: ['@module_handler', '@string_translation']
+ arguments: ['@module_handler', '@string_translation', '@controller_resolver']
parameters:
user.tempstore.expire: 604800