Skip to content
This repository has been archived by the owner on Aug 18, 2024. It is now read-only.

Group content entity operations ~~hook~~ event #684

Merged
merged 11 commits into from
Aug 12, 2020
59 changes: 59 additions & 0 deletions og.api.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

/**
* @file
* Hooks provided by the Organic Groups module.
*/

declare(strict_types = 1);

use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Entity\EntityPublishedInterface;
use Drupal\og\OgAccess;

/**
* @addtogroup hooks
* @{
*/

/**
* Allows modules to alter group level permissions.
*
* @param array $permissions
* The list of group level permissions, passed by reference.
* @param \Drupal\Core\Cache\CacheableMetadata $cacheable_metadata
* The cache metadata.
* @param array $context
* An associative array containing contextual information, with keys:
* - 'permission': The group level permission being checked, as a string.
* - 'group': The group entity on which the permission applies.
* - 'user': The user account for which access is being determined.
*/
function hook_og_user_access_alter(array &$permissions, CacheableMetadata $cacheable_metadata, array $context): void {
// This example implements a use case where a custom module allows site
// builders to toggle a configuration setting that will prevent groups to be
// deleted if they are published.
// Retrieve the module configuration.
$config = \Drupal::config('mymodule.settings');

// Check if the site is configured to allow deletion of published groups.
$published_groups_can_be_deleted = $config->get('delete_published_groups');

// If deletion is not allowed and the group is published, revoke the
// permission.
$group = $context['group'];
if ($group instanceof EntityPublishedInterface && !$group->isPublished() && !$published_groups_can_be_deleted) {
$key = array_search(OgAccess::DELETE_GROUP_PERMISSION, $permissions);
if ($key !== FALSE) {
unset($permissions[$key]);
}
}

// Since our access result depends on our custom module configuration, we need
// to add it to the cache metadata.
$cacheable_metadata->addCacheableDependency($config);
}

/**
* @} End of "addtogroup hooks".
*/
4 changes: 2 additions & 2 deletions og.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ services:
- { name: 'cache.context'}
og.access:
class: Drupal\og\OgAccess
arguments: ['@config.factory', '@current_user', '@module_handler', '@og.group_type_manager', '@og.permission_manager', '@og.membership_manager']
arguments: ['@config.factory', '@current_user', '@module_handler', '@og.group_type_manager', '@og.permission_manager', '@og.membership_manager', '@event_dispatcher']
og.context:
class: Drupal\og\ContextProvider\OgContext
arguments: ['@plugin.manager.og.group_resolver', '@config.factory']
tags:
- { name: 'context_provider' }
og.event_subscriber:
class: Drupal\og\EventSubscriber\OgEventSubscriber
arguments: ['@og.permission_manager', '@entity_type.manager', '@entity_type.bundle.info']
arguments: ['@og.permission_manager', '@entity_type.manager', '@entity_type.bundle.info', '@og.access']
tags:
- { name: 'event_subscriber' }
og.group_audience_helper:
Expand Down
98 changes: 98 additions & 0 deletions src/Event/AccessEventBase.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php

declare(strict_types = 1);

namespace Drupal\og\Event;

use Drupal\Core\Access\AccessResult;
use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyTrait;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Session\AccountInterface;
use Symfony\Component\EventDispatcher\Event;

/**
* Base class for OG access events.
*/
class AccessEventBase extends Event implements AccessEventInterface {

use RefinableCacheableDependencyTrait;

/**
* The access result.
*
* @var \Drupal\Core\Access\AccessResultInterface
*/
protected $access;

/**
* The group that provides the context for the access check.
*
* @var \Drupal\Core\Entity\ContentEntityInterface
*/
protected $group;

/**
* The user for which to check access.
*
* @var \Drupal\Core\Session\AccountInterface
*/
protected $user;

/**
* Constructs an AccessEventBase event.
*
* @param \Drupal\Core\Entity\ContentEntityInterface $group
* The group that provides the context in which to perform the access check.
* @param \Drupal\Core\Session\AccountInterface $user
* The user for which to check access.
*/
public function __construct(ContentEntityInterface $group, AccountInterface $user) {
$this->group = $group;
$this->user = $user;
$this->access = AccessResult::neutral();
}

/**
* {@inheritdoc}
*/
public function grantAccess(): void {
$this->access = $this->access->orIf(AccessResult::allowed());
}

/**
* {@inheritdoc}
*/
public function denyAccess(): void {
$this->access = $this->access->orIf(AccessResult::forbidden());
}

/**
* {@inheritdoc}
*/
public function getGroup(): ContentEntityInterface {
return $this->group;
}

/**
* {@inheritdoc}
*/
public function getUser(): AccountInterface {
return $this->user;
}

/**
* {@inheritdoc}
*/
public function getAccessResult(): AccessResultInterface {
$access = $this->access;

if ($access instanceof RefinableCacheableDependencyInterface) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe worth adding a comment - I'm not clear on this part?

Copy link
Contributor Author

@pfrenssen pfrenssen Aug 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ensures that our code adheres correctly to our interface and avoids a potential fatal error.

We are returning an AccessResultInterface object, and this doesn't offer ::addCacheableDependency() so we are not supposed to call this method. But in reality all access objects in Drupal core are extending AccessResult, which does have this method because it also implements RefinableCacheableDependencyInterface. So in typical Drupal code cache metadata is supported and we can call this method. Drupal is full of these kind of discrepancies, and often developers ignore these cases where a method is part of a different interface.

It is not reliable though. It is always possible someone will implement their own AccessResultInterface object which does not implement RefinableCacheableDependencyInterface and then we get a fatal error. Just putting this simple if statement here makes sure we are never going to throw a fatal error if somebody is using a custom implementation.

We in fact are offering extra functionality (cache metadata support) in case the object we are dealing with supports it. This is commonly called "type widening" in PHP. Modern IDE's like PHPStorm have become very good at pointing out these kind of errors.

I created a PR to document this section: #697

$access->addCacheableDependency($this);
}

return $access;
}

}
58 changes: 58 additions & 0 deletions src/Event/AccessEventInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<?php

declare(strict_types = 1);

namespace Drupal\og\Event;

use Drupal\Core\Access\AccessResultInterface;
use Drupal\Core\Cache\RefinableCacheableDependencyInterface;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Session\AccountInterface;

/**
* Interface for events that determine access in Organic Groups.
*/
interface AccessEventInterface extends RefinableCacheableDependencyInterface {

/**
* Declare that access is being granted.
*
* Calling this method will cause access to be granted for the action that is
* being checked, unless another event listener denies access.
*/
public function grantAccess(): void;

/**
* Declare that access is being denied.
*
* Calling this method will cause access to be denied for the action that is
* being checked. This takes precedence over any other event listeners that
* might grant access.
*/
public function denyAccess(): void;

/**
* Returns the group that provides the context for the access check.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* The group entity.
*/
public function getGroup(): ContentEntityInterface;

/**
* Returns the user for which access is being determined.
*
* @return \Drupal\Core\Session\AccountInterface
* The user.
*/
public function getUser(): AccountInterface;

/**
* Returns the current access result object.
*
* @return \Drupal\Core\Access\AccessResultInterface
* The access result object.
*/
public function getAccessResult(): AccessResultInterface;

}
61 changes: 61 additions & 0 deletions src/Event/GroupContentEntityOperationAccessEvent.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
<?php

declare(strict_types = 1);

namespace Drupal\og\Event;

use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Session\AccountInterface;

/**
* Event that determines access to group content entity operations.
*/
class GroupContentEntityOperationAccessEvent extends AccessEventBase implements GroupContentEntityOperationAccessEventInterface {

/**
* The entity operation being performed.
*
* @var string
*/
protected $operation;

/**
* The group content entity upon which the operation is being performed.
*
* @var \Drupal\Core\Entity\ContentEntityInterface
*/
protected $groupContent;

/**
* Constructs a GroupContentEntityOperationAccessEvent.
*
* @param string $operation
* The entity operation, such as "create", "update" or "delete".
* @param \Drupal\Core\Entity\ContentEntityInterface $group
* The group in scope of which the access check is being performed.
* @param \Drupal\Core\Entity\ContentEntityInterface $groupContent
* The group content upon which the entity operation is performed.
* @param \Drupal\Core\Session\AccountInterface $user
* The user for which to check access.
*/
public function __construct(string $operation, ContentEntityInterface $group, ContentEntityInterface $groupContent, AccountInterface $user) {
parent::__construct($group, $user);
$this->operation = $operation;
$this->groupContent = $groupContent;
}

/**
* {@inheritdoc}
*/
public function getOperation(): string {
return $this->operation;
}

/**
* {@inheritdoc}
*/
public function getGroupContent(): ContentEntityInterface {
return $this->groupContent;
}

}
35 changes: 35 additions & 0 deletions src/Event/GroupContentEntityOperationAccessEventInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types = 1);

namespace Drupal\og\Event;

use Drupal\Core\Entity\ContentEntityInterface;

/**
* Interface for events that provide access to group content entity operations.
*/
interface GroupContentEntityOperationAccessEventInterface extends AccessEventInterface {

/**
* The event name.
*/
const EVENT_NAME = 'og.group_content_entity_operation_access';

/**
* Returns the entity operation being performed.
*
* @return string
* The entity operation, such as 'create', 'update' or 'delete'.
*/
public function getOperation(): string;

/**
* Returns the group content entity upon which the operation is performed.
*
* @return \Drupal\Core\Entity\ContentEntityInterface
* The group content entity.
*/
public function getGroupContent(): ContentEntityInterface;

}
Loading