acb.module in Access Control Bridge 8
Same filename and directory in other branches
Drupal hooks and functions for the acb module.
@TODO: code cleanup. @TODO: define services where appropriate.
File
acb.moduleView source
<?php
/**
* @file
* Drupal hooks and functions for the acb module.
*
* @TODO: code cleanup.
* @TODO: define services where appropriate.
*/
use Drupal\Core\Session\AccountInterface;
use Drupal\node\NodeInterface;
use Drupal\user\Entity\User;
/**
* Implements hook_modules_installed().
*/
function acb_modules_installed($modules) {
// Check if newly enabled modules are access control modules.
$new = count(array_intersect(acb_get_modules(TRUE), $modules));
if ($new) {
// Ask for rebuilding node permissions.
node_access_needs_rebuild(TRUE);
}
}
/**
* Returns a list of all active access control modules.
*
* @param bool $self
* Boolean indicating if acb itself should be included in the list.
*
* @return array
* An array containing all active access controlling modules.
*/
function acb_get_modules($self = FALSE) {
$modules = array_unique(array_merge(\Drupal::moduleHandler()
->getImplementations('node_grants'), \Drupal::moduleHandler()
->getImplementations('node_access_records')));
// Exclude ACL as it doesn't control anything on its own.
$exclude = [
'acl',
];
if ($self) {
$exclude[] = 'acb';
}
// Sort the list so the modules are always returned in the same order.
sort($modules);
return array_diff($modules, $exclude);
}
/**
* Implements hook_node_access_records_alter().
*
* Creates and inserts our cascaded access records for nodes.
*/
function acb_node_access_records_alter(&$grants, NodeInterface $node) {
// Split all grants into their respective controlling modules.
$split_grants = [];
foreach (acb_get_modules() as $module) {
$grant_storage = [];
foreach ($grants as $key => $grant) {
switch ($grant['realm']) {
// 'all' and ACL require special care.
case 'acl':
case 'all':
// Check 'module' or '#module' key. If not present, then assume it to be Content Access.
if (isset($grant['module'])) {
$realm = $grant['module'];
}
elseif (isset($grant['#module'])) {
$realm = $grant['#module'];
}
else {
$realm = 'content_access';
}
break;
default:
$realm = $grant['realm'];
break;
}
if (acb_is_module_realm($realm, $module) === TRUE) {
$grant_storage[$key] = $grant;
}
}
$split_grants[$module] = $grant_storage;
}
// Filter out any modules that didn't set access records.
$split_grants = array_filter($split_grants);
// Only proceed and alter the grants if multiple modules control this node.
// If this is not the case, the legacy user grants will control the node as usual.
if (count($split_grants) <= 1) {
return;
}
// Convert 'all' realm grants to anonymous and authenticated role grants.
// @TODO: check if still needed in D8.
$roles = [
0 => AccountInterface::ANONYMOUS_ROLE,
1 => AccountInterface::AUTHENTICATED_ROLE,
];
foreach ($split_grants as $module => $split_grant_module) {
foreach ($split_grant_module as $key => $split_grant) {
if ($split_grant['realm'] == 'all') {
foreach ($roles as $rid => $rolename) {
$split_grants[$module][$rid] = $split_grant;
$split_grants[$module][$rid]['realm'] = $module . '_rid';
$split_grants[$module][$rid]['gid'] = $rid;
}
unset($split_grants[$module][$key]);
}
}
}
// We keep the existing grants and merge ours because we overrule them
// by setting a high priority on our own grants in _acb_cascade_grants().
// $grants = array_merge($grants, _acb_cascade_grants($split_grants, TRUE));
// @TODO: Find out if there is a reason to keep existing grants.
// Currently we only assign the ACB grants, to enforce multiple access control
// modules working together as expected.
$grants = _acb_cascade_grants($split_grants, TRUE);
}
/**
* Implements hook_node_grants_alter().
*
* Let Drupal know of the current user's grants.
* We're using the _alter function because we need existing grants to calculate ours.
*/
function acb_node_grants_alter(&$grants, AccountInterface $account, $op) {
// Split all grants into their respective controlling modules and prepare the array for the recursive cascader.
$split_grants = [];
// ACL requires special care. The responsible module can only be determined by using the database.
if (\Drupal::moduleHandler()
->moduleExists('acl')) {
$acl = db_select('acl')
->fields('acl', [
'acl_id',
'module',
])
->execute()
->fetchAllAssoc('acl_id');
}
// For our ACB grants, we need the 'Edit any domain content' and 'Delete any domain content' permissions to be enabled to
// enable other modules to control it effectively (due to the AND-logic we're implementing).
// Add them to the user grants if not already present if Domain Access is active.
// Additionally, fork $grants so we don't add them permanently to the referenced grants array.
$acb_grants = $grants;
if (\Drupal::moduleHandler()
->moduleExists('domain_access') && ($op == 'update' || $op == 'delete')) {
$user = User::load($account
->id());
$domains = \Drupal::service('domain_access.manager')
->getAccessValues($user);
if (!empty($domains)) {
foreach ($domains as $id) {
if (abs($id) > 0) {
$value = $id > 0 ? $id : 0;
if (!isset($acb_grants['domain_id']) || !in_array($value, $acb_grants['domain_id'])) {
$acb_grants['domain_id'][] = $value;
}
}
}
}
}
foreach (acb_get_modules() as $module) {
// ACL requires special care. Extract the ACL ids for the current module.
if (\Drupal::moduleHandler()
->moduleExists('acl')) {
$entry_storage = [];
foreach ($acl as $key => $entry) {
if (acb_is_module_realm($entry->module, $module) === TRUE) {
$entry_storage[$key] = $entry;
}
}
$acl_current = array_keys($entry_storage);
}
foreach ($acb_grants as $realm => $gids) {
// ACL requires special care. Filter out any ACL entries that were not set by the current module.
if ($realm == 'acl') {
$gids = array_intersect($gids, $acl_current);
}
// If this realm is originating from the current module (or if ACL has entries), then add the gids.
if (acb_is_module_realm($realm, $module) === TRUE || $realm == 'acl' && !empty($gids)) {
// Eventually prepare the module's subarray for pushing.
if (empty($split_grants[$module])) {
$split_grants[$module] = [];
}
foreach ($gids as $gid) {
array_push($split_grants[$module], [
'realm' => $realm,
'gid' => $gid,
]);
}
}
}
}
// We keep the existing grants and merge ACB's to support the case where only one module controls the node.
// Filter out any modules that didn't set grants.
$grants = array_merge($grants, _acb_cascade_grants(array_filter($split_grants)));
}
/**
* Checks if a realm is originating from a specified module.
*
* @param string $realm
* A string containing a realm machine_name
* @param string $module
* A module's machine name
*
* @return bool
* TRUE if the specified realm originates from the given module,
* FALSE otherwise.
*/
function acb_is_module_realm($realm, $module) {
// Just in case, ensure consistency.
if ($module == 'domain_access') {
$module = 'domain';
}
switch ($realm) {
case 'term_access':
return $module == 'taxonomy_access';
break;
default:
return strpos($realm, $module) === 0;
break;
}
}
/**
* The beating heart of this module. Receives split-sorted grants
* and converts it into cascaded grants to be used by this module.
* Workhorse for both hook_node_grants_alter() and hook_node_access_records_alter().
*
* @param array $grants
* The recursive grants array.
* @param bool $access_records
* (optional) Boolean indicating if we should cascade access records (TRUE) or not user grants (FALSE)
* @param array $output
* (optional) The final output array used for recursing.
* @param array $realm
* (optional) The realm storage array used for recursing.
* @param bool $view
* (optional) The grant_view value used for recursing.
* @param bool $update
* (optional) The grant_update value used for recursing.
* @param bool $delete
* (optional) The grant_delete value used for recursing.
*
* @return array
* The output grants array.
*/
function _acb_cascade_grants($grants, $access_records = FALSE, &$output = [], $realm = [], $view = 1, $update = 1, $delete = 1) {
// Let's make any possible cross-combination respecting the given element order of $grants.
if (count($grants)) {
// This nesting level has available combinations left.
foreach (reset($grants) as $grant) {
// Assemble new pieces for the realm.
$realm_storage = $realm;
$realm_storage[] = $grant['realm'] . '+' . $grant['gid'];
// Try to go one level deeper.
$grants_storage = array_slice($grants, 1);
// The recursive function is called differently for node access records and user grants.
if ($access_records) {
// We can use simple math to cascade each node access record grant value:
// multiplication ensures that zeros (deny) are retained throughout cascading
// while a one (allow) is only possible if all cascaded grants allow access.
_acb_cascade_grants($grants_storage, $access_records, $output, $realm_storage, $view * $grant['grant_view'], $update * $grant['grant_update'], $delete * $grant['grant_delete']);
}
else {
// For the user grants we call the recursive function twice: once using the updated realm and once pretending the realm wouldn't exist.
_acb_cascade_grants($grants_storage, $access_records, $output, $realm);
_acb_cascade_grants($grants_storage, $access_records, $output, $realm_storage);
}
}
}
// Create a record for this combination if:
// - in case of access records: there are no remaining nesting levels
// - in case of user grants: always!
if (!count($grants) || !$access_records) {
// Prepend 'acb' to comply to best practice of prefixing realms with the name of their responsible module.
$final_realm = 'acb&' . implode('&', $realm);
// Finished combination; save it.
// Exclude empty access record grants as they are useless and only take up memory.
if ($access_records && ($view || $update || $delete)) {
// Node access record.
$output[$final_realm] = [
'realm' => $final_realm,
'gid' => 0,
'grant_view' => (int) $view,
'grant_update' => (int) $update,
'grant_delete' => (int) $delete,
'priority' => 100,
];
}
elseif (!$access_records) {
// User grant.
if (!empty($realm)) {
$output[$final_realm] = [
0,
];
}
}
}
return $output;
}
Functions
Name | Description |
---|---|
acb_get_modules | Returns a list of all active access control modules. |
acb_is_module_realm | Checks if a realm is originating from a specified module. |
acb_modules_installed | Implements hook_modules_installed(). |
acb_node_access_records_alter | Implements hook_node_access_records_alter(). |
acb_node_grants_alter | Implements hook_node_grants_alter(). |
_acb_cascade_grants | The beating heart of this module. Receives split-sorted grants and converts it into cascaded grants to be used by this module. Workhorse for both hook_node_grants_alter() and hook_node_access_records_alter(). |