og-8.x-1.x-dev/src/EventSubscriber/OgEventSubscriber.php
src/EventSubscriber/OgEventSubscriber.php
<?php
declare(strict_types=1);
namespace Drupal\og\EventSubscriber;
use Drupal\Core\Access\AccessResult;
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
use Drupal\Core\Entity\EntityTypeManagerInterface;
use Drupal\Core\Extension\ModuleHandlerInterface;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\og\Event\DefaultRoleEventInterface;
use Drupal\og\Event\GroupContentEntityOperationAccessEventInterface;
use Drupal\og\Event\OgAdminRoutesEventInterface;
use Drupal\og\Event\PermissionEventInterface;
use Drupal\og\GroupContentOperationPermission;
use Drupal\og\GroupPermission;
use Drupal\og\OgAccess;
use Drupal\og\OgAccessInterface;
use Drupal\og\OgRoleInterface;
use Drupal\og\PermissionManagerInterface;
use Drupal\user\EntityOwnerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
/**
* Event subscribers for Organic Groups.
*/
class OgEventSubscriber implements EventSubscriberInterface {
use StringTranslationTrait;
public function __construct(
protected readonly PermissionManagerInterface $permissionManager,
protected readonly EntityTypeManagerInterface $entityTypeManager,
protected readonly EntityTypeBundleInfoInterface $entityTypeBundleInfo,
protected readonly OgAccessInterface $ogAccess,
protected readonly ModuleHandlerInterface $moduleHandler,
) {}
/**
* {@inheritdoc}
*/
public static function getSubscribedEvents(): array {
return [
PermissionEventInterface::EVENT_NAME => [
// Provide a higher priority for the generic event subscriber so that it
// can run first and set default values for all supported entity types,
// which can then be overridden by other subscribers that set module
// specific permissions.
['provideDefaultOgPermissions', 10],
['provideDefaultNodePermissions'],
],
DefaultRoleEventInterface::EVENT_NAME => [['provideDefaultRoles']],
OgAdminRoutesEventInterface::EVENT_NAME => [['provideOgAdminRoutes']],
GroupContentEntityOperationAccessEventInterface::EVENT_NAME => [['checkGroupContentEntityOperationAccess']],
];
}
/**
* Provides default OG permissions.
*
* @param \Drupal\og\Event\PermissionEventInterface $event
* The OG permission event.
*/
public function provideDefaultOgPermissions(PermissionEventInterface $event) {
$event->setPermissions([
new GroupPermission([
'name' => OgAccess::ADMINISTER_GROUP_PERMISSION,
'title' => $this->t('Administer group'),
'description' => $this->t('Manage group members and content in the group.'),
'default roles' => [OgRoleInterface::ADMINISTRATOR],
'restrict access' => TRUE,
]),
new GroupPermission([
'name' => OgAccess::DELETE_GROUP_PERMISSION,
'title' => $this->t('Delete group'),
'description' => $this->t('Delete the group entity.'),
'default roles' => [OgRoleInterface::ADMINISTRATOR],
]),
new GroupPermission([
'name' => OgAccess::UPDATE_GROUP_PERMISSION,
'title' => $this->t('Edit group'),
'description' => $this->t('Edit the group entity.'),
'default roles' => [OgRoleInterface::ADMINISTRATOR],
]),
new GroupPermission([
'name' => 'subscribe',
'title' => $this->t('Subscribe to group'),
'description' => $this->t('Allow non-members to request membership to a group (approval required).'),
'roles' => [OgRoleInterface::ANONYMOUS],
'default roles' => [OgRoleInterface::ANONYMOUS],
]),
new GroupPermission([
'name' => 'subscribe without approval',
'title' => $this->t('Subscribe to group (no approval required)'),
'description' => $this->t('Allow non-members to join a group without an approval from group administrators.'),
'roles' => [OgRoleInterface::ANONYMOUS],
'default roles' => [],
]),
new GroupPermission([
'name' => 'approve and deny subscription',
'title' => $this->t('Approve and deny subscription'),
'description' => $this->t("Users may allow or deny another user's subscription request."),
'default roles' => [OgRoleInterface::ADMINISTRATOR],
]),
new GroupPermission([
'name' => 'add user',
'title' => $this->t('Add user'),
'description' => $this->t('Users may add other users to the group without approval.'),
'default roles' => [OgRoleInterface::ADMINISTRATOR],
]),
new GroupPermission([
'name' => 'manage members',
'title' => $this->t('Manage members'),
'description' => $this->t('Users may add and remove group members, and alter member status and roles.'),
'default roles' => [OgRoleInterface::ADMINISTRATOR],
'restrict access' => TRUE,
]),
new GroupPermission([
'name' => 'administer permissions',
'title' => $this->t('Administer permissions'),
'description' => $this->t('Users may view, create, edit and delete permissions and roles within the group.'),
'default roles' => [OgRoleInterface::ADMINISTRATOR],
'restrict access' => TRUE,
]),
]);
// Add a list of generic CRUD permissions for all group content.
$group_content_permissions = $this->getDefaultEntityOperationPermissions($event->getGroupContentBundleIds());
$event->setPermissions($group_content_permissions);
}
/**
* Provides default permissions for the Node entity.
*
* @param \Drupal\og\Event\PermissionEventInterface $event
* The OG permission event.
*/
public function provideDefaultNodePermissions(PermissionEventInterface $event) {
$bundle_ids = $event->getGroupContentBundleIds();
if (!array_key_exists('node', $bundle_ids)) {
return;
}
$permissions = [];
$bundle_info = $this->entityTypeBundleInfo->getBundleInfo('node');
foreach ($bundle_ids['node'] as $bundle_id) {
$args = ['%type_name' => $bundle_info[$bundle_id]['label']];
$permission_values = [
[
'name' => "create $bundle_id content",
'title' => $this->t('%type_name: Create new content', $args),
'operation' => 'create',
],
[
'name' => "edit own $bundle_id content",
'title' => $this->t('%type_name: Edit own content', $args),
'operation' => 'update',
'owner' => TRUE,
],
[
'name' => "edit any $bundle_id content",
'title' => $this->t('%type_name: Edit any content', $args),
'operation' => 'update',
'owner' => FALSE,
],
[
'name' => "delete own $bundle_id content",
'title' => $this->t('%type_name: Delete own content', $args),
'operation' => 'delete',
'owner' => TRUE,
],
[
'name' => "delete any $bundle_id content",
'title' => $this->t('%type_name: Delete any content', $args),
'operation' => 'delete',
'owner' => FALSE,
],
];
// Quick Node Clone module permissions.
if ($this->moduleHandler->moduleExists('quick_node_clone')) {
foreach ($bundle_ids['node'] as $bundle_id) {
$args = ['%type_name' => $bundle_info[$bundle_id]['label']];
$permission_values[] = [
'name' => "clone $bundle_id content",
'title' => $this->t('%type_name: Clone content', $args),
'operation' => 'clone',
'owner' => FALSE,
];
$permission_values[] = [
'name' => "clone own $bundle_id content",
'title' => $this->t('%type_name: Clone own content', $args),
'operation' => 'clone',
'owner' => TRUE,
];
}
}
foreach ($permission_values as $values) {
$values += [
'entity type' => 'node',
'bundle' => $bundle_id,
];
$permissions[] = new GroupContentOperationPermission($values);
}
}
$event->setPermissions($permissions);
}
/**
* Provides a default role for the group administrator.
*/
public function provideDefaultRoles(DefaultRoleEventInterface $event): void {
/** @var \Drupal\og\OgRoleInterface $role */
$role = $this->entityTypeManager->getStorage('og_role')->create([
'name' => OgRoleInterface::ADMINISTRATOR,
'label' => 'Administrator',
'is_admin' => TRUE,
]);
$event->addRole($role);
}
/**
* Returns a list of generic entity operation permissions for group content.
*
* This returns generic group content entity operation permissions for the
* operations 'create', 'update' and 'delete'.
*
* In Drupal the entity operation permissions are not following a machine
* writable naming scheme, but instead they use an arbitrary human readable
* format. For example the permission to update nodes of type article is 'edit
* own article content'. This does not even contain the operation 'update' or
* the entity type 'node'.
*
* OG needs to be able to provide basic CRUD permissions for its group content
* even if it cannot generate the proper human readable versions. This method
* settles for a generic permission format '{operation} {ownership} {bundle}
* {entity type}'. For example for editing articles this would become 'update
* own article node'.
*
* Modules can implement their own PermissionEvent to declare their proper
* permissions to use instead of the generic ones. For an example
* implementation, see `provideDefaultNodePermissions()`.
*
* @param array $group_content_bundle_ids
* An array of group content bundle IDs, keyed by group content entity type
* ID.
*
* @return \Drupal\og\GroupContentOperationPermission[]
* The array of permissions.
*
* @see \Drupal\og\EventSubscriber\OgEventSubscriber::provideDefaultNodePermissions()
*/
protected function getDefaultEntityOperationPermissions(array $group_content_bundle_ids) {
$permissions = [];
foreach ($group_content_bundle_ids as $group_content_entity_type_id => $bundle_ids) {
foreach ($bundle_ids as $bundle_id) {
$permissions = array_merge($permissions, $this->generateEntityOperationPermissionList($group_content_entity_type_id, $bundle_id));
}
}
return $permissions;
}
/**
* Helper method to generate entity operation permissions for a given bundle.
*
* @param string $group_content_entity_type_id
* The entity type ID for which to generate the permission list.
* @param string $group_content_bundle_id
* The bundle ID for which to generate the permission list.
*
* @return array
* An array of permission names and descriptions.
*/
protected function generateEntityOperationPermissionList($group_content_entity_type_id, $group_content_bundle_id) {
$permissions = [];
$entity_info = $this->entityTypeManager->getDefinition($group_content_entity_type_id);
$bundle_info = $this->entityTypeBundleInfo->getBundleInfo($group_content_entity_type_id)[$group_content_bundle_id];
// Build standard list of permissions for this bundle.
$args = [
'%bundle' => $bundle_info['label'],
'@entity' => $entity_info->getPluralLabel(),
];
// @todo This needs to support all entity operations for the given entity
// type, not just the standard CRUD operations.
// @see https://github.com/amitaibu/og/issues/222
$operations = [
[
'name' => "create $group_content_bundle_id $group_content_entity_type_id",
'title' => $this->t('Create %bundle @entity', $args),
'operation' => 'create',
],
[
'name' => "update own $group_content_bundle_id $group_content_entity_type_id",
'title' => $this->t('Edit own %bundle @entity', $args),
'operation' => 'update',
'owner' => TRUE,
],
[
'name' => "update any $group_content_bundle_id $group_content_entity_type_id",
'title' => $this->t('Edit any %bundle @entity', $args),
'operation' => 'update',
'owner' => FALSE,
],
[
'name' => "delete own $group_content_bundle_id $group_content_entity_type_id",
'title' => $this->t('Delete own %bundle @entity', $args),
'operation' => 'delete',
'owner' => TRUE,
],
[
'name' => "delete any $group_content_bundle_id $group_content_entity_type_id",
'title' => $this->t('Delete any %bundle @entity', $args),
'operation' => 'delete',
'owner' => FALSE,
],
];
// Add default permissions.
foreach ($operations as $values) {
$permission = new GroupContentOperationPermission($values);
$permission
->setEntityType($group_content_entity_type_id)
->setBundle($group_content_bundle_id)
->setDefaultRoles([OgRoleInterface::ADMINISTRATOR]);
$permissions[] = $permission;
}
return $permissions;
}
/**
* Provide OG admin routes.
*
* @param \Drupal\og\Event\OgAdminRoutesEventInterface $event
* The OG admin routes event object.
*/
public function provideOgAdminRoutes(OgAdminRoutesEventInterface $event) {
$routes_info = $event->getRoutesInfo();
$routes_info['members'] = [
'controller' => '\Drupal\og\Controller\OgAdminMembersController::membersList',
'title' => 'Members',
'description' => 'Manage members',
'path' => 'members',
'requirements' => [
'_og_user_access_group' => implode('|', [
OgAccess::ADMINISTER_GROUP_PERMISSION,
'manage members',
]),
// Views module must be enabled.
'_module_dependencies' => 'views',
],
];
$routes_info['add_membership_page'] = [
'controller' => '\Drupal\og\Controller\OgAdminMembersController::addPage',
'title' => 'Add a member',
'path' => 'members/add',
'requirements' => [
'_og_membership_add_access' => 'TRUE',
],
];
$event->setRoutesInfo($routes_info);
}
/**
* Checks if a user has access to perform a group content entity operation.
*
* @param \Drupal\og\Event\GroupContentEntityOperationAccessEventInterface $event
* The event fired when a group content entity operation is performed.
*/
public function checkGroupContentEntityOperationAccess(GroupContentEntityOperationAccessEventInterface $event): void {
$group_content_entity = $event->getGroupContent();
$group_entity = $event->getGroup();
$user = $event->getUser();
$operation = $event->getOperation();
// Users who bypass node access can always assign node-based groups.
if ($group_entity->getEntityTypeId() === 'node' && $user->hasPermission('bypass node access')) {
$event->mergeAccessResult(AccessResult::allowed()->cachePerPermissions());
return;
}
// Check if the user owns the entity which is being operated on.
$is_owner = $group_content_entity instanceof EntityOwnerInterface && $group_content_entity->getOwnerId() == $user->id();
// Retrieve the group content entity operation permissions.
$group_entity_type_id = $group_entity->getEntityTypeId();
$group_bundle_id = $group_entity->bundle();
$group_content_bundle_ids = [$group_content_entity->getEntityTypeId() => [$group_content_entity->bundle()]];
$permissions = $this->permissionManager->getDefaultEntityOperationPermissions($group_entity_type_id, $group_bundle_id, $group_content_bundle_ids);
// Filter the permissions by operation and ownership.
// If the user does not own the group content, only the non-owner permission
// is relevant (for example 'edit any article node'). However when the user
// _is_ the owner, then both permissions are relevant: an owner will have
// access if they either have the 'edit any article node' or the 'edit own
// article node' permission.
$ownerships = $is_owner ? [FALSE, TRUE] : [FALSE];
$permissions = array_filter($permissions, function (GroupContentOperationPermission $permission) use ($operation, $ownerships) {
return $permission->getOperation() === $operation && in_array($permission->getOwner(), $ownerships);
});
if ($permissions) {
foreach ($permissions as $permission) {
$event->mergeAccessResult($this->ogAccess->userAccess($group_entity, $permission->getName(), $user));
}
}
}
}
