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 <a href="!url">all content</a>.', 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 <em>view revisions</em> and <em>edit rights</em> for nodes in question, or <em>administer nodes</em>.'), - ), - 'delete all revisions' => array( - 'title' => t('Delete all revisions'), - 'description' => t('Role requires permission to <em>view revisions</em> and <em>delete rights</em> for nodes in question, or <em>administer nodes</em>.'), - ), - ); - - // 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 <em>view revisions</em> and <em>edit rights</em> for nodes in question, or <em>administer nodes</em>.'), - ), - "delete $type->type revisions" => array( - 'title' => t('%type_name: Delete revisions', array('%type_name' => $type->name)), - 'description' => t('Role requires permission to <em>view revisions</em> and <em>delete rights</em> for nodes in question, or <em>administer nodes</em>.'), - ), - ); - 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 <em>view revisions</em> and <em>edit rights</em> for nodes in question or <em>administer nodes</em>.' +delete all revisions: + title: 'Delete all revisions' + description: 'Role requires permission to <em>view revisions</em> and <em>delete rights</em> for nodes in question or <em>administer nodes</em>.' + +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 @@ +<?php + +/** + * @file + * Contains \Drupal\node\NodePermissions. + */ + +namespace Drupal\node; + +use Drupal\Core\Routing\UrlGeneratorTrait; +use Drupal\Core\StringTranslation\StringTranslationTrait; +use Drupal\node\Entity\NodeType; + +/** + * Defines a class containing permission callbacks. + */ +class NodePermissions { + + use StringTranslationTrait; + use UrlGeneratorTrait; + + /** + * Returns an array of content permissions. + * + * @return array + */ + public function contentPermissions() { + return array( + 'access content overview' => array( + 'title' => $this->t('Access the Content overview page'), + 'description' => $this->t('Get an overview of <a href="!url">all content</a>.', 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 <em>view revisions</em> and <em>edit rights</em> for nodes in question, or <em>administer nodes</em>.'), + ), + "delete $type_id revisions" => array( + 'title' => $this->t('%type_name: Delete revisions', $type_params), + 'description' => $this->t('Role requires permission to <em>view revisions</em> and <em>delete rights</em> for nodes in question, or <em>administer nodes</em>.'), + ), + ); + } + +} 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