Commit 6227e3aa authored by webchick's avatar webchick

Issue #2295469 by Cottser, dawehner | effulgentsia: Add support for static...

Issue #2295469 by Cottser, dawehner | effulgentsia: Add support for static permission definitions with *.permissions.yml.
parent a9854eeb
......@@ -16,7 +16,7 @@ interface DiscoverableInterface {
* Returns an array of discoverable items.
*
* @return array
* An array of discovered data.
* An array of discovered data keyed by provider.
*/
public function findAll();
......
......@@ -649,7 +649,7 @@ protected function drupalCreateRole(array $permissions, $rid = NULL, $name = NUL
* TRUE if the permissions are valid, FALSE otherwise.
*/
protected function checkPermissions(array $permissions) {
$available = array_keys(\Drupal::moduleHandler()->invokeAll('permission'));
$available = array_keys(\Drupal::service('user.permissions')->getPermissions());
$valid = TRUE;
foreach ($permissions as $permission) {
if (!in_array($permission, $available)) {
......
......@@ -32,7 +32,7 @@ class BreadcrumbTest extends MenuTestBase {
protected function setUp() {
parent::setUp();
$perms = array_keys(\Drupal::moduleHandler()->invokeAll('permission'));
$perms = array_keys(\Drupal::service('user.permissions')->getPermissions());
$this->admin_user = $this->drupalCreateUser($perms);
$this->drupalLogin($this->admin_user);
......
......@@ -45,7 +45,7 @@ protected function setUp() {
// Create an administrator with all permissions, as well as a regular user
// who can only access administration pages and perform some Locale module
// administrative tasks, but not all of them.
$this->admin_user = $this->drupalCreateUser(array_keys(\Drupal::moduleHandler()->invokeAll('permission')));
$this->admin_user = $this->drupalCreateUser(array_keys(\Drupal::service('user.permissions')->getPermissions()));
$this->web_user = $this->drupalCreateUser(array(
'access administration pages',
'translate interface',
......
......@@ -8,9 +8,9 @@
namespace Drupal\user\Form;
use Drupal\Component\Utility\String;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\Form\FormBase;
use Drupal\Core\Form\FormStateInterface;
use Drupal\user\PermissionHandlerInterface;
use Drupal\user\RoleStorageInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
......@@ -20,11 +20,11 @@
class UserPermissionsForm extends FormBase {
/**
* The module handler.
* The permission handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
* @var \Drupal\user\PermissionHandlerInterface
*/
protected $moduleHandler;
protected $permissionHandler;
/**
* The role storage.
......@@ -36,13 +36,13 @@ class UserPermissionsForm extends FormBase {
/**
* Constructs a new UserPermissionsForm.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\user\PermissionHandlerInterface $permission_handler
* The permission handler.
* @param \Drupal\user\RoleStorageInterface $role_storage
* The role storage.
*/
public function __construct(ModuleHandlerInterface $module_handler, RoleStorageInterface $role_storage) {
$this->moduleHandler = $module_handler;
public function __construct(PermissionHandlerInterface $permission_handler, RoleStorageInterface $role_storage) {
$this->permissionHandler = $permission_handler;
$this->roleStorage = $role_storage;
}
......@@ -51,7 +51,7 @@ public function __construct(ModuleHandlerInterface $module_handler, RoleStorageI
*/
public static function create(ContainerInterface $container) {
return new static(
$container->get('module_handler'),
$container->get('user.permissions'),
$container->get('entity.manager')->getStorage('user_role')
);
}
......@@ -96,14 +96,6 @@ public function buildForm(array $form, FormStateInterface $form_state) {
$module_info = system_rebuild_module_data();
$hide_descriptions = system_admin_compact_mode();
// Get a list of all the modules implementing a hook_permission() and sort by
// display name.
$modules = array();
foreach ($this->moduleHandler->getImplementations('permission') as $module) {
$modules[$module] = $module_info[$module]->info['name'];
}
asort($modules);
$form['system_compact_link'] = array(
'#theme' => 'system_compact_link',
);
......@@ -121,55 +113,59 @@ public function buildForm(array $form, FormStateInterface $form_state) {
);
}
foreach ($modules as $module => $display_name) {
if ($permissions = $this->moduleHandler->invoke($module, 'permission')) {
// Module name.
$form['permissions'][$module] = array(array(
$permissions = $this->permissionHandler->getPermissions();
$permissions_by_provider = array();
foreach ($permissions as $permission_name => $permission) {
$permissions_by_provider[$permission['provider']][$permission_name] = $permission;
}
foreach ($permissions_by_provider as $provider => $permissions) {
// Module name.
$form['permissions'][$provider] = array(array(
'#wrapper_attributes' => array(
'colspan' => count($role_names) + 1,
'class' => array('module'),
'id' => 'module-' . $provider,
),
'#markup' => $module_info[$provider]->info['name'],
));
foreach ($permissions as $perm => $perm_item) {
// Fill in default values for the permission.
$perm_item += array(
'description' => '',
'restrict access' => FALSE,
'warning' => !empty($perm_item['restrict access']) ? $this->t('Warning: Give to trusted roles only; this permission has security implications.') : '',
);
$options[$perm] = $perm_item['title'];
// Show the permission description.
if (!$hide_descriptions) {
$user_permission_description = $perm_item['description'];
// Append warning message.
if (!empty($perm_item['warning'])) {
$user_permission_description .= ' <em class="permission-warning">' . $perm_item['warning'] . '</em>';
}
}
$form['permissions'][$perm]['description'] = array(
'#wrapper_attributes' => array(
'colspan' => count($role_names) + 1,
'class' => array('module'),
'id' => 'module-' . $module,
'class' => array('permission'),
),
'#markup' => $module_info[$module]->info['name'],
));
foreach ($permissions as $perm => $perm_item) {
// Fill in default values for the permission.
$perm_item += array(
'description' => '',
'restrict access' => FALSE,
'warning' => !empty($perm_item['restrict access']) ? $this->t('Warning: Give to trusted roles only; this permission has security implications.') : '',
);
$options[$perm] = $perm_item['title'];
// Show the permission description.
if (!$hide_descriptions) {
$user_permission_description = $perm_item['description'];
// Append warning message.
if (!empty($perm_item['warning'])) {
$user_permission_description .= ' <em class="permission-warning">' . $perm_item['warning'] . '</em>';
}
}
$form['permissions'][$perm]['description'] = array(
'#type' => 'item',
'#markup' => $perm_item['title'],
'#description' => $user_permission_description,
);
$options[$perm] = '';
foreach ($role_names as $rid => $name) {
$form['permissions'][$perm][$rid] = array(
'#title' => $name . ': ' . $perm_item['title'],
'#title_display' => 'invisible',
'#wrapper_attributes' => array(
'class' => array('permission'),
'class' => array('checkbox'),
),
'#type' => 'item',
'#markup' => $perm_item['title'],
'#description' => $user_permission_description,
'#type' => 'checkbox',
'#default_value' => in_array($perm, $role_permissions[$rid]) ? 1 : 0,
'#attributes' => array('class' => array('rid-' . $rid)),
'#parents' => array($rid, $perm),
);
$options[$perm] = '';
foreach ($role_names as $rid => $name) {
$form['permissions'][$perm][$rid] = array(
'#title' => $name . ': ' . $perm_item['title'],
'#title_display' => 'invisible',
'#wrapper_attributes' => array(
'class' => array('checkbox'),
),
'#type' => 'checkbox',
'#default_value' => in_array($perm,$role_permissions[$rid]) ? 1 : 0,
'#attributes' => array('class' => array('rid-' . $rid)),
'#parents' => array($rid, $perm),
);
}
}
}
}
......
<?php
/**
* @file
* Contains \Drupal\user\PermissionHandler.
*/
namespace Drupal\user;
use Drupal\Component\Discovery\YamlDiscovery;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
/**
* Provides the available permissions based on hook_permission and yml files.
*
* To define permissions you can use a $module.permissions.yml file:
*
* @code
* 'access all views':
* title: 'Bypass views access control'
* description: 'Bypass access control when accessing views.'
* 'restrict access': TRUE
* @encode
*
* @see hook_permission()
*/
class PermissionHandler implements PermissionHandlerInterface {
use StringTranslationTrait;
/**
* The module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface
*/
protected $moduleHandler;
/**
* The YAML discovery class to find all .permissions.yml files.
*
* @var \Drupal\Component\Discovery\YamlDiscovery
*/
protected $yamlDiscovery;
/**
* Constructs a new PermissionHandler.
*
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
* The module handler.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation.
*/
public function __construct(ModuleHandlerInterface $module_handler, TranslationInterface $string_translation) {
// @todo It would be nice if you could pull all module directories from the
// container.
$this->moduleHandler = $module_handler;
$this->stringTranslation = $string_translation;
}
/**
* Gets the YAML discovery.
*
* @return \Drupal\Component\Discovery\YamlDiscovery
* The YAML discovery.
*/
protected function getYamlDiscovery() {
if (!isset($this->yamlDiscovery)) {
$this->yamlDiscovery = new YamlDiscovery('permissions', $this->moduleHandler->getModuleDirectories());
}
return $this->yamlDiscovery;
}
/**
* {@inheritdoc}
*/
public function getPermissions() {
$all_permissions = $this->buildPermissionsYaml();
$all_permissions += $this->buildPermissionsModules();
return $this->sortPermissionsByProviderName($all_permissions);
}
/**
* Builds all permissions provided by .permissions.yml files.
*
* @return array[]
* Each return permission is an array with the following keys:
* - title: The title of the permission.
* - description: The description of the permission, defaults to NULL.
* - provider: The provider of the permission.
*/
protected function buildPermissionsYaml() {
$all_permissions = array();
foreach ($this->getYamlDiscovery()->findAll() as $provider => $permissions) {
foreach ($permissions as &$permission) {
if (!is_array($permission)) {
$permission = array(
'title' => $permission,
);
}
$permission['title'] = $this->t($permission['title']);
$permission['description'] = isset($permission['description']) ? $this->t($permission['description']) : NULL;
$permission['provider'] = $provider;
}
$all_permissions += $permissions;
}
return $all_permissions;
}
/**
* Builds all permissions provided by .module files.
*
* @return array[]
* Each return permission is an array with the following keys:
* - title: The title of the permission.
* - description: The description of the permission, defaults to NULL.
* - provider: The provider of the permission.
*/
protected function buildPermissionsModules() {
$all_permissions = array();
foreach ($this->moduleHandler->getImplementations('permission') as $provider) {
$permissions = $this->moduleHandler->invoke($provider, 'permission');
foreach ($permissions as &$permission) {
if (!is_array($permission)) {
$permission = array(
'title' => $permission,
'description' => NULL,``
);
}
$permission['provider'] = $provider;
}
$all_permissions += $permissions;
}
return $all_permissions;
}
/**
* Sorts the given permissions by provider name.
*
* @param array $all_permissions
* The permissions to be sorted.
*
* @return array[]
* Each return permission is an array with the following keys:
* - title: The title of the permission.
* - description: The description of the permission, defaults to NULL.
* - provider: The provider of the permission.
*/
protected function sortPermissionsByProviderName(array $all_permissions = array()) {
// Get a list of all the modules providing permissions and sort by
// display name.
$modules = $this->getModuleNames();
uasort($all_permissions, function (array $permission_a, array $permission_b) use ($modules) {
return $modules[$permission_a['provider']] > $modules[$permission_b['provider']];
});
return $all_permissions;
}
/**
* Returns all module names.
*
* @return string[]
* Returns the human readable names of all modules keyed by machine name.
*/
protected function getModuleNames() {
$modules = array();
$module_info = $this->systemRebuildModuleData();
foreach (array_keys($this->moduleHandler->getModuleList()) as $module) {
$modules[$module] = $module_info[$module]->info['name'];
}
asort($modules);
return $modules;
}
/**
* Wraps system_rebuild_module_data()
*
* @return \Drupal\Core\Extension\Extension[]
*/
protected function systemRebuildModuleData() {
return system_rebuild_module_data();
}
}
<?php
/**
* @file
* Contains \Drupal\user\PermissionHandlerInterface.
*/
namespace Drupal\user;
/**
* Defines an interface to list available permissions.
*/
interface PermissionHandlerInterface {
/**
* Gets all available permissions.
*
* @return array
* An array whose keys are permission names and whose corresponding values
* are arrays containing the following key-value pairs:
* - title: The human-readable name of the permission, to be shown on the
* permission administration page. This should be wrapped in the t()
* function so it can be translated.
* - description: (optional) A description of what the permission does. This
* should be wrapped in the t() function so it can be translated.
* - restrict access: (optional) A boolean which can be set to TRUE to
* indicate that site administrators should restrict access to this
* permission to trusted users. This should be used for permissions that
* have inherent security risks across a variety of potential use cases
* (for example, the "administer filters" and "bypass node access"
* permissions provided by Drupal core). When set to TRUE, a standard
* warning message defined in user_admin_permissions() will be displayed
* with the permission on the permission administration page. Defaults
* to FALSE.
* - warning: (optional) A translated warning message to display for this
* permission on the permission administration page. This warning
* overrides the automatic warning generated by 'restrict access' being
* set to TRUE. This should rarely be used, since it is important for all
* permissions to have a clear, consistent security warning that is the
* same across the site. Use the 'description' key instead to provide any
* information that is specific to the permission you are defining.
* - provider: (optional) The provider name of the permission.
*/
public function getPermissions();
}
......@@ -45,7 +45,7 @@ public function alterRouteDefinition(Route $route) {
}
public function summaryTitle() {
$permissions = \Drupal::moduleHandler()->invokeAll('permission');
$permissions = \Drupal::service('user.permissions')->getPermissions();
if (isset($permissions[$this->options['perm']])) {
return $permissions[$this->options['perm']]['title'];
}
......
......@@ -83,7 +83,7 @@ public function preRender(&$values) {
$uids = array();
$this->items = array();
$permission_names = \Drupal::moduleHandler()->invokeAll('permission');
$permission_names = \Drupal::service('user.permissions')->getPermissions();
$rids = array();
foreach ($values as $result) {
......
<?php
/**
* @file
* Contains \Drupal\user\Tests\PermissionHandlerTest.
*/
namespace Drupal\user\Tests;
use Drupal\Core\Extension\Extension;
use Drupal\Tests\UnitTestCase;
use Drupal\user\PermissionHandler;
use org\bovigo\vfs\vfsStream;
use org\bovigo\vfs\vfsStreamDirectory;
use org\bovigo\vfs\vfsStreamWrapper;
/**
* Tests the permission handler.
*
* @group user
*
* @coversDefaultClass \Drupal\user\PermissionHandler
*/
class PermissionHandlerTest extends UnitTestCase {
/**
* The tested permission handler.
*
* @var \Drupal\user\Tests\TestPermissionHandler|\Drupal\user\PermissionHandler
*/
protected $permissionHandler;
/**
* The mocked module handler.
*
* @var \Drupal\Core\Extension\ModuleHandlerInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $moduleHandler;
/**
* The mocked string translation.
*
* @var \Drupal\Core\StringTranslation\TranslationInterface|\PHPUnit_Framework_MockObject_MockObject
*/
protected $stringTranslation;
/**
* {@inheritdoc}
*/
protected function setUp() {
$this->stringTranslation = $this->getStringTranslationStub();
}
/**
* Provides an extension object for a given module with a human name.
*
* @param string $module
* The module machine name.
* @param string $name
* The module human name.
*
* @return \Drupal\Core\Extension\Extension
* The extension object.
*/
protected function mockModuleExtension($module, $name) {
$extension = new Extension($module, "modules/$module");
$extension->info['name'] = $name;
return $extension;
}
/**
* Tests permissions by hook_permission.
*
* @covers ::__construct
* @covers ::getPermissions
* @covers ::buildPermissions
* @covers ::buildPermissionsModules
* @covers ::sortPermissionsByProviderName
* @covers ::getModuleNames
*/
public function testBuildPermissionsModules() {
$modules = array('module_a', 'module_b', 'module_c');
$extensions = array(
'module_a' => $this->mockModuleExtension('module_a', 'Module a'),
'module_b' => $this->mockModuleExtension('module_b', 'Moduleb'),
'module_c' => $this->mockModuleExtension('module_c', 'Module c'),
);
$permissions = array(
'module_a' => array('access_module_a' => 'single_description'),
'module_b' => array('access module b' => array('title' => 'Access B', 'description' => 'bla bla')),
'module_c' => array('access_module_c' => array('title' => 'Access C', 'description' => 'bla bla', 'restrict access' => TRUE)),
);
$this->moduleHandler = $this->getMock('Drupal\Core\Extension\ModuleHandlerInterface');
$this->moduleHandler->expects($this->once())
->method('getModuleDirectories')
->willReturn([]);
$this->moduleHandler->expects($this->at(1))
->method('getImplementations')
->with('permission')
->willReturn($modules);
// Setup the module handler.
$i = 2;
foreach ($modules as $module_name) {
$this->moduleHandler->expects($this->at($i++))
->method('invoke')
->with($module_name)
->willReturn($permissions[$module_name]);
}
$this->moduleHandler->expects($this->any())
->method('getModuleList')
->willReturn(array_flip($modules));
$this->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation);
// Setup system_rebuild_module_data().
$this->permissionHandler->setSystemRebuildModuleData($extensions);
$actual_permissions = $this->permissionHandler->getPermissions();
$this->assertPermissions($actual_permissions);
// Ensure that the human name of the module is taken into account for the
// sorting.
$this->assertSame(array('access_module_a', 'access_module_c', 'access module b'), array_keys($actual_permissions));
}
/**
* Tests permissions provided by YML files.
*
* @covers ::__construct
* @covers ::getPermissions
* @covers ::buildPermissions
* @covers ::buildPermissionsYaml
*/
public function testBuildPermissionsYaml() {
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',
"access_module_a: single_description"
);
mkdir($url . '/module_b');
file_put_contents($url . '/module_b/module_b.permissions.yml',
"'access module b':
title: 'Access B'
description: 'bla bla'
");
mkdir($url . '/module_c');
file_put_contents($url . '/module_c/module_c.permissions.yml',
"'access_module_c':
title: 'Access C'
description: 'bla bla'
'restrict access': TRUE
");
$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->permissionHandler = new TestPermissionHandler($this->moduleHandler, $this->stringTranslation);
// Setup system_rebuild_module_data().
$this->permissionHandler->setSystemRebuildModuleData($extensions);
$actual_permissions = $this->permissionHandler->getPermissions();
$this->assertPermissions($actual_permissions);
}
/**
* Checks that the permissions are like expected.
*
* @param array $actual_permissions
* The actual permissions
*/
protected function assertPermissions(array $actual_permissions) {
$this->assertCount(3, $actual_permissions);
$this->assertEquals($actual_permissions['access_module_a']['title'], 'single_description');
$this->assertEquals($actual_permissions['access_module_a']['provider'], 'module_a');
$this->assertEquals($actual_permissions['access module b']['title'], 'Access B');
$this->assertEquals($actual_permissions['access module b']['provider'], 'module_b');
$this->assertEquals($actual_permissions['access_module_c']['title'], 'Access C');
$this->assertEquals($actual_permissions['access_module_c']['provider'], 'module_c');
$this->assertEquals($actual_permissions['access_module_c']['restrict access'], TRUE);
}
}
class TestPermissionHandler extends PermissionHandler {
/**
* Test module data.
*
* @var array
*/
protected $systemModuleData;
protected function systemRebuildModuleData() {
return $this->systemModuleData;
}
public function setSystemRebuildModuleData(array $extensions) {
$this->systemModuleData = $extensions;
}
}
......@@ -59,6 +59,9 @@ services:
arguments: ['@serialization.phpserialize', '@database', '@lock', '%user.tempstore.expire%']
tags:
- { name: backend_overridable }
user.permissions:
class: Drupal\user\PermissionHandler
arguments: ['@module_handler', '@string_translation']
parameters:
user.tempstore.expire: 604800
......@@ -292,19 +292,6 @@ function views_theme_suggestions_comment_alter(array &$suggestions, array $varia
}
}
/**
* Implements hook_permission().
*/