diff --git a/.travis.yml b/.travis.yml index ea2e4b03b..81c01c9c7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,11 @@ sudo: false php: - 5.5 - 5.6 + - 7.0 + +env: + - DRUPAL_CORE=8.1.x + - DRUPAL_CORE=8.2.x mysql: database: og @@ -11,23 +16,42 @@ mysql: encoding: utf8 before_script: - # Remember the current rules test directory for later use in the Drupal - # installation. + # Remove Xdebug as we don't need it and it causes "PHP Fatal error: Maximum + # function nesting level of '256' reached." + # We also don't care if that file exists or not on PHP 7. + - phpenv config-rm xdebug.ini || true + + # Remember the current directory for later use in the Drupal installation. - TESTDIR=$(pwd) + # Navigate out of module directory to prevent blown stack by recursive module # lookup. - cd .. # Create database. - mysql -e 'create database og' + + # Export database variable for kernel tests. + - export SIMPLETEST_DB=mysql://root:@127.0.0.1/og + # Download Drupal 8 core. - - git clone --branch 8.0.x --depth 1 http://git.drupal.org/project/drupal.git + - travis_retry git clone --branch $DRUPAL_CORE --depth 1 https://git.drupal.org/project/drupal.git - cd drupal + # Install Composer dependencies. + - composer self-update && composer install + + # Reference OG in the Drupal site. - ln -s $TESTDIR modules/og - # Adding DB so PHPUnit could mock the environment. - - export SIMPLETEST_DB=mysql://root:@127.0.0.1/og; + # Start a web server on port 8888 in the background. + - nohup php -S localhost:8888 > /dev/null 2>&1 & + + # Wait until the web server is responding. + - until curl -s localhost:8888; do true; done > /dev/null + + # Export web server URL for browser tests. + - export SIMPLETEST_BASE_URL=http://localhost:8888 script: # Run the PHPUnit tests which also include the kernel tests. diff --git a/README.md b/README.md index 17dad2105..b1e3a616a 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,48 @@ As is the case with Drupal itself, in Organic Groups different permissions can be assigned to different user roles. This allows group members to perform a different set of actions, in different group contexts. +### OG Membership Entity + +The membership entity that connects a group and a user. + +When dealing with non-user entities that are group content, that is content +that is associated with a group, we do it via an entity reference field that +has the default storage. The only information that we hold is that a group +content is referencing a group. + +However, when dealing with the user entity we recognize that we need to +special case it. It won't suffice to just hold the reference between the user +and the group content as it will be laking crucial information such as: the +state of the user's membership in the group (active, pending or blocked), the +time the membership was created, the user's OG role in the group, etc. + +For this meta data we have the fieldable OgMembership entity, that is always +connecting between a user and a group. There cannot be an OgMembership entity +connecting two non-user entities. + +Creating such a relation is done for example in the following way: + +```php + $membership = OgMembership::create(['type' => \Drupal\og\OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser(2) + ->setEntityId(1) + ->setGroupEntityType('node') + ->setFieldName(OgGroupAudienceHelper::DEFAULT_FIELD) + ->save(); +``` + +Notice how the relation of the user to the group also includes the OG +audience field name this association was done by. Like this we are able to +express different membership types such as the default membership that comes +out of the box, or a "premium membership" that can be for example expired +after a certain amount of time (the logic for the expired membership in the +example is out of the scope of OG core). + +Having this field separation is what allows having multiple OG audience +fields attached to the user, where each group they are associated with may be +a result of different membership types. + ## INSTALLATION DRUPAL 8.x Note that the following guide is here to get you started. Names for content types, groups and group content given here are suggestions and are given to diff --git a/composer.json b/composer.json new file mode 100644 index 000000000..a6359acfa --- /dev/null +++ b/composer.json @@ -0,0 +1,13 @@ +{ + "name": "drupal/og", + "description": "API to allow associating content with groups.", + "type": "drupal-module", + "license": "GPL-2.0+", + "homepage": "https://drupal.org/project/og", + "support": { + "issues": "https://drupal.org/project/issues/og", + "irc": "irc://irc.freenode.org/drupal-og", + "source": "https://cgit.drupalcode.org/og" + }, + "minimum-stability": "dev" +} diff --git a/config/install/og.settings.yml b/config/install/og.settings.yml index 534051c13..e8f0ae2b2 100644 --- a/config/install/og.settings.yml +++ b/config/install/og.settings.yml @@ -1,5 +1,5 @@ group_manager_full_access: true groups: [] node_access_strict: true -orphans_delete: false -use_queue: false +delete_orphans: false +delete_orphans_plugin_id: simple diff --git a/config/schema/og.schema.yml b/config/schema/og.schema.yml index e8caaff7e..b22ad62c5 100644 --- a/config/schema/og.schema.yml +++ b/config/schema/og.schema.yml @@ -20,6 +20,28 @@ field.field_settings.og_membership_reference: type: boolean label: 'Access Override' +field.storage_settings.og_standard_reference: + type: mapping + label: 'Organic Groups reference field storage settings' + mapping: + target_type: + type: string + label: 'Type of entity to reference' + +field.field_settings.og_standard_reference: + type: mapping + label: 'Organic Groups reference field settings' + mapping: + handler: + type: string + label: 'Reference method' + handler_settings: + type: entity_reference_selection.[%parent.handler] + label: 'Organic Groups reference selection plugin settings' + access_override: + type: boolean + label: 'Access Override' + og.settings: type: config_object label: 'Organic Groups settings' @@ -34,12 +56,12 @@ og.settings: node_access_strict: type: boolean label: 'Strict node access permissions' - orphans_delete: + delete_orphans: type: boolean label: 'Delete orphaned group content when a group is deleted' - use_queue: - type: boolean - label: 'Use queue' + delete_orphans_plugin_id: + type: string + label: 'The method to use when deleting orphaned group content' og.settings.group.*: type: sequence @@ -83,14 +105,34 @@ og.og_role.*: type: string label: 'Group ID' group_type: - type: label + type: string label: 'Group type' group_bundle: - type: label + type: string label: 'Group bundle' + is_admin: + type: boolean + label: 'User is group admin' permissions: type: sequence label: 'Permissions' sequence: type: string label: 'Permission' + role_type: + type: string + label: 'Role type' + +field.widget.settings.og_complex: + type: mapping + label: 'OG Group Audience field widget' + mapping: + match_operator: + type: string + label: 'Autocomplete matching' + size: + type: integer + label: 'Size of textfield' + placeholder: + type: label + label: 'Placeholder' diff --git a/og.install b/og.install index 55c496769..0365819a8 100644 --- a/og.install +++ b/og.install @@ -5,94 +5,20 @@ * Install, update, and uninstall functions for the Organic groups module. */ +/** + * Implements hook_uninstall(). + */ +function og_uninstall() { + \Drupal::queue('og_orphaned_group_content')->deleteQueue(); + \Drupal::queue('og_orphaned_group_content_cron')->deleteQueue(); +} + /** * Implements hook_schema(). */ function og_schema() { $schema = array(); - $schema['og_role_permission'] = array( - 'description' => 'Stores the permissions assigned to user roles per group.', - 'fields' => array( - 'id' => array( - 'type' => 'serial', - 'description' => "The role permission unique identifier.", - 'unsigned' => TRUE, - 'not null' => TRUE, - ), - 'rid' => array( - 'type' => 'int', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'Foreign Key: {role}.rid.', - ), - 'permission' => array( - 'type' => 'varchar', - 'length' => 128, - 'not null' => TRUE, - 'default' => '', - 'description' => 'A single permission granted to the role identified by rid.', - ), - 'module' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => "The module declaring the permission.", - ), - ), - 'primary key' => array('id'), - 'indexes' => array( - 'permission' => array('permission'), - ), - 'foreign keys' => array( - 'og_role' => array( - 'table' => 'og_role', - 'columns' => array('rid' => 'rid'), - ), - ), - ); - - $schema['og_role'] = array( - 'description' => 'Stores user roles per group.', - 'fields' => array( - 'rid' => array( - 'type' => 'serial', - 'unsigned' => TRUE, - 'not null' => TRUE, - 'description' => 'Primary Key: Unique role ID.', - ), - 'gid' => array( - 'description' => "The group's unique ID.", - 'type' => 'int', - 'size' => 'normal', - 'not null' => TRUE, - ), - 'group_type' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => "The group's entity type.", - ), - 'group_bundle' => array( - 'type' => 'varchar', - 'length' => 255, - 'not null' => TRUE, - 'default' => '', - 'description' => "The group's bundle name.", - ), - 'name' => array( - 'type' => 'varchar', - 'length' => 64, - 'not null' => TRUE, - 'default' => '', - 'description' => 'Unique role name per group.', - ), - ), - 'primary key' => array('rid'), - ); - $schema['og_users_roles'] = array( 'description' => 'Maps users to roles.', 'fields' => array( diff --git a/og.module b/og.module index 8a8cfa72d..e38d8ea0d 100755 --- a/og.module +++ b/og.module @@ -7,15 +7,16 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Session\AccountInterface; -use Drupal\og\Entity\OgRoleInterface; +use Drupal\og\Entity\OgMembership; use Drupal\og\Og; use Drupal\og\OgAccess; -use Drupal\og\Entity\OgMembership; +use Drupal\og\OgGroupAudienceHelper; use Drupal\og\OgMembershipInterface; +use Drupal\og\OgRoleInterface; use Drupal\user\EntityOwnerInterface; -use Drupal\Core\Field\FieldDefinitionInterface; -use Drupal\Core\Field\FieldItemListInterface; /** * Group default roles and permissions field. @@ -34,15 +35,39 @@ function og_entity_insert(EntityInterface $entity) { // Subscribe the group manager. $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); $membership - ->setMemberEntityId($entity->getOwnerId()) - ->setMemberEntityType('user') - ->setGroupEntityid($entity->id()) + ->setUser($entity->getOwnerId()) + ->setEntityId($entity->id()) ->setGroupEntityType($entity->getEntityTypeId()) ->setFieldName($membership->getFieldName()) ->save(); } } +/** + * Implements hook_entity_predelete(). + */ +function og_entity_predelete(EntityInterface $entity) { + if (Og::isGroup($entity->getEntityTypeId(), $entity->bundle())) { + // Register orphaned group content for deletion, if this option has been + // enabled. + $config = \Drupal::config('og.settings'); + if ($config->get('delete_orphans')) { + $plugin_id = $config->get('delete_orphans_plugin_id'); + /** @var \Drupal\og\OgDeleteOrphansInterface $plugin */ + $plugin = \Drupal::service('plugin.manager.og.delete_orphans')->createInstance($plugin_id, []); + $plugin->register($entity); + } + + // @todo Delete user roles. + // @see https://github.com/amitaibu/og/issues/175 + // og_delete_user_roles_by_group($entity_type, $entity); + } + if ($entity instanceof \Drupal\user\UserInterface) { + // @todo Delete memberships when deleting users. + // @see https://github.com/amitaibu/og/issues/176 + } +} + /** * Implements hook_entity_field_access(). * @@ -64,7 +89,7 @@ function og_entity_field_access($operation, FieldDefinitionInterface $field_defi return AccessResult::neutral(); } - if (!Og::isGroupAudienceField($field_definition)) { + if (!OgGroupAudienceHelper::isGroupAudienceField($field_definition)) { return AccessResult::neutral(); } @@ -79,7 +104,7 @@ function og_entity_field_access($operation, FieldDefinitionInterface $field_defi * Implements hook_entity_access(). */ function og_entity_access(EntityInterface $entity, $operation, AccountInterface $account) { - // We only care about content entities. + // We only care about content entities that are groups or group content. if (!$entity instanceof ContentEntityInterface) { return AccessResult::neutral(); } @@ -91,21 +116,18 @@ function og_entity_access(EntityInterface $entity, $operation, AccountInterface $entity_type_id = $entity->getEntityTypeId(); $bundle_id = $entity->bundle(); - $access = OgAccess::userAccessEntity('administer group', $entity, $account); - - if ($access->isNeutral()) { - // The node isn't in an OG context, so no need to keep testing. - return $access; - } - else { - // Any and own content. - $access = $access->orIf(OgAccess::userAccessEntity($operation, $entity, $account)); + if (!Og::isGroup($entity_type_id, $bundle_id) && !Og::isGroupContent($entity_type_id, $bundle_id)) { + return AccessResult::neutral(); } - if (!$access->isAllowed() && ($operation === 'update') && Og::isGroup($entity_type_id, $bundle_id)) { - $access = OgAccess::userAccessEntity($operation, $entity, $account); + // If the user has permission to administer all groups, allow access. + if ($account->hasPermission('administer group')) { + return AccessResult::allowed(); } + /** @var \Drupal\Core\Access\AccessResult $access */ + $access = \Drupal::service('og.access')->userAccessEntity($operation, $entity, $account); + if ($access->isAllowed()) { return $access; } @@ -124,11 +146,6 @@ function og_entity_access(EntityInterface $entity, $operation, AccountInterface * Implements hook_entity_create_access(). */ function og_entity_create_access(AccountInterface $account, array $context, $bundle) { - // @todo: Remove when https://www.drupal.org/node/2627852 lands. - if (empty($context['entity_type_id'])) { - return AccessResult::neutral(); - } - $entity_type_id = $context['entity_type_id']; if (!Og::isGroupContent($entity_type_id, $bundle)) { @@ -150,26 +167,25 @@ function og_entity_create_access(AccountInterface $account, array $context, $bun } } - - - // We can't check if user has create permissions, as there is no group // context. However, we can check if there are any groups the user will be - // able to select, and if not, we don't allow access. + // able to select, and if not, we don't allow access but if there are, + // AccessResult::neutral() will be returned in order to not override other + // access results. // @see \Drupal\og\Plugin\EntityReferenceSelection\OgSelection::buildEntityQuery() $required = FALSE; $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type_id, $bundle); foreach ($field_definitions as $field_name => $field_definition) { /** @var \Drupal\Core\Field\FieldDefinitionInterface $field_definition */ - if (!Og::isGroupAudienceField($field_definition)) { + if (!OgGroupAudienceHelper::isGroupAudienceField($field_definition)) { continue; } $handler = Og::getSelectionHandler($field_definition); if ($handler->getReferenceableEntities()) { - return Accessresult::allowed(); + return Accessresult::neutral(); } // Allow users to create content outside of groups, if none of the @@ -202,40 +218,6 @@ function _og_modules_uninstalled($modules) { og_permissions_delete_by_module($modules); } -/** - * Implements hook_og_permission(). - */ -function _og_og_permission() { - // Generate standard node permissions for all applicable node types. - $perms = array(); - - $perms['update group'] = array( - 'title' => t('Edit group'), - 'description' => t('Edit the group. Note: This permission controls only node entity type groups.'), - 'default role' => array(OgRoleInterface::ADMINISTRATOR), - ); - $perms['administer group'] = array( - 'title' => t('Administer group'), - 'description' => t('Manage group members and content in the group.'), - 'default role' => array(OgRoleInterface::ADMINISTRATOR), - 'restrict access' => TRUE, - ); - - foreach (node_permissions_get_configured_types() as $type) { - $perms = array_merge($perms, og_list_permissions($type)); - } - - return $perms; -} - - -/** - * Implements hook_og_default_roles(). - */ -function _og_og_default_roles() { - return array(OgRoleInterface::ADMINISTRATOR); -} - /** * Implements hook_field_create_instance(). * @@ -257,7 +239,7 @@ function _og_field_create_instance($instance) { // We add a different field, so each field can be set differently. $entity_type = $instance['entity_type']; $bundle = $instance['bundle']; - foreach (array_keys(Og::getAllGroupAudienceFields('user', 'user')) as $field_name) { + foreach (array_keys(OgGroupAudienceHelper::getAllGroupAudienceFields('user', 'user')) as $field_name) { $field = field_info_field($field_name); if ($field['settings']['target_type'] == $entity_type && empty($field['settings']['handler_settings']['target_bundles'])) { @@ -320,7 +302,7 @@ function _og_field_delete_instance($instance) { function _og_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) { list(,, $bundle) = entity_extract_ids($entity_type, $entity); - if (Og::getAllGroupAudienceFields($entity_type, $bundle)) { + if (OgGroupAudienceHelper::getAllGroupAudienceFields($entity_type, $bundle)) { $form['#validate'][] = 'og_form_group_reference_validate'; } @@ -368,7 +350,7 @@ function _og_form_group_reference_validate($form, &$form_state) { return; } - foreach (array_keys(Og::getAllGroupAudienceFields($entity_type, $bundle)) as $field_name) { + foreach (array_keys(OgGroupAudienceHelper::getAllGroupAudienceFields($entity_type, $bundle)) as $field_name) { // If there is at least one group selected, return. if (!empty($form_state['values'][$field_name][LANGUAGE_NONE])) { return; @@ -379,6 +361,25 @@ function _og_form_group_reference_validate($form, &$form_state) { form_set_error('og', t('You must select one or more groups for this content.')); } + +/** + * Implements hook_field_formatter_info_alter() + * + * Allow OG audience fields to have entity reference formatters. + */ +function og_field_formatter_info_alter(array &$info) { + foreach (array_keys($info) as $key) { + if (!in_array('entity_reference', $info[$key]['field_types'])) { + // Not an entity reference formatter. + continue; + } + + $info[$key]['field_types'][] = OgGroupAudienceHelper::USER_TO_GROUP_REFERENCE_FIELD_TYPE; + $info[$key]['field_types'][] = OgGroupAudienceHelper::NON_USER_TO_GROUP_REFERENCE_FIELD_TYPE; + } +} + + /** * Validate handler; Make sure a group can be created. * @@ -509,7 +510,7 @@ function __og_update_entity_fields($entity_type, $entity) { } $wrapper = entity_metadata_wrapper($entity_type, $entity); - foreach (Og::getAllGroupAudienceFields($entity_type, $bundle) as $field_name => $label) { + foreach (OgGroupAudienceHelper::getAllGroupAudienceFields($entity_type, $bundle) as $field_name => $label) { $field = field_info_field($field_name); $gids = array(); if ($field['cardinality'] == 1) { @@ -529,24 +530,6 @@ function __og_update_entity_fields($entity_type, $entity) { } } -/** - * Implements hook_entity_delete(). - */ -function _og_entity_delete($entity, $entity_type) { - list($id, , $bundle) = entity_extract_ids($entity_type, $entity); - if (og_is_group($entity_type, $entity)) { - og_delete_user_roles_by_group($entity_type, $entity); - og_membership_delete_by_group($entity_type, $entity); - } - if (og_is_group_content_type($entity_type, $bundle)) { - // As the field attachers are called after hook_entity_presave() we - // can't delete the OG memberships here. So we just mark the entity - // as being deleted, and we will do the actual delete in - // OgBehaviorHandler::delete(). - $entity->delete_og_membership = TRUE; - } -} - /** * Implements hook_og_membership_insert(). */ @@ -1169,57 +1152,6 @@ function __og_orphans_move($ids, $group_type, $gid) { } } -/** - * Register memberships for deletion. - * - * if the property "skip_og_membership_delete_by_group" exists on the - * entity, this function _will return early, and allow other implementing - * modules to deal with the deletion logic. - * - * @param $entity_type - * The group type. - * @param $entity - * The group entity object. - */ -function _og_membership_delete_by_group($entity_type, $entity) { - if (!empty($entity->skip_og_membership_delete_by_group)) { - return; - } - - list($gid) = entity_extract_ids($entity_type, $entity); - $query = new EntityFieldQuery(); - $result = $query - ->entityCondition('entity_type', 'og_membership') - ->propertyCondition('group_type', $entity_type, '=') - ->propertyCondition('gid', $gid, '=') - ->execute(); - - if (empty($result['og_membership'])) { - return; - } - - if (\Drupal::config('og.settings')->get('use_queue')) { - $queue = DrupalQueue::get('og_membership_orphans'); - // Add item to the queue. - $data = array( - 'group_type' => $entity_type, - 'gid' => $gid, - // Allow implementing modules to determine the disposition (e.g. delete - // orphan group content). - 'orphans' => array( - 'delete' => isset($entity->og_orphans['delete']) ? $entity->og_orphans['delete'] : \Drupal::config('og.settings')->get('orphans_delete'), - 'move' => isset($entity->og_orphans['move']) ? $entity->og_orphans['move'] : array(), - ), - ); - - // Exit now, as the task will be processed via queue. - return $queue->createItem($data); - } - - // No scalable solution was chosen, so just delete OG memberships. - og_membership_delete_multiple(array_keys($result['og_membership'])); -} - /** * Label callback; Return the label of OG membership entity. */ @@ -1692,7 +1624,7 @@ function _og_get_group_type($entity_type, $bundle_name, $type = 'group') { return (bool)field_info_instance($entity_type, OG_GROUP_FIELD, $bundle_name); } elseif ($type == 'group content') { - return (bool)Og::getAllGroupAudienceFields($entity_type, $bundle_name); + return (bool)OgGroupAudienceHelper::getAllGroupAudienceFields($entity_type, $bundle_name); } } @@ -2613,7 +2545,7 @@ function _og_get_groups_by_user($account = NULL, $group_type = NULL) { $account = $user; } - if (!Og::getAllGroupAudienceFields('user', 'user')) { + if (!OgGroupAudienceHelper::getAllGroupAudienceFields('user', 'user')) { // User entity doesn't have group audience fields. return; } diff --git a/og.services.yml b/og.services.yml index 6a6360d00..92db7870a 100644 --- a/og.services.yml +++ b/og.services.yml @@ -1,10 +1,24 @@ services: - plugin.manager.og.fields: - class: Drupal\og\OgFieldsPluginManager - parent: default_plugin_manager + og.access: + class: Drupal\og\OgAccess + arguments: ['@config.factory', '@current_user', '@module_handler'] + og.event_subscriber: + class: Drupal\og\EventSubscriber\OgEventSubscriber + arguments: ['@og.permission_manager'] + tags: + - { name: 'event_subscriber' } og.group.manager: class: Drupal\og\GroupManager - arguments: ['@config.factory'] + arguments: ['@config.factory', '@entity_type.manager', '@entity_type.bundle.info', '@event_dispatcher', '@state'] og.permissions: class: Drupal\og\OgPermissionHandler arguments: ['@module_handler', '@string_translation', '@controller_resolver'] + og.permission_manager: + class: Drupal\og\PermissionManager + arguments: ['@og.group.manager', '@entity_type.manager', '@entity_type.bundle.info'] + plugin.manager.og.delete_orphans: + class: Drupal\og\OgDeleteOrphansPluginManager + parent: default_plugin_manager + plugin.manager.og.fields: + class: Drupal\og\OgFieldsPluginManager + parent: default_plugin_manager diff --git a/og.views.inc b/og.views.inc new file mode 100644 index 000000000..6a5cc4dab --- /dev/null +++ b/og.views.inc @@ -0,0 +1,95 @@ + 'uid', + 'base' => 'og_membership', + 'base field' => 'uid', + 'label' => t('OG Membership'), + 'title' => t('OG Membership'), + 'id' => 'standard', + ]; +} + +/** + * Implements hook_field_views_data(). + * + * This is an almost verbatim copy of core_field_views_data() except for the + * field type check. + */ +function og_field_views_data(FieldStorageConfigInterface $field_storage) { + $data = views_field_default_views_data($field_storage); + + // This is the same as entity reference integration as the OG standard + // reference item is no different really. + switch ($field_storage->getType()) { + case 'og_standard_reference': + $entity_manager = \Drupal::entityManager(); + $entity_type_id = $field_storage->getTargetEntityTypeId(); + /** @var \Drupal\Core\Entity\Sql\DefaultTableMapping $table_mapping */ + $table_mapping = $entity_manager->getStorage($entity_type_id)->getTableMapping(); + + foreach ($data as $table_name => $table_data) { + // Add a relationship to the target entity type. + $target_entity_type_id = $field_storage->getSetting('target_type'); + $target_entity_type = $entity_manager->getDefinition($target_entity_type_id); + $entity_type_id = $field_storage->getTargetEntityTypeId(); + $entity_type = $entity_manager->getDefinition($entity_type_id); + $target_base_table = $target_entity_type->getDataTable() ?: $target_entity_type->getBaseTable(); + $field_name = $field_storage->getName(); + + // Provide a relationship for the entity type with the entity reference + // field. + $args = [ + '@label' => $target_entity_type->getLabel(), + '@field_name' => $field_name, + ]; + $data[$table_name][$field_name]['relationship'] = array( + 'title' => t('@label referenced from @field_name', $args), + 'label' => t('@field_name: @label', $args), + 'group' => $entity_type->getLabel(), + 'help' => t('Appears in: @bundles.', ['@bundles' => implode(', ', $field_storage->getBundles())]), + 'id' => 'standard', + 'base' => $target_base_table, + 'entity type' => $target_entity_type_id, + 'base field' => $target_entity_type->getKey('id'), + 'relationship field' => $field_name . '_target_id', + ); + + // Provide a reverse relationship for the entity type that is referenced by + // the field. + $args['@entity'] = $entity_type->getLabel(); + $args['@label'] = $target_entity_type->getLowercaseLabel(); + $pseudo_field_name = 'reverse__' . $entity_type_id . '__' . $field_name; + $data[$target_base_table][$pseudo_field_name]['relationship'] = [ + 'title' => t('@entity using @field_name', $args), + 'label' => t('@field_name', ['@field_name' => $field_name]), + 'group' => $target_entity_type->getLabel(), + 'help' => t('Relate each @entity with a @field_name set to the @label.', $args), + 'id' => 'entity_reverse', + 'base' => $entity_type->getDataTable() ?: $entity_type->getBaseTable(), + 'entity_type' => $entity_type_id, + 'base field' => $entity_type->getKey('id'), + 'field_name' => $field_name, + 'field table' => $table_mapping->getDedicatedDataTableName($field_storage), + 'field field' => $field_name . '_target_id', + 'join_extra' => [ + [ + 'field' => 'deleted', + 'value' => 0, + 'numeric' => TRUE, + ], + ], + ]; + } + break; + } + + return $data; +} diff --git a/og_ui/css/form.css b/og_ui/css/form.css index 5493be753..65a632337 100644 --- a/og_ui/css/form.css +++ b/og_ui/css/form.css @@ -1,3 +1,4 @@ -form .child-item { +form fieldset.child-item, +.form-item-og-orphans-delete-method .description { margin-left: 1.5em; /* LTR */ } diff --git a/og_ui/og_ui.links.menu.yml b/og_ui/og_ui.links.menu.yml index e315cbf0b..41b603b64 100644 --- a/og_ui/og_ui.links.menu.yml +++ b/og_ui/og_ui.links.menu.yml @@ -11,3 +11,19 @@ og_ui.settings: parent: og_ui.admin_index description: 'Administer OG settings.' route_name: og_ui.settings + +og_ui.roles_overview: + title: 'OG roles' + parent: og_ui.admin_index + description: 'Administer OG roles.' + route_name: og_ui.roles_permissions_overview + route_parameters: + type: 'roles' + +og_ui.permissions_overview: + title: 'OG permissions' + parent: og_ui.admin_index + description: 'Administer OG permissions.' + route_name: og_ui.roles_permissions_overview + route_parameters: + type: 'permissions' diff --git a/og_ui/og_ui.module b/og_ui/og_ui.module index 711f96520..056e4e51b 100644 --- a/og_ui/og_ui.module +++ b/og_ui/og_ui.module @@ -9,9 +9,11 @@ use Drupal\Core\Entity\BundleEntityFormBase; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\field\Entity\FieldConfig; +use Drupal\field\Entity\FieldStorageConfig; use Drupal\og\Og; use Drupal\og\OgGroupAudienceHelper; use Drupal\og_ui\BundleFormAlter; +use Drupal\og\OgRoleInterface; /** * Implements hook_form_alter(). @@ -75,18 +77,20 @@ function og_ui_entity_type_save(EntityInterface $entity) { } // Change the field target type and bundle. + if ($field_storage = FieldStorageConfig::loadByName($entity_type_id, OgGroupAudienceHelper::DEFAULT_FIELD)) { + $target_type = $field_storage->getSetting('target_type'); + if (!empty($entity->og_target_type) && $entity->og_target_type !== $target_type) { + // @todo It's probably not possible to change the field storage after the + // field has data. We should disable this option in the UI. + $field_storage->setSetting('target_type', $entity->og_target_type); + $field_storage->save(); + } + } if ($field = FieldConfig::loadByName($entity_type_id, $bundle, OgGroupAudienceHelper::DEFAULT_FIELD)) { $handler_settings = $field->getSetting('handler_settings'); - $save = FALSE; - foreach (['target_type', 'target_bundles'] as $key) { - $entity_key = 'og_' . $key; - if (!isset($handler_settings[$key]) || $entity->$entity_key != $handler_settings[$key]) { - $handler_settings[$key] = $entity->$entity_key; - $field->setSetting('handler_settings', $handler_settings); - $save = TRUE; - } - } - if ($save) { + if (!isset($handler_settings['target_bundles']) || $entity->og_target_bundles != $handler_settings['target_bundles']) { + $handler_settings['target_bundles'] = $entity->og_target_bundles; + $field->setSetting('handler_settings', $handler_settings); $field->save(); } } diff --git a/og_ui/og_ui.routing.yml b/og_ui/og_ui.routing.yml index 81d533fc9..04c448b52 100644 --- a/og_ui/og_ui.routing.yml +++ b/og_ui/og_ui.routing.yml @@ -15,3 +15,28 @@ og_ui.settings: _title: 'OG settings' requirements: _permission: 'administer group' + +og_ui.roles_permissions_overview: + path: 'admin/config/group/{type}' + defaults: + _controller: '\Drupal\og_ui\Controller\OgUiController::rolesPermissionsOverviewPage' + _title_callback: '\Drupal\og_ui\Controller\OgUiController::rolesPermissionsOverviewTitleCallback' + requirements: + _permission: 'administer group' + type: '^(roles|permissions)$' + +og_ui.roles_form: + path: 'admin/config/group/roles/{entity_type}/{bundle}' + defaults: + _form: '\Drupal\og_ui\Form\OgRolesForm' + _title: '@todo - create title callback' + requirements: + _permission: 'administer group' + +og_ui.permissions_form: + path: 'admin/config/group/permissions/{entity_type}/{bundle}' + defaults: + _form: '\Drupal\og_ui\Form\OgPermissionsForm' + _title: '@todo - create title callback' + requirements: + _permission: 'administer group' diff --git a/og_ui/og_ui.services.yml b/og_ui/og_ui.services.yml new file mode 100644 index 000000000..3aed6aa14 --- /dev/null +++ b/og_ui/og_ui.services.yml @@ -0,0 +1,5 @@ +services: + og_ui.event_subscriber: + class: Drupal\og_ui\EventSubscriber\OgUiEventSubscriber + tags: + - { name: 'event_subscriber' } diff --git a/og_ui/src/BundleFormAlter.php b/og_ui/src/BundleFormAlter.php index e2a85ffe1..a5e205472 100644 --- a/og_ui/src/BundleFormAlter.php +++ b/og_ui/src/BundleFormAlter.php @@ -63,8 +63,8 @@ public function formAlter(array &$form, FormStateInterface $form_state) { /** * AJAX callback displaying the target bundles select box. */ - public function ajaxCallback(array $form, array &$form_state) { - return $form['og']['target_bundles']; + public function ajaxCallback(array $form, FormStateInterface $form_state) { + return $form['og']['og_target_bundles']; } /** @@ -73,7 +73,7 @@ public function ajaxCallback(array $form, array &$form_state) { * @param array $form * @param $form_state */ - protected function prepare(array &$form, $form_state) { + protected function prepare(array &$form, FormStateInterface $form_state) { // Example: article. $this->bundle = $this->entity->id(); // Example: Article. @@ -82,115 +82,125 @@ protected function prepare(array &$form, $form_state) { // Example: node. $this->entityTypeId = $this->definition->getBundleOf(); - $form['og'] = array( + $form['og'] = [ '#type' => 'details', '#title' => t('Organic groups'), '#collapsible' => TRUE, '#group' => 'additional_settings', '#description' => t('This bundle may serve as a group, may belong to a group, or may not participate in OG at all.'), - ); + ]; } /** * Adds the "is group?" checkbox. */ - protected function addGroupType(array &$form, $form_state) { - $form['og']['og_is_group'] = array( + protected function addGroupType(array &$form, FormStateInterface $form_state) { + if ($this->entity->isNew()) { + $description = t('Every entity in this bundle is a group which can contain entities and can have members.'); + } + else { + $description = t('Every "%bundle" is a group which can contain entities and can have members.', [ + '%bundle' => Unicode::lcfirst($this->bundleLabel), + ]); + } + $form['og']['og_is_group'] = [ '#type' => 'checkbox', '#title' => t('Group'), '#default_value' => Og::isGroup($this->entityTypeId, $this->bundle), - '#description' => t('Every "%bundle" is a group which can contain entities and can have members.', [ - '%bundle' => Unicode::lcfirst($this->bundleLabel), - ]), - ); + '#description' => $description, + ]; } /** * Adds the "is group content?" checkbox and target settings elements. */ - protected function addGroupContent(array &$form, $form_state) { - $is_group_content = Og::isGroupContent($this->entityTypeId, $this->bundle); - - $target_type_default = FALSE; - $handler_settings = []; - if ($field = FieldConfig::loadByName($this->entityTypeId, $this->bundle, OgGroupAudienceHelper::DEFAULT_FIELD)) { - $handler_settings = $field->getSetting('handler_settings'); - if (isset($handler_settings['target_type'])) { - $target_type_default = $handler_settings['target_type']; - } - } + protected function addGroupContent(array &$form, FormStateInterface $form_state) { + // Get the stored config from the default group audience field if it exists. + $field = FieldConfig::loadByName($this->entityTypeId, $this->bundle, OgGroupAudienceHelper::DEFAULT_FIELD); + $handler_settings = $field ? $field->getSetting('handler_settings') : []; + // Compile a list of group entity types and bundles. $target_types = []; - $bundle_options = []; - $all_group_bundles = Og::groupManager()->getAllGroupBundles(); - foreach ($all_group_bundles as $group_entity_type => $bundles) { - if (!$target_type_default) { - $target_type_default = $group_entity_type; - } - $target_types[$group_entity_type] = \Drupal::entityTypeManager() - ->getDefinition($group_entity_type) - ->getLabel(); - } - - if ($all_group_bundles) { - $bundle_info = \Drupal::service('entity_type.bundle.info') - ->getBundleInfo($target_type_default); - foreach ($all_group_bundles[$target_type_default] as $bundle_name) { - $bundle_options[$bundle_name] = $bundle_info[$bundle_name]['label']; + $target_bundles = []; + foreach (Og::groupManager()->getAllGroupBundles() as $entity_type => $bundles) { + $target_types[$entity_type] = \Drupal::entityTypeManager()->getDefinition($entity_type)->getLabel(); + $bundle_info = \Drupal::service('entity_type.bundle.info')->getBundleInfo($entity_type); + foreach ($bundles as $bundle) { + $target_bundles[$entity_type][$bundle] = $bundle_info[$bundle]['label']; } - $description = ''; - } - else { - $description = t('There are no group bundles defined.'); } - $form['og']['og_group_content_bundle'] = array( + $form['og']['og_group_content_bundle'] = [ '#type' => 'checkbox', '#title' => t('Group content'), - '#default_value' => $is_group_content, - '#description' => $description, - ); + '#default_value' => Og::isGroupContent($this->entityTypeId, $this->bundle), + '#description' => empty($target_bundles) ? t('There are no group bundles defined.') : '', + ]; if ($target_types) { - // Don't show the settings, as there might be multiple OG audience fields - // in the same bundle. - $form['og']['og_target_type'] = array( + // If a group audience field already exists, use its value. Otherwise fall + // back to the first entity type that was returned. + reset($target_types); + $target_type_default = $field && !empty($field->getSetting('target_type')) ? $field->getSetting('target_type') : key($target_types); + + // If the target type was set using AJAX, use that instead of the default. + $ajax_value = $form_state->getValue('og_target_type'); + $target_type_default = $ajax_value ? $ajax_value : $target_type_default; + + $form['og']['og_target_type'] = [ '#type' => 'select', '#title' => t('Target type'), '#options' => $target_types, '#default_value' => $target_type_default, - '#description' => t('The entity type that can be referenced thru this field.'), - '#ajax' => array( + '#description' => t('The entity type that can be referenced through this field.'), + '#ajax' => [ 'callback' => [$this, 'ajaxCallback'], 'wrapper' => 'og-settings-wrapper', - ), - '#states' => array( - 'visible' => array( - ':input[name="og_group_content_bundle"]' => array('checked' => TRUE), - ), - ), - ); + ], + '#states' => [ + 'visible' => [ + ':input[name="og_group_content_bundle"]' => ['checked' => TRUE], + ], + ], + ]; // Get the bundles that are acting as group. - $form['og']['og_target_bundles'] = array( + $form['og']['og_target_bundles'] = [ '#prefix' => '
', '#suffix' => '
', '#type' => 'select', '#title' => t('Target bundles'), - '#options' => $bundle_options, - '#default_value' => isset($handler_settings['target_bundles']) ? $handler_settings['target_bundles'] : [], + '#options' => $target_bundles[$target_type_default], + '#default_value' => !empty($handler_settings['target_bundles']) ? $handler_settings['target_bundles'] : NULL, '#multiple' => TRUE, '#description' => t('The bundles of the entity type that can be referenced. Optional, leave empty for all bundles.'), - '#states' => array( - 'visible' => array( - ':input[name="og_group_content_bundle"]' => array('checked' => TRUE), - ), - ), - ); + '#states' => [ + 'visible' => [ + ':input[name="og_group_content_bundle"]' => ['checked' => TRUE], + ], + ], + ]; + $form['#validate'][] = [get_class($this), 'validateTargetBundleElement']; } else { + // Don't show the settings, as there might be multiple OG audience fields + // in the same bundle. $form['og']['og_group_content_bundle']['#disabled'] = TRUE; } } + /** + * Form validate handler. + */ + public static function validateTargetBundleElement(array &$form, FormStateInterface $form_state) { + // If no checkboxes were checked for 'og_target_bundles', store NULL ("all + // bundles are referenceable") rather than empty array ("no bundle is + // referenceable" - typically happens when all referenceable bundles have + // been deleted). + if ($form_state->getValue('og_target_bundles') === []) { + $form_state->setValue('og_target_bundles', NULL); + } + } + + } diff --git a/og_ui/src/Controller/OgUiController.php b/og_ui/src/Controller/OgUiController.php new file mode 100644 index 000000000..3398b7320 --- /dev/null +++ b/og_ui/src/Controller/OgUiController.php @@ -0,0 +1,117 @@ +groupManager = $group_manager; + $this->entityTypeManager = $entity_type_manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('og.group.manager'), + $container->get('entity_type.manager'), + $container->get('entity_type.bundle.info') + ); + } + + /** + * Returns the overview of OG roles and permissions. + * + * @param string $type + * The type of overview, either 'roles' or 'permissions'. + * + * @return array + * The overview as a render array. + */ + public function rolesPermissionsOverviewPage($type) { + $action = $type === 'roles' ? t('Edit roles') : t('Edit permissions'); + $header = [t('Group type'), t('Operations')]; + $rows = []; + + foreach ($this->groupManager->getAllGroupBundles() as $entity_type => $bundles) { + $definition = $this->entityTypeManager->getDefinition($entity_type); + $bundle_info = $this->entityTypeBundleInfo->getBundleInfo($entity_type); + foreach ($bundles as $bundle) { + $rows[] = [ + [ + 'data' => $definition->getLabel() . ' - ' . $bundle_info[$bundle]['label'], + ], + [ + 'data' => Link::createFromRoute($action, 'og_ui.' . $type . '_form', [ + 'entity_type' => $entity_type, + 'bundle' => $bundle, + ]), + ], + ]; + } + } + + $build['roles_table'] = [ + '#theme' => 'table', + '#header' => $header, + '#rows' => $rows, + '#empty' => $this->t('No group types available.'), + ]; + + return $build; + } + + /** + * Title callback for rolesPermissionsOverviewPage. + * + * @param string $type + * The type of overview, either 'roles' or 'permissions'. + * + * @return \Drupal\Core\StringTranslation\TranslatableMarkup + */ + public function rolesPermissionsOverviewTitleCallback($type) { + return $this->t('OG @type overview', ['@type' => $type]); + } + +} diff --git a/og_ui/src/EventSubscriber/OgUiEventSubscriber.php b/og_ui/src/EventSubscriber/OgUiEventSubscriber.php new file mode 100644 index 000000000..c8b73d9c8 --- /dev/null +++ b/og_ui/src/EventSubscriber/OgUiEventSubscriber.php @@ -0,0 +1,80 @@ + [['provideDefaultOgPermissions']], + ]; + } + + /** + * Provides default OG permissions. + * + * @param \Drupal\og\Event\PermissionEventInterface $event + * The OG permission event. + */ + public function provideDefaultOgPermissions(PermissionEventInterface $event) { + $event->setPermissions([ + 'subscribe' => [ + 'title' => t('Subscribe to group'), + 'description' => t('Allow non-members to request membership to a group (approval required).'), + 'roles' => [OgRoleInterface::ANONYMOUS], + 'default roles' => [OgRoleInterface::ANONYMOUS], + ], + 'subscribe without approval' => [ + 'title' => t('Subscribe to group (no approval required)'), + 'description' => t('Allow non-members to join a group without an approval from group administrators.'), + 'roles' => [OgRoleInterface::ANONYMOUS], + ], + 'unsubscribe' => [ + 'title' => t('Unsubscribe from group'), + 'description' => t('Allow members to unsubscribe themselves from a group, removing their membership.'), + 'roles' => [OgRoleInterface::AUTHENTICATED], + 'default roles' => [OgRoleInterface::AUTHENTICATED], + ], + 'approve and deny subscription' => [ + 'title' => t('Approve and deny subscription'), + 'description' => t('Users may allow or deny another user\'s subscription request.'), + 'default roles' => [OgRoleInterface::ADMINISTRATOR], + ], + 'add user' => [ + 'title' => t('Add user'), + 'description' => t('Users may add other users to the group without approval.'), + 'default roles' => [OgRoleInterface::ADMINISTRATOR], + ], + 'manage members' => [ + 'title' => t('Manage members'), + 'description' => t('Users may remove group members and alter member status and roles.'), + 'default roles' => [OgRoleInterface::ADMINISTRATOR], + 'restrict access' => TRUE, + ], + 'manage roles' => [ + 'title' => t('Add roles'), + 'description' => t('Users may view group roles and add new roles if group default roless are overridden.'), + 'default roles' => [OgRoleInterface::ADMINISTRATOR], + 'restrict access' => TRUE, + ], + 'manage permissions' => [ + 'title' => t('Manage permissions'), + 'description' => t('Users may view the group permissions page and change permissions if group default roless are overridden.'), + 'default roles' => [OgRoleInterface::ADMINISTRATOR], + 'restrict access' => TRUE, + ], + ]); + } + +} + diff --git a/og_ui/src/Form/AdminSettingsForm.php b/og_ui/src/Form/AdminSettingsForm.php index 851384b9b..48250840b 100644 --- a/og_ui/src/Form/AdminSettingsForm.php +++ b/og_ui/src/Form/AdminSettingsForm.php @@ -7,14 +7,47 @@ namespace Drupal\og_ui\Form; +use Drupal\Component\Plugin\PluginManagerInterface; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Form\ConfigFormBase; use Drupal\Core\Form\FormStateInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Provides the main administration settings form for Organic groups. */ class AdminSettingsForm extends ConfigFormBase { + /** + * The manager for OgDeleteOrphans plugins. + * + * @var \Drupal\Component\Plugin\PluginManagerInterface + */ + protected $ogDeleteOrphansPluginManager; + + /** + * Constructs an AdminSettingsForm object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The factory for configuration objects. + * @param \Drupal\Component\Plugin\PluginManagerInterface $delete_orphans_plugin_manager + * The manager for OgDeleteOrphans plugins. + */ + public function __construct(ConfigFactoryInterface $config_factory, PluginManagerInterface $delete_orphans_plugin_manager) { + parent::__construct($config_factory); + $this->ogDeleteOrphansPluginManager = $delete_orphans_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('config.factory'), + $container->get('plugin.manager.og.delete_orphans') + ); + } + /** * {@inheritdoc} */ @@ -56,26 +89,55 @@ public function buildForm(array $form, FormStateInterface $form_state) { // @todo: Port og_ui_admin_people_view. - $form['og_use_queue'] = [ - '#type' => 'checkbox', - '#title' => $this->t('Use queue'), - '#description' => t("Use the core's queue process for operations such as deleting memberships when groups are deleted."), - '#default_value' => $config_og->get('use_queue'), - ]; - - $form['og_orphans_delete'] = [ + $form['og_delete_orphans'] = [ '#type' => 'checkbox', '#title' => $this->t('Delete orphans'), '#description' => $this->t('Delete orphaned group content (excluding users) when a group is deleted.'), - '#default_value' => $config_og->get('orphans_delete'), + '#default_value' => $config_og->get('delete_orphans'), + ]; + + $definitions = $this->ogDeleteOrphansPluginManager->getDefinitions(); + ksort($definitions); + $options = array_map(function ($definition) { + return $definition['label']; + }, $definitions); + + $form['og_delete_orphans_plugin_id'] = [ + '#type' => 'radios', + '#title' => $this->t('Deletion method'), + '#default_value' => $config_og->get('delete_orphans_plugin_id'), + '#options' => $options, '#states' => [ 'visible' => [ - ':input[name="og_use_queue"]' => ['checked' => TRUE], + ':input[name="og_delete_orphans"]' => ['checked' => TRUE], ], ], '#attributes' => ['class' => ['child-item']], ]; + foreach ($definitions as $id => $definition) { + /** @var \Drupal\og\OgDeleteOrphansInterface $plugin */ + $plugin = $this->ogDeleteOrphansPluginManager->createInstance($id, []); + + // Add the description for each delete method. + $form['og_delete_orphans_plugin_id'][$id] = [ + '#description' => $definition['description'], + ]; + + // Show the configuration options for the chosen plugin. + $configuration = $plugin->configurationForm($form, $form_state); + if ($configuration) { + $form['og_delete_orphans_options_' . $id] = $configuration + [ + '#states' => [ + 'visible' => [ + ':input[name="og_delete_orphans"]' => ['checked' => TRUE], + ':input[name="og_delete_orphans_plugin_id"]' => ['value' => $id], + ], + ], + ]; + } + } + $form['#attached']['library'][] = 'og_ui/form'; return $form; @@ -91,7 +153,8 @@ public function submitForm(array &$form, FormStateInterface $form_state) { ->set('group_manager_full_access', $form_state->getValue('og_group_manager_full_access')) ->set('node_access_strict', $form_state->getValue('og_node_access_strict')) ->set('use_queue', $form_state->getValue('og_use_queue')) - ->set('orphans_delete', $form_state->getValue('og_orphans_delete')) + ->set('delete_orphans', $form_state->getValue('og_delete_orphans')) + ->set('delete_orphans_plugin_id', $form_state->getValue('og_delete_orphans_plugin_id')) ->save(); } diff --git a/og_ui/src/Tests/BundleFormAlterTest.php b/og_ui/src/Tests/BundleFormAlterTest.php deleted file mode 100644 index 1e0b5d699..000000000 --- a/og_ui/src/Tests/BundleFormAlterTest.php +++ /dev/null @@ -1,47 +0,0 @@ -drupalCreateUser(array('bypass node access', 'administer content types')); - $this->drupalLogin($web_user); - $edit = [ - 'name' => 'school', - 'type' => 'school', - 'og_is_group' => 1, - ]; - $this->drupalPostForm('admin/structure/types/add', $edit, t('Save content type')); - $edit = [ - 'name' => 'class', - 'type' => 'class', - 'og_group_content_bundle' => 1, - 'og_target_type' => 'node', - 'og_target_bundles[]' => ['school'], - ]; - $this->drupalPostForm('admin/structure/types/add', $edit, t('Save content type')); - $this->drupalGet('admin/structure/types/manage/class'); - $this->assertOptionSelected('edit-og-target-bundles', 'school'); - } - -} diff --git a/og_ui/tests/src/Functional/BundleFormAlterTest.php b/og_ui/tests/src/Functional/BundleFormAlterTest.php new file mode 100644 index 000000000..fd4505a4b --- /dev/null +++ b/og_ui/tests/src/Functional/BundleFormAlterTest.php @@ -0,0 +1,167 @@ +entityTypeManager = \Drupal::entityTypeManager(); + + // Log in as an administrator that can manage blocks and content types. + $this->adminUser = $this->drupalCreateUser([ + 'administer blocks', + 'administer content types', + 'bypass node access', + ]); + $this->drupalLogin($this->adminUser); + } + + /** + * Test that group and group content bundles can be created through the UI. + */ + public function testCreate() { + // Create a custom block and define it as a group type. We make sure the + // group and group content are of different entity types so we can test that + // the correct entity type is referenced. + $edit = [ + 'label' => 'school', + 'id' => 'school', + 'og_is_group' => 1, + ]; + $this->drupalGet('admin/structure/block/block-content/types/add'); + $this->submitForm($edit, t('Save')); + + $edit = [ + 'name' => 'class', + 'type' => 'class', + 'og_group_content_bundle' => 1, + 'og_target_type' => 'block_content', + 'og_target_bundles[]' => ['school'], + ]; + $this->drupalGet('admin/structure/types/add'); + $this->submitForm($edit, t('Save content type')); + $this->content = $this->drupalGet('admin/structure/types/manage/class'); + $this->assertOptionSelected('edit-og-target-bundles', 'school'); + $this->assertTargetType('block_content', 'The target type is set to the "Custom Block" entity type.'); + $this->assertTargetBundles(['school' => 'school'], 'The target bundles are set to the "school" bundle.'); + + // Test that if the target bundles are unselected, the value for the target + // bundles becomes NULL rather than an empty array. The entity reference + // selection plugin considers the value NULL to mean 'all bundles', while an + // empty array means 'no bundles are allowed'. + // @see \Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection::buildEntityQuery() + $edit = [ + 'name' => 'class', + 'og_group_content_bundle' => 1, + 'og_target_type' => 'block_content', + 'og_target_bundles[]' => [], + ]; + $this->drupalGet('admin/structure/types/manage/class'); + $this->submitForm($edit, t('Save content type')); + $this->assertTargetBundles(NULL, 'When the target bundle field is cleared from all values, it takes on the value NULL.'); + } + + /** + * Tests AJAX behavior for selecting group content entity types and bundles. + */ + public function testGroupContentAjax() { + // Create two group bundles of different entity types. + NodeType::create(['name' => 'group node', 'type' => 'group'])->save(); + Og::groupManager()->addGroup('node', 'group'); + Og::groupManager()->addGroup('entity_test', 'entity_test'); + + // BrowserTestBase doesn't support JavaScript yet. Replace the following + // unit test with a functional test once JavaScript support is added. + // @see https://www.drupal.org/node/2469713 + $form = []; + $form_state = new FormState(); + // Set the form state as if the 'entity_test' option was chosen with AJAX. + $form_state->setValue('og_target_type', 'entity_test'); + $entity = $this->entityTypeManager->getStorage('node_type')->create([]); + (new BundleFormAlter($entity))->formAlter($form, $form_state); + + // Check that the target bundles are set to the test entity bundle. + $this->assertEquals(['entity_test' => 'Entity Test Bundle'], $form['og']['og_target_bundles']['#options']); + } + + /** + * Checks whether the target bundles in the group content are as expected. + * + * @param array|NULL $expected + * The expected value for the target bundles. + * @param string $message + * The message to display with the assertion. + */ + protected function assertTargetBundles($expected, $message) { + /** @var EntityFieldManagerInterface $entity_field_manager */ + $entity_field_manager = $this->container->get('entity_field.manager'); + $entity_field_manager->clearCachedFieldDefinitions(); + $field_definitions = $entity_field_manager->getFieldDefinitions('node', 'class'); + $settings = $field_definitions['og_group_ref']->getSetting('handler_settings'); + $this->assertEquals($expected, $settings['target_bundles'], $message); + } + + /** + * Checks whether the target entity type in the group content is as expected. + * + * @param string $expected + * The expected target entity type. + * @param string $message + * The message to display with the assertion. + */ + protected function assertTargetType($expected, $message) { + /** @var EntityFieldManagerInterface $entity_field_manager */ + $entity_field_manager = $this->container->get('entity_field.manager'); + $entity_field_manager->clearCachedFieldDefinitions(); + $field_definitions = $entity_field_manager->getFieldStorageDefinitions('node'); + $setting = $field_definitions['og_group_ref']->getSetting('target_type'); + $this->assertEquals($expected, $setting, $message); + } + +} diff --git a/src/Annotation/OgDeleteOrphans.php b/src/Annotation/OgDeleteOrphans.php new file mode 100644 index 000000000..7056f7ce0 --- /dev/null +++ b/src/Annotation/OgDeleteOrphans.php @@ -0,0 +1,39 @@ + \Drupal\og\OgMembershipInterface::TYPE_DEFAULT)); + * $membership = OgMembership::create(['type' => \Drupal\og\OgMembershipInterface::TYPE_DEFAULT]); * $membership - * ->setContentId(2) - * ->setContentType('node') - * ->setGid(1) - * ->setEntityType('node') + * ->setUser(2) + * ->setEntityId(1) + * ->setGroupEntityType('node') * ->setFieldName(OgGroupAudienceHelper::DEFAULT_FIELD) * ->save(); * @endcode * - * Although the reference stored in the base table og_membership, there is a - * need for an easy way the group and the group content content via the UI. This - * is where the entity reference field come in: The field tables in the DB are - * empty, but when asking the content of the field there a is work behind the - * scene that structured the field value's on the fly. That's one of OG magic. + * Notice how the relation of the user to the group also includes the OG + * audience field name this association was done by. Like this we are able to + * express different membership types such as the default membership that comes + * out of the box, or a "premium membership" that can be for example expired + * after a certain amount of time (the logic for the expired membership in the + * example is out of the scope of OG core). + * + * Having this field separation is what allows having multiple OG audience + * fields attached to the user, where each group they are associated with may be + * a result of different membership types. + * * * @ContentEntityType( * id = "og_membership", @@ -50,10 +73,13 @@ * }, * bundle_keys = { * "bundle" = "type" + * }, + * handlers = { + * "views_data" = "Drupal\og\OgMembershipViewsData", * } * ) */ -class OgMembership extends ContentEntityBase implements ContentEntityInterface { +class OgMembership extends ContentEntityBase implements OgMembershipInterface { /** * {@inheritdoc} @@ -62,7 +88,6 @@ public function getCreatedTime() { return $this->get('created')->value; } - /** * {@inheritdoc} */ @@ -71,45 +96,23 @@ public function setCreatedTime($timestamp) { return $this; } - - /** - * @param mixed $entityType - * - * @return OgMembership. - */ - public function setMemberEntityType($entityType) { - $this->set('member_entity_type', $entityType); - return $this; - } - - /** - * @return string - */ - public function getMemberEntityType() { - return $this->get('member_entity_type')->value; - } - /** - * @param mixed $etid - * - * @return OgMembership + * {@inheritdoc} */ - public function setMemberEntityId($etid) { - $this->set('member_entity_id', $etid); + public function setUser($etid) { + $this->set('uid', $etid); return $this; } /** - * @return mixed + * {@inheritdoc} */ - public function getMemberEntityId() { - return $this->get('member_entity_type')->value; + public function getUser() { + return $this->get('uid')->entity; } /** - * @param mixed $fieldName - * - * @return OgMembership. + * {@inheritdoc} */ public function setFieldName($fieldName) { $this->set('field_name', $fieldName); @@ -117,50 +120,44 @@ public function setFieldName($fieldName) { } /** - * @return mixed + * {@inheritdoc} */ public function getFieldName() { return $this->get('field_name')->value; } /** - * @param mixed $gid - * - * @return OgMembership. + * {@inheritdoc} */ - public function setGroupEntityid($gid) { - $this->set('group_entity_id', $gid); + public function setEntityId($gid) { + $this->set('entity_id', $gid); return $this; } /** - * @return mixed + * {@inheritdoc} */ - public function getGroupEntityid() { - return $this->get('group_entity_id')->value; + public function getEntityId() { + return $this->get('entity_id')->value; } /** - * @param mixed $groupType - * - * @return OgMembership + * {@inheritdoc} */ public function setGroupEntityType($groupType) { - $this->set('group_entity_type', $groupType); + $this->set('entity_type', $groupType); return $this; } /** - * @return mixed + * {@inheritdoc} */ public function getGroupEntityType() { - return $this->get('group_entity_type')->value; + return $this->get('entity_type')->value; } /** - * @param mixed $state - * - * @return OgMembership. + * {@inheritdoc} */ public function setState($state) { $this->set('state', $state); @@ -168,7 +165,7 @@ public function setState($state) { } /** - * @return mixed + * {@inheritdoc} */ public function getState() { return $this->get('state')->value; @@ -181,6 +178,61 @@ public function getType() { return $this->bundle(); } + /** + * {@inheritdoc} + */ + public function setRoles($role_ids) { + $this->set('roles', $role_ids); + + return $this; + } + + /** + * {@inheritdoc} + */ + public function addRole($role_id) { + $rids = $this->getRolesIds(); + $rids[] = $role_id; + + return $this->setRoles(array_unique($rids)); + } + + /** + * {@inheritdoc} + */ + public function revokeRole($role_id) { + $rids = $this->getRolesIds(); + $key = array_search($role_id, $rids); + unset($rids[$key]); + + return $this->setRoles(array_unique($rids)); + } + + /** + * {@inheritdoc} + */ + public function getRoles() { + return $this->get('roles')->referencedEntities(); + } + + /** + * {@inheritdoc} + */ + public function getRolesIds() { + return array_map(function (OgRole $role) { + return $role->id(); + }, $this->getRoles()); + } + + /** + * {@inheritdoc} + */ + public function hasPermission($permission) { + return array_filter($this->getRoles(), function (OgRole $role) use ($permission) { + return $role->hasPermission($permission); + }); + } + /** * {@inheritdoc} */ @@ -203,19 +255,16 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setDescription(t('The bundle of the membership')) ->setSetting('target_type', 'og_membership_type'); - $fields['member_entity_type'] = BaseFieldDefinition::create('string') - ->setLabel(t('Member entity type.')) - ->setDescription(t("The entity type (e.g. node, comment, etc') of the member.")); + $fields['uid'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Member User ID')) + ->setDescription(t('The user ID of the member.')) + ->setSetting('target_type', 'user'); - $fields['member_entity_id'] = BaseFieldDefinition::create('integer') - ->setLabel(t('Member entity ID')) - ->setDescription(t('The entity ID of the member.')); - - $fields['group_entity_type'] = BaseFieldDefinition::create('string') + $fields['entity_type'] = BaseFieldDefinition::create('string') ->setLabel(t('Group entity type')) ->setDescription(t('The entity type of the group.')); - $fields['group_entity_id'] = BaseFieldDefinition::create('integer') + $fields['entity_id'] = BaseFieldDefinition::create('string') ->setLabel(t('Group entity id.')) ->setDescription(t("The entity ID of the group.")); @@ -224,6 +273,12 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setDescription(t("The state of the group content.")) ->setDefaultValue(TRUE); + $fields['roles'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Roles')) + ->setDescription(t('The OG roles related to an OG membership entity.')) + ->setCardinality(FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED) + ->setSetting('target_type', 'og_role'); + $fields['created'] = BaseFieldDefinition::create('created') ->setLabel(t('Create')) ->setDescription(t('The Unix timestamp when the group content was created.')); @@ -243,30 +298,42 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { /** * {@inheritdoc} */ - public function PreSave(EntityStorageInterface $storage) { + public function preSave(EntityStorageInterface $storage) { if (!$this->getFieldName()) { $this->setFieldName(OgGroupAudienceHelper::DEFAULT_FIELD); } - parent::PreSave($storage); + // Check the value directly rather than using the entity, if there is one. + // This will watch actual empty values and '0'. + if (!$this->get('uid')->target_id) { + // Throw a generic logic exception as this will likely get caught in + // \Drupal\Core\Entity\Sql\SqlContentEntityStorage::save and turned in an + // EntityStorageException anyway. + throw new \LogicException('OG membership can not be created for an empty or anonymous user.'); + } + + parent::preSave($storage); } /** - * Get the group object. - * - * @return EntityInterface + * {@inheritdoc} */ - public function getGroup() { - return \Drupal::entityTypeManager()->getStorage($this->getGroupEntityType())->load($this->getGroupEntityid()); + public function save() { + $result = parent::save(); + + // Reset internal cache. + Og::reset(); + \Drupal::service('og.access')->reset(); + + return $result; } /** - * Get the entity belong to the current membership. - * - * @return EntityInterface + * {@inheritdoc} */ - public function getEntityMembership() { - return \Drupal::entityTypeManager()->getStorage($this->get('member_entity_type'))->load($this->getMemberEntityId()); + public function getGroup() { + return \Drupal::entityTypeManager()->getStorage($this->getGroupEntityType())->load($this->getEntityId()); } + } diff --git a/src/Entity/OgRole.php b/src/Entity/OgRole.php index bed9b9590..112a324d7 100644 --- a/src/Entity/OgRole.php +++ b/src/Entity/OgRole.php @@ -1,12 +1,9 @@ id = $id; $this->set('id', $id); return $this; } /** + * Returns the label. + * * @return string + * The label. */ public function getLabel() { return $this->get('label'); } /** + * Sets the label. + * * @param string $label + * The label to set. * - * @return OgRole + * @return $this */ public function setLabel($label) { - $this->label = $label; $this->set('label', $label); return $this; } /** + * Returns the group ID. + * * @return int + * The group ID. */ - public function getGroupID() { + public function getGroupId() { return $this->get('group_id'); } /** - * @param int $groupID + * Sets the group ID. + * + * @param int $group_id + * The group ID to set. * - * @return OgRole + * @return $this */ - public function setGroupID($groupID) { - $this->group_id = $groupID; - $this->set('group_id', $groupID); + public function setGroupId($group_id) { + $this->set('group_id', $group_id); return $this; } /** + * Returns the group type. + * * @return string + * The group type. */ public function getGroupType() { return $this->get('group_type'); } /** - * @param string $groupType + * Sets the group type. + * + * @param string $group_type + * The group type to set. * - * @return OgRole + * @return $this */ - public function setGroupType($groupType) { - $this->group_type = $groupType; - $this->set('group_type', $groupType); + public function setGroupType($group_type) { + $this->set('group_type', $group_type); return $this; } /** + * Returns the group bundle. + * * @return string + * The group bundle. */ public function getGroupBundle() { return $this->get('group_bundle'); } /** - * @param string $groupBundle + * Sets the group bundle. * - * @return OgRole + * @param string $group_bundle + * The group bundle to set. + * + * @return $this */ - public function setGroupBundle($groupBundle) { - $this->group_bundle = $groupBundle; - $this->set('group_bundle', $groupBundle); + public function setGroupBundle($group_bundle) { + $this->set('group_bundle', $group_bundle); return $this; } + /** + * Returns the role type. + * + * @return string + * The role type. One of OgRoleInterface::ROLE_TYPE_REQUIRED or + * OgRoleInterface::ROLE_TYPE_STANDARD. + */ + public function getRoleType() { + return $this->get('role_type'); + } + + /** + * Sets the role type. + * + * @param string $role_type + * The role type to set. One of OgRoleInterface::ROLE_TYPE_REQUIRED or + * OgRoleInterface::ROLE_TYPE_STANDARD. + * + * @return $this + * + * @throws \InvalidArgumentException + * Thrown when an invalid role type is given. + */ + public function setRoleType($role_type) { + if (!in_array($role_type, [ + self::ROLE_TYPE_REQUIRED, + self::ROLE_TYPE_STANDARD, + ])) { + throw new \InvalidArgumentException("'$role_type' is not a valid role type."); + } + return $this->set('role_type', $role_type); + } + /** * {@inheritdoc} */ public function save() { - if ($this->isNew()) { - - if (empty($this->group_type)) { + if (empty($this->getGroupType())) { throw new ConfigValueException('The group type can not be empty.'); } - if (empty($this->group_bundle)) { + if (empty($this->getGroupBundle())) { throw new ConfigValueException('The group bundle can not be empty.'); } // When assigning a role to group we need to add a prefix to the ID in // order to prevent duplicate IDs. - $prefix = $this->group_type . '-' . $this->group_bundle . '-'; + $prefix = $this->getGroupType() . '-' . $this->getGroupBundle() . '-'; - if (!empty($this->group_id)) { - $prefix .= $this->group_id . '-'; + if (!empty($this->getGroupId())) { + $prefix .= $this->getGroupId() . '-'; } $this->id = $prefix . $this->id(); @@ -172,4 +200,100 @@ public function save() { parent::save(); } + + /** + * {@inheritdoc} + */ + public function set($property_name, $value) { + // Prevent the ID, role type, group ID, group entity type or bundle from + // being changed once they are set. These properties are required and + // shouldn't be tampered with. + $is_locked_property = in_array($property_name, [ + 'id', + 'role_type', + 'group_id', + 'group_type', + 'group_bundle', + ]); + if ($is_locked_property && !$this->isNew()) { + throw new OgRoleException("The $property_name cannot be changed."); + } + return parent::set($property_name, $value); + } + + /** + * {@inheritdoc} + */ + public function delete() { + // The default roles are required. Prevent them from being deleted for as + // long as the group still exists. + if (in_array($this->id(), [self::ANONYMOUS, self::AUTHENTICATED]) && $this->groupManager()->isGroup($this->getGroupType(), $this->getGroupBundle())) { + throw new OgRoleException('The default roles "non-member" and "member" cannot be deleted.'); + } + parent::delete(); + } + + /** + * Returns default properties for the default OG roles. + * + * These are the two roles that are required by every group: the 'member' and + * 'non-member' roles. + * + * All other default roles are provided by DefaultRoleEvent. + * + * @return array + * An array of properties, keyed by OG role. + * + * @see \Drupal\og\Event\DefaultRoleEventInterface + * @see \Drupal\og\GroupManager::getDefaultRoles() + */ + public static function getDefaultRoles() { + return [ + self::ANONYMOUS => [ + 'role_type' => OgRoleInterface::ROLE_TYPE_REQUIRED, + 'label' => 'Non-member', + ], + self::AUTHENTICATED => [ + 'role_type' => OgRoleInterface::ROLE_TYPE_REQUIRED, + 'label' => 'Member', + ], + ]; + } + + /** + * Maps role names to role types. + * + * The 'anonymous' and 'authenticated' roles should not be changed or deleted. + * All others are standard roles. + * + * @param string $role_name + * The role name for which to return the type. + * + * @return string + * The role type, either OgRoleInterface::ROLE_TYPE_REQUIRED or + * OgRoleInterface::ROLE_TYPE_STANDARD. + */ + public static function getRoleTypeByName($role_name) { + return in_array($role_name, [ + OgRoleInterface::ANONYMOUS, + OgRoleInterface::AUTHENTICATED, + ]) ? OgRoleInterface::ROLE_TYPE_REQUIRED : OgRoleInterface::ROLE_TYPE_STANDARD; + } + + /** + * Gets the group manager. + * + * @return \Drupal\og\GroupManager + * The group manager. + */ + protected function groupManager() { + // Returning the group manager by calling the global factory method might + // seem less than ideal, but Entity classes are not designed to work with + // proper dependency injection. The ::create() method only accepts a $values + // array, which is not compatible with ContainerInjectionInterface. + // See for example Entity::uuidGenerator() in the base Entity class, it + // also uses this pattern. + return \Drupal::service('og.group.manager'); + } + } diff --git a/src/Event/DefaultRoleEvent.php b/src/Event/DefaultRoleEvent.php new file mode 100644 index 000000000..82d8d7bf2 --- /dev/null +++ b/src/Event/DefaultRoleEvent.php @@ -0,0 +1,163 @@ +roles[$name])) { + throw new \InvalidArgumentException("The '$name' role does not exist.'"); + } + return $this->roles[$name]; + } + + /** + * {@inheritdoc} + */ + public function getRoles() { + return $this->roles; + } + + /** + * {@inheritdoc} + */ + public function addRole($name, array $properties) { + if (array_key_exists($name, $this->roles)) { + throw new \InvalidArgumentException("The '$name' role already exists."); + } + $this->validate($name, $properties); + + // Provide default value for the role type. + if (empty($properties['role_type'])) { + $properties['role_type'] = OgRoleInterface::ROLE_TYPE_STANDARD; + } + + $this->roles[$name] = $properties; + } + + /** + * {@inheritdoc} + */ + public function addRoles(array $roles) { + foreach ($roles as $role => $properties) { + $this->addRole($role, $properties); + } + } + + /** + * {@inheritdoc} + */ + public function setRole($name, array $properties) { + $this->deleteRole($name); + $this->addRole($name, $properties); + } + + /** + * {@inheritdoc} + */ + public function setRoles(array $roles) { + foreach ($roles as $name => $properties) { + $this->setRole($name, $properties); + } + } + + /** + * {@inheritdoc} + */ + public function deleteRole($name) { + unset($this->roles[$name]); + } + + /** + * {@inheritdoc} + */ + public function hasRole($name) { + return isset($this->roles[$name]); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($key) { + return $this->getRole($key); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($key, $value) { + $this->setRole($key, $value); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($key) { + $this->deleteRole($key); + } + + /** + * {@inheritdoc} + */ + public function offsetExists($key) { + return $this->hasRole($key); + } + + /** + * {@inheritdoc} + */ + public function getIterator() { + return new \ArrayIterator($this->roles); + } + + /** + * Validates a role that is about to be set or added. + * + * @param string $name + * The name of the role to add or set. + * @param array $properties + * The role properties to validate. + * + * @throws \InvalidArgumentException + * Thrown when the role name is empty, the 'label' property is missing, or + * the 'role_type' property is invalid. + */ + protected function validate($name, $properties) { + if (empty($name)) { + throw new \InvalidArgumentException('Role name is required.'); + } + + if (empty($properties['label'])) { + throw new \InvalidArgumentException('The label property is required.'); + } + + $valid_role_types = [ + OgRoleInterface::ROLE_TYPE_STANDARD, + OgRoleInterface::ROLE_TYPE_REQUIRED, + ]; + if (!empty($properties['role_type']) && !in_array($properties['role_type'], $valid_role_types)) { + throw new \InvalidArgumentException('The role type is invalid.'); + } + } + +} diff --git a/src/Event/DefaultRoleEventInterface.php b/src/Event/DefaultRoleEventInterface.php new file mode 100644 index 000000000..89023bfaa --- /dev/null +++ b/src/Event/DefaultRoleEventInterface.php @@ -0,0 +1,105 @@ +entityTypeId = $entity_type_id; + $this->bundleId = $bundle_id; + } + + /** + * {@inheritdoc} + */ + public function getPermission($name) { + if (!isset($this->permissions[$name])) { + throw new \InvalidArgumentException("The '$name' permission does not exist.'"); + } + return $this->permissions[$name]; + } + + /** + * {@inheritdoc} + */ + public function getPermissions() { + return $this->permissions; + } + + /** + * {@inheritdoc} + */ + public function setPermission($name, array $permission) { + if (empty($name)) { + throw new \InvalidArgumentException('Permission name is required.'); + } + if (empty($permission['title'])) { + throw new \InvalidArgumentException('The permission title is required.'); + } + $this->permissions[$name] = $permission; + } + + /** + * {@inheritdoc} + */ + public function setPermissions(array $permissions) { + foreach ($permissions as $name => $permission) { + $this->setPermission($name, $permission); + } + } + + /** + * {@inheritdoc} + */ + public function deletePermission($name) { + if ($this->hasPermission($name)) { + unset($this->permissions[$name]); + } + } + + /** + * {@inheritdoc} + */ + public function hasPermission($name) { + return isset($this->permissions[$name]); + } + + /** + * {@inheritdoc} + */ + public function getEntityTypeId() { + return $this->entityTypeId; + } + + /** + * {@inheritdoc} + */ + public function getBundleId() { + return $this->bundleId; + } + + /** + * {@inheritdoc} + */ + public function filterByDefaultRole($role_name) { + return array_filter($this->permissions, function ($permission) use ($role_name) { + return !empty($permission['default roles']) && in_array($role_name, $permission['default roles']); + }); + } + + /** + * {@inheritdoc} + */ + public function offsetGet($key) { + return $this->getPermission($key); + } + + /** + * {@inheritdoc} + */ + public function offsetSet($key, $value) { + $this->setPermission($key, $value); + } + + /** + * {@inheritdoc} + */ + public function offsetUnset($key) { + if ($this->hasPermission($key)) { + $this->deletePermission($key); + } + } + + /** + * {@inheritdoc} + */ + public function offsetExists($key) { + return $this->hasPermission($key); + } + + /** + * {@inheritdoc} + */ + public function getIterator() { + return new \ArrayIterator($this->permissions); + } + +} diff --git a/src/Event/PermissionEventInterface.php b/src/Event/PermissionEventInterface.php new file mode 100644 index 000000000..e808186d9 --- /dev/null +++ b/src/Event/PermissionEventInterface.php @@ -0,0 +1,126 @@ +permissionManager = $permission_manager; + } + + /** + * {@inheritdoc} + */ + public static function getSubscribedEvents() { + return [ + PermissionEventInterface::EVENT_NAME => [['provideDefaultOgPermissions']], + DefaultRoleEventInterface::EVENT_NAME => [['provideDefaultRoles']], + ]; + } + + /** + * Provides default OG permissions. + * + * @param \Drupal\og\Event\PermissionEventInterface $event + * The OG permission event. + */ + public function provideDefaultOgPermissions(PermissionEventInterface $event) { + $event->setPermissions([ + 'update group' => [ + 'title' => t('Edit group'), + 'description' => t('Edit the group. Note: This permission controls only node entity type groups.'), + 'default roles' => [OgRoleInterface::ADMINISTRATOR], + ], + 'administer group' => [ + 'title' => t('Administer group'), + 'description' => t('Manage group members and content in the group.'), + 'default roles' => [OgRoleInterface::ADMINISTRATOR], + 'restrict access' => TRUE, + ], + ] + $this->permissionManager->getPermissionList($event->getEntityTypeId(), $event->getBundleId())); + } + + /** + * Provides a default role for the group administrator. + * + * @param \Drupal\og\Event\DefaultRoleEventInterface $event + * The default role event. + */ + public function provideDefaultRoles(DefaultRoleEventInterface $event) { + $event->addRole(OgRoleInterface::ADMINISTRATOR, ['label' => 'Administrator']); + } + +} diff --git a/src/Exception/OgRoleException.php b/src/Exception/OgRoleException.php new file mode 100644 index 000000000..cd150f83f --- /dev/null +++ b/src/Exception/OgRoleException.php @@ -0,0 +1,9 @@ +getGroupMap() instead. + * * @var array */ protected $groupMap; + /** + * A map of group and group content relations. + * + * Do not access this property directly, use $this->getGroupRelationMap() + * instead. + * + * @var array $groupRelationMap + * An associative array representing group and group content relations, in + * the following format: + * @code + * [ + * 'group_entity_type_id' => [ + * 'group_bundle_id' => [ + * 'group_content_entity_type_id' => [ + * 'group_content_bundle_id', + * ], + * ], + * ], + * ] + * @endcode + */ + protected $groupRelationMap = []; + /** * Constructs an GroupManager object. + * + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info + * The service providing information about bundles. + * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface + * The event dispatcher. + * @param \Drupal\Core\State\StateInterface $state + * The state service. */ - public function __construct(ConfigFactoryInterface $config_factory) { + public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EventDispatcherInterface $event_dispatcher, StateInterface $state) { $this->configFactory = $config_factory; - $this->refreshGroupMap(); + $this->ogRoleStorage = $entity_type_manager->getStorage('og_role'); + $this->entityTypeBundleInfo = $entity_type_bundle_info; + $this->eventDispatcher = $event_dispatcher; + $this->state = $state; } /** @@ -59,7 +140,8 @@ public function __construct(ConfigFactoryInterface $config_factory) { * @return bool */ public function isGroup($entity_type_id, $bundle) { - return isset($this->groupMap[$entity_type_id]) && in_array($bundle, $this->groupMap[$entity_type_id]); + $group_map = $this->getGroupMap(); + return isset($group_map[$entity_type_id]) && in_array($bundle, $group_map[$entity_type_id]); } /** @@ -68,22 +150,92 @@ public function isGroup($entity_type_id, $bundle) { * @return array */ public function getGroupsForEntityType($entity_type_id) { - return isset($this->groupMap[$entity_type_id]) ? $this->groupMap[$entity_type_id] : []; + $group_map = $this->getGroupMap(); + return isset($group_map[$entity_type_id]) ? $group_map[$entity_type_id] : []; } /** * Get all group bundles keyed by entity type. * * @return array + * An associative array, keyed by entity type, each value an indexed array + * of bundle IDs. */ public function getAllGroupBundles($entity_type = NULL) { - return !empty($this->groupMap[$entity_type]) ? $this->groupMap[$entity_type] : $this->groupMap; + $group_map = $this->getGroupMap(); + return !empty($group_map[$entity_type]) ? $group_map[$entity_type] : $group_map; + } + + /** + * Returns all group bundles that are referenced by the given group content. + * + * @param string $group_content_entity_type_id + * The entity type ID of the group content type for which to return + * associated group bundle IDs. + * @param string $group_content_bundle_id + * The bundle ID of the group content type for which to return associated + * group bundle IDs. + * + * @return array + * An array of group bundle IDs, keyed by group entity type ID. + */ + public function getGroupBundleIdsByGroupContentBundle($group_content_entity_type_id, $group_content_bundle_id) { + $bundles = []; + + foreach (OgGroupAudienceHelper::getAllGroupAudienceFields($group_content_entity_type_id, $group_content_bundle_id) as $field) { + $group_entity_type_id = $field->getSetting('target_type'); + $handler_settings = $field->getSetting('handler_settings'); + $group_bundle_ids = !empty($handler_settings['target_bundles']) ? $handler_settings['target_bundles'] : []; + + // If the group bundles are empty, it means that all bundles are + // referenced. + if (empty($group_bundle_ids)) { + $group_bundle_ids = $this->getGroupMap()[$group_entity_type_id]; + } + + foreach ($group_bundle_ids as $group_bundle_id) { + $bundles[$group_entity_type_id][$group_bundle_id] = $group_bundle_id; + } + } + + return $bundles; + } + + /** + * Returns group content bundles that are referencing the given group content. + * + * @param string $group_entity_type_id + * The entity type ID of the group type for which to return associated group + * content bundle IDs. + * @param string $group_bundle_id + * The bundle ID of the group type for which to return associated group + * content bundle IDs. + * + * @return array + * An array of group content bundle IDs, keyed by group content entity type + * ID. + */ + public function getGroupContentBundleIdsByGroupBundle($group_entity_type_id, $group_bundle_id) { + $group_relation_map = $this->getGroupRelationMap(); + return isset($group_relation_map[$group_entity_type_id][$group_bundle_id]) ? $group_relation_map[$group_entity_type_id][$group_bundle_id] : []; } /** * Sets an entity type instance as being an OG group. + * + * @param string $entity_type_id + * The entity type ID of the bundle to declare as being a group. + * @param string $bundle_id + * The bundle ID of the bundle to declare as being a group. + * + * @throws \InvalidArgumentException + * Thrown when the given bundle is already a group. */ public function addGroup($entity_type_id, $bundle_id) { + // Throw an error if the entity type is already defined as a group. + if ($this->isGroup($entity_type_id, $bundle_id)) { + throw new \InvalidArgumentException("The '$entity_type_id' of type '$bundle_id' is already a group."); + } $editable = $this->configFactory->getEditable('og.settings'); $groups = $editable->get('groups'); $groups[$entity_type_id][] = $bundle_id; @@ -91,11 +243,10 @@ public function addGroup($entity_type_id, $bundle_id) { $groups[$entity_type_id] = array_unique($groups[$entity_type_id]); $editable->set('groups', $groups); - $saved = $editable->save(); + $editable->save(); + $this->createPerBundleRoles($entity_type_id, $bundle_id); $this->refreshGroupMap(); - - return $saved; } /** @@ -112,21 +263,177 @@ public function removeGroup($entity_type_id, $bundle_id) { unset($groups[$entity_type_id][$search_key]); } + // Clean up entity types that have become empty. + $groups = array_filter($groups); + // Only update and refresh the map if a key was found and unset. $editable->set('groups', $groups); - $saved = $editable->save(); + $editable->save(); + + // Remove all roles associated with this group type. + $this->removeRoles($entity_type_id, $bundle_id); + + $this->resetGroupMap(); + } + } + + /** + * Creates the roles for the given group type, based on the default roles. + * + * This is intended to be called after a new group type has been created. + * + * @param string $entity_type_id + * The entity type ID of the group for which to create default roles. + * @param string $bundle_id + * The bundle ID of the group for which to create default roles. + * + * @todo: Would a dedicated RoleManager service be a better place for this? + */ + protected function createPerBundleRoles($entity_type_id, $bundle_id) { + foreach ($this->getDefaultRoles() as $role_name => $default_properties) { + $properties = [ + 'group_type' => $entity_type_id, + 'group_bundle' => $bundle_id, + 'id' => $role_name, + 'role_type' => OgRole::getRoleTypeByName($role_name), + ]; + + // Populate the default permissions. + $event = new PermissionEvent($entity_type_id, $bundle_id); + /** @var \Drupal\og\Event\PermissionEventInterface $permissions */ + $permissions = $this->eventDispatcher->dispatch(PermissionEventInterface::EVENT_NAME, $event); + $properties['permissions'] = array_keys($permissions->filterByDefaultRole($role_name)); + + $role = $this->ogRoleStorage->create($properties + $default_properties); + $role->save(); + } + } + + /** + * Returns the default roles. + * + * @return array + * An associative array of default role properties, keyed by role name. Each + * role property is an associative array with the following keys: + * - 'label': The human readable label. + * - 'role_type': Either OgRoleInterface::ROLE_TYPE_STANDARD or + * OgRoleInterface::ROLE_TYPE_REQUIRED. + * + * @todo: Would a dedicated RoleManager service be a better place for this? + */ + public function getDefaultRoles() { + /** @var \Drupal\og\Event\DefaultRoleEvent $default_role_event */ + $event = new DefaultRoleEvent(); + $default_role_event = $this->eventDispatcher->dispatch(DefaultRoleEventInterface::EVENT_NAME, $event); + + return OgRole::getDefaultRoles() + $default_role_event->getRoles(); + } + + /** + * Deletes the roles associated with a group type. + * + * @param string $entity_type_id + * The entity type ID of the group for which to delete the roles. + * @param string $bundle_id + * The bundle ID of the group for which to delete the roles. + * + * @todo: Would a dedicated RoleManager service be a better place for this? + */ + protected function removeRoles($entity_type_id, $bundle_id) { + $properties = [ + 'group_type' => $entity_type_id, + 'group_bundle' => $bundle_id, + ]; + foreach ($this->ogRoleStorage->loadByProperties($properties) as $role) { + $role->delete(); + } + } + + /** + * Resets all locally stored data. + */ + public function reset() { + $this->resetGroupMap(); + $this->resetGroupRelationMap(); + } + /** + * Resets the cached group map. + * + * Call this after adding or removing a group type. + */ + public function resetGroupMap() { + $this->groupMap = []; + } + + /** + * Resets the cached group relation map. + * + * Call this after making a change to the relationship between a group type + * and a group content type. + */ + public function resetGroupRelationMap() { + $this->groupRelationMap = []; + $this->state->delete(self::GROUP_RELATION_MAP_CACHE_KEY); + } + + /** + * Returns the group map. + * + * @return array + * The group map. + */ + protected function getGroupMap() { + if (empty($this->groupMap)) { $this->refreshGroupMap(); + } + return $this->groupMap; + } - return $saved; + /** + * Returns the group relation map. + * + * @return array + * The group relation map. + */ + protected function getGroupRelationMap() { + if (empty($this->groupRelationMap)) { + $this->refreshGroupRelationMap(); } + return $this->groupRelationMap; } /** * Refreshes the groupMap property with currently configured groups. */ protected function refreshGroupMap() { - $this->groupMap = $this->configFactory->get(static::SETTINGS_CONFIG_KEY)->get(static::GROUPS_CONFIG_KEY); + $group_map = $this->configFactory->get(static::SETTINGS_CONFIG_KEY)->get(static::GROUPS_CONFIG_KEY); + $this->groupMap = !empty($group_map) ? $group_map : []; + } + + /** + * Populates the map of relations between group types and group content types. + */ + protected function refreshGroupRelationMap() { + // Retrieve a cached version of the map if it exists. + if ($group_relation_map = $this->state->get(self::GROUP_RELATION_MAP_CACHE_KEY)) { + $this->groupRelationMap = $group_relation_map; + return; + } + + $this->groupRelationMap = []; + + foreach ($this->entityTypeBundleInfo->getAllBundleInfo() as $group_content_entity_type_id => $bundles) { + foreach ($bundles as $group_content_bundle_id => $bundle_info) { + foreach ($this->getGroupBundleIdsByGroupContentBundle($group_content_entity_type_id, $group_content_bundle_id) as $group_entity_type_id => $group_bundle_ids) { + foreach ($group_bundle_ids as $group_bundle_id) { + $this->groupRelationMap[$group_entity_type_id][$group_bundle_id][$group_content_entity_type_id][$group_content_bundle_id] = $group_content_bundle_id; + } + } + } + } + // Cache the map. + $this->state->set(self::GROUP_RELATION_MAP_CACHE_KEY, $this->groupRelationMap); } } diff --git a/src/Og.php b/src/Og.php index 273facb41..441362e67 100644 --- a/src/Og.php +++ b/src/Og.php @@ -7,10 +7,12 @@ namespace Drupal\og; -use Drupal\Component\Render\FormattableMarkup; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Entity\Display\EntityDisplayInterface; +use Drupal\Core\Entity\Display\EntityFormDisplayInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; +use Drupal\Core\Session\AccountInterface; use Drupal\field\Entity\FieldConfig; use Drupal\field\Entity\FieldStorageConfig; use Drupal\og\Plugin\EntityReferenceSelection\OgSelection; @@ -21,11 +23,11 @@ class Og { /** - * Static cache for groups per entity. + * Static cache for heavy queries. * * @var array */ - protected static $entityGroupCache = []; + protected static $cache = []; /** * Create an organic groups field in a bundle. @@ -44,6 +46,10 @@ class Og { * config definitions. Values should comply with FieldStorageConfig::create() * - field_config: Array with values to override the field config * definitions. Values should comply with FieldConfig::create() + * - form_display: Array with values to override the form display + * definitions. + * - view_display: Array with values to override the view display + * definitions. * * @return \Drupal\Core\Field\FieldConfigInterface * The created or existing field config. @@ -52,13 +58,15 @@ public static function createField($plugin_id, $entity_type, $bundle, array $set $settings = $settings + [ 'field_storage_config' => [], 'field_config' => [], + 'form_display' => [], + 'view_display' => [], ]; $field_name = !empty($settings['field_name']) ? $settings['field_name'] : $plugin_id; // Get the field definition and add the entity info to it. By doing so // we validate the the field can be attached to the entity. For example, - // the OG accesss module's field can be attached only to node entities, so + // the OG access module's field can be attached only to node entities, so // any other entity will throw an exception. /** @var \Drupal\og\OgFieldBase $og_field */ $og_field = static::getFieldBaseDefinition($plugin_id) @@ -67,13 +75,12 @@ public static function createField($plugin_id, $entity_type, $bundle, array $set ->setEntityType($entity_type); if (!FieldStorageConfig::loadByName($entity_type, $field_name)) { - $field_storage_config = NestedArray::mergeDeep($og_field->getFieldStorageConfigBaseDefinition(), $settings['field_storage_config']); + $field_storage_config = NestedArray::mergeDeep($og_field->getFieldStorageBaseDefinition(), $settings['field_storage_config']); FieldStorageConfig::create($field_storage_config)->save(); } - if (!$field_definition = FieldConfig::loadByName($entity_type, $bundle, $field_name)) { - $field_config = NestedArray::mergeDeep($og_field->getFieldConfigBaseDefinition(), $settings['field_config']); + $field_config = NestedArray::mergeDeep($og_field->getFieldBaseDefinition(), $settings['field_config']); $field_definition = FieldConfig::create($field_config); $field_definition->save(); @@ -82,54 +89,153 @@ public static function createField($plugin_id, $entity_type, $bundle, array $set static::invalidateCache(); } + // Make the field visible in the default form display. + /** @var EntityFormDisplayInterface $form_display */ + $form_display = \Drupal::entityTypeManager()->getStorage('entity_form_display')->load("$entity_type.$bundle.default"); + + // If not found, create a fresh form display object. This is by design, + // configuration entries are only created when an entity form display is + // explicitly configured and saved. + if (!$form_display) { + $form_display = \Drupal::entityTypeManager()->getStorage('entity_form_display')->create([ + 'targetEntityType' => $entity_type, + 'bundle' => $bundle, + 'mode' => 'default', + 'status' => TRUE, + ]); + } + + $form_display_definition = $og_field->getFormDisplayDefinition($settings['form_display']); + + + $form_display->setComponent($plugin_id, $form_display_definition); + $form_display->save(); + + + // Set the view display for the "default" view display. + $view_display_definition = $og_field->getViewDisplayDefinition($settings['view_display']); + + /** @var EntityDisplayInterface $view_display */ + $view_display = \Drupal::entityTypeManager()->getStorage('entity_view_display')->load("$entity_type.$bundle.default"); + + if (!$view_display) { + $view_display = \Drupal::entityTypeManager()->getStorage('entity_view_display')->create([ + 'targetEntityType' => $entity_type, + 'bundle' => $bundle, + 'mode' => 'default', + 'status' => TRUE, + ]); + } + + $view_display->setComponent($plugin_id, $view_display_definition); + $view_display->save(); + + // Refresh the group manager data, we have added a group type. + static::groupManager()->resetGroupRelationMap(); + return $field_definition; } /** - * Gets the groups an entity is associated with. + * Returns all group IDs associated with the given user. * - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to get groups for. - * @param $states + * This is similar to \Drupal\og\Og::getGroupIds() but for users. The reason + * there is a separate method for user entities is because the storage is + * handled differently. For group content the relation to the group is stored + * on a field attached to the content entity, while user memberships are + * tracked in OgMembership entities. + * + * @param \Drupal\Core\Session\AccountInterface $user + * The user to get groups for. + * @param array $states * (optional) Array with the state to return. Defaults to active. - * @param $field_name + * @param string $field_name * (optional) The field name associated with the group. * * @return array - * An array with the group's entity type as the key, and array - keyed by - * the OG membership ID and the group ID as the value. If nothing found, - * then an empty array. + * An associative array, keyed by group entity type, each item an array of + * group entity IDs. + * + * @see \Drupal\og\Og::getGroupIds() */ - public static function getEntityGroups(EntityInterface $entity, array $states = [OgMembershipInterface::STATE_ACTIVE], $field_name = NULL) { - $entity_type_id = $entity->getEntityTypeId(); - $entity_id = $entity->id(); + public static function getUserGroupIds(AccountInterface $user, array $states = [OgMembershipInterface::STATE_ACTIVE], $field_name = NULL) { + $group_ids = []; - // Get a string identifier of the states, so we can retrieve it from cache. - if ($states) { - sort($states); - $state_identifier = implode(':', $states); + /** @var \Drupal\og\Entity\OgMembership[] $memberships */ + $memberships = static::getUserMemberships($user, $states, $field_name); + foreach ($memberships as $membership) { + $group_ids[$membership->getGroupEntityType()][] = $membership->getEntityId(); } - else { - $state_identifier = FALSE; + + return $group_ids; + } + + /** + * Returns all groups associated with the given user. + * + * This is similar to \Drupal\og\Og::getGroups() but for users. The reason + * there is a separate method for user entities is because the storage is + * handled differently. For group content the relation to the group is stored + * on a field attached to the content entity, while user memberships are + * tracked in OgMembership entities. + * + * @param \Drupal\Core\Session\AccountInterface $user + * The user to get groups for. + * @param array $states + * (optional) Array with the states to return. Defaults to active. + * @param string $field_name + * (optional) The field name associated with the group. + * + * @return \Drupal\Core\Entity\EntityInterface[][] + * An associative array, keyed by group entity type, each item an array of + * group entities. + * + * @see \Drupal\og\Og::getGroups() + * @see \Drupal\og\Og::getUserMemberships() + */ + public static function getUserGroups(AccountInterface $user, array $states = [OgMembershipInterface::STATE_ACTIVE], $field_name = NULL) { + $groups = []; + + foreach (static::getUserGroupIds($user, $states, $field_name) as $entity_type => $entity_ids) { + $groups[$entity_type] = \Drupal::entityTypeManager()->getStorage($entity_type)->loadMultiple($entity_ids); } + return $groups; + } + + /** + * Returns the group memberships a user is associated with. + * + * @param \Drupal\Core\Session\AccountInterface $user + * The user to get groups for. + * @param array $states + * (optional) Array with the state to return. Defaults to active. + * @param string $field_name + * (optional) The field name associated with the group. + * + * @return \Drupal\og\Entity\OgMembership[] + * An array of OgMembership entities, keyed by ID. + */ + public static function getUserMemberships(AccountInterface $user, array $states = [OgMembershipInterface::STATE_ACTIVE], $field_name = NULL) { + // Get a string identifier of the states, so we can retrieve it from cache. + sort($states); + $states_identifier = implode('|', array_unique($states)); + $identifier = [ - $entity_type_id, - $entity_id, - $state_identifier, + __METHOD__, + $user->id(), + $states_identifier, $field_name, ]; - $identifier = implode(':', $identifier); - if (isset(static::$entityGroupCache[$identifier])) { - // Return cached values. - return static::$entityGroupCache[$identifier]; + + // Return cached result if it exists. + if (isset(static::$cache[$identifier])) { + return static::$cache[$identifier]; } - static::$entityGroupCache[$identifier] = []; $query = \Drupal::entityQuery('og_membership') - ->condition('member_entity_type', $entity_type_id) - ->condition('member_entity_id', $entity_id); + ->condition('uid', $user->id()); if ($states) { $query->condition('state', $states, 'IN'); @@ -142,25 +248,241 @@ public static function getEntityGroups(EntityInterface $entity, array $states = $results = $query->execute(); /** @var \Drupal\og\Entity\OgMembership[] $memberships */ - $memberships = \Drupal::entityTypeManager() + static::$cache[$identifier] = \Drupal::entityTypeManager() ->getStorage('og_membership') ->loadMultiple($results); - /** @var \Drupal\og\Entity\OgMembership $membership */ - foreach ($memberships as $membership) { - static::$entityGroupCache[$identifier][$membership->getGroupEntityType()][$membership->id()] = $membership->getGroup(); + return static::$cache[$identifier]; + } + + /** + * Returns the group membership for a given user and group. + * + * @param \Drupal\Core\Session\AccountInterface $user + * The user to get the membership for. + * @param \Drupal\Core\Entity\EntityInterface $group + * The group to get the membership for. + * @param array $states + * (optional) Array with the state to return. Defaults to active. + * @param string $field_name + * (optional) The field name associated with the group. + * + * @return \Drupal\og\Entity\OgMembership|NULL + * The OgMembership entity, or NULL if the user is not a member of the + * group. + */ + public static function getMembership(AccountInterface $user, EntityInterface $group, array $states = [OgMembershipInterface::STATE_ACTIVE], $field_name = NULL) { + foreach (static::getUserMemberships($user, $states, $field_name) as $membership) { + if ($membership->getGroupEntityType() === $group->getEntityTypeId() && $membership->getEntityId() === $group->id()) { + return $membership; + } + } + } + + /** + * Returns all group IDs associated with the given group content entity. + * + * Do not use this to retrieve group IDs associated with a user entity. Use + * Og::getUserGroups() instead. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The group content entity for which to return the associated groups. + * @param string $group_type_id + * Filter results to only include group IDs of this entity type. + * @param string $group_bundle + * Filter list to only include group IDs with this bundle. + * + * @return array + * An associative array, keyed by group entity type, each item an array of + * group entity IDs. + * + * @throws \InvalidArgumentException + * Thrown when a user entity is passed in. + * + * @see \Drupal\og\Og::getUserGroups() + */ + public static function getGroupIds(EntityInterface $entity, $group_type_id = NULL, $group_bundle = NULL) { + // This does not work for user entities. + if ($entity->getEntityTypeId() === 'user') { + throw new \InvalidArgumentException('\Drupal\og\Og::getGroupIds() cannot be used for user entities. Use \Drupal\og\Og::getUserGroups() instead.'); + } + + $identifier = [ + __METHOD__, + $entity->id(), + $group_type_id, + $group_bundle, + ]; + + $identifier = implode(':', $identifier); + + if (isset(static::$cache[$identifier])) { + // Return cached values. + return static::$cache[$identifier]; } - return static::$entityGroupCache[$identifier]; + $group_ids = []; + + $fields = OgGroupAudienceHelper::getAllGroupAudienceFields($entity->getEntityTypeId(), $entity->bundle(), $group_type_id, $group_bundle); + foreach ($fields as $field) { + $target_type = $field->getFieldStorageDefinition()->getSetting('target_type'); + + // Optionally filter by group type. + if (!empty($group_type_id) && $group_type_id !== $target_type) { + continue; + } + + // Compile a list of group target IDs. + $target_ids = array_map(function ($value) { + return $value['target_id']; + }, $entity->get($field->getName())->getValue()); + + if (empty($target_ids)) { + continue; + } + + // Query the database to get the actual list of groups. The target IDs may + // contain groups that no longer exist. Entity reference doesn't clean up + // orphaned target IDs. + $entity_type = \Drupal::entityTypeManager()->getDefinition($target_type); + $query = \Drupal::entityQuery($target_type) + ->condition($entity_type->getKey('id'), $target_ids, 'IN'); + + // Optionally filter by group bundle. + if (!empty($group_bundle)) { + $query->condition($entity_type->getKey('bundle'), $group_bundle); + } + + $group_ids = NestedArray::mergeDeep($group_ids, [$target_type => $query->execute()]); + } + + static::$cache[$identifier] = $group_ids; + + return $group_ids; + } + + /** + * Returns all groups that are associated with the given group content entity. + * + * Do not use this to retrieve group memberships for a user entity. Use + * Og::getUserGroups() instead. + * + * The reason there are separate method for group content and user entities is + * because the storage is handled differently. For group content the relation + * to the group is stored on a field attached to the content entity, while + * user memberships are tracked in OgMembership entities. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The group content entity for which to return the groups. + * @param string $group_type_id + * Filter results to only include groups of this entity type. + * @param string $group_bundle + * Filter results to only include groups of this bundle. + * + * @return \Drupal\Core\Entity\EntityInterface[][] + * An associative array, keyed by group entity type, each item an array of + * group entities. + * + * @see \Drupal\og\Og::getUserGroups() + */ + public static function getGroups(EntityInterface $entity, $group_type_id = NULL, $group_bundle = NULL) { + $groups = []; + + foreach (static::getGroupIds($entity, $group_type_id, $group_bundle) as $entity_type => $entity_ids) { + $groups[$entity_type] = \Drupal::entityTypeManager()->getStorage($entity_type)->loadMultiple($entity_ids); + } + + return $groups; + } + + /** + * Returns the number of groups associated with a given group content entity. + * + * Do not use this to retrieve the group membership count for a user entity. + * Use count(Og::GetEntityGroups()) instead. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The group content entity for which to count the associated groups. + * @param string $group_type_id + * Only count groups of this entity type. + * @param string $group_bundle + * Only count groups of this bundle. + * + * @return int + * The number of associated groups. + */ + public static function getGroupCount(EntityInterface $entity, $group_type_id = NULL, $group_bundle = NULL) { + return array_reduce(static::getGroupIds($entity, $group_type_id, $group_bundle), function ($carry, $item) { + return $carry + count($item); + }, 0); } /** - * Return whether a group content belongs to a group. + * Returns all the group content IDs associated with a given group entity. + * + * This does not return information about users that are members of the given + * group. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The group entity for which to return group content IDs. + * @param array $entity_types + * Optional list of group content entity types for which to return results. + * If an empty array is passed, the group content is not filtered. Defaults + * to an empty array. + * + * @return array + * An associative array, keyed by group content entity type, each item an + * array of group content entity IDs. + */ + public static function getGroupContentIds(EntityInterface $entity, array $entity_types = []) { + $group_content = []; + + + // Retrieve the fields which reference our entity type and bundle. + $query = \Drupal::entityQuery('field_storage_config') + ->condition('type', OgGroupAudienceHelper::NON_USER_TO_GROUP_REFERENCE_FIELD_TYPE); + + // Optionally filter group content entity types. + if ($entity_types) { + $query->condition('entity_type', $entity_types, 'IN'); + } + + /** @var \Drupal\field\FieldStorageConfigInterface[] $fields */ + $fields = array_filter(FieldStorageConfig::loadMultiple($query->execute()), function ($field) use ($entity) { + /** @var \Drupal\field\FieldStorageConfigInterface $field */ + $type_matches = $field->getSetting('target_type') === $entity->getEntityTypeId(); + // If the list of target bundles is empty, it targets all bundles. + $bundle_matches = empty($field->getSetting('target_bundles')) || in_array($entity->bundle(), $field->getSetting('target_bundles')); + return $type_matches && $bundle_matches; + }); + + // Compile the group content. + foreach ($fields as $field) { + $group_content_entity_type = $field->getTargetEntityTypeId(); + + // Group the group content per entity type. + if (!isset($group_content[$group_content_entity_type])) { + $group_content[$group_content_entity_type] = []; + } + + // Query all group content that references the group through this field. + $results = \Drupal::entityQuery($group_content_entity_type) + ->condition($field->getName() . '.target_id', $entity->id()) + ->execute(); + + $group_content[$group_content_entity_type] = array_merge($group_content[$group_content_entity_type], $results); + } + + return $group_content; + } + + /** + * Returns whether a user belongs to a group. * * @param \Drupal\Core\Entity\EntityInterface $group * The group entity. - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity to test the membership for. + * @param \Drupal\Core\Session\AccountInterface $user + * The user to test the membership for. * @param array $states * (optional) Array with the membership states to check the membership. * Defaults to active memberships. @@ -169,31 +491,27 @@ public static function getEntityGroups(EntityInterface $entity, array $states = * TRUE if the entity (e.g. the user or node) belongs to a group with * a certain state. */ - public static function isMember(EntityInterface $group, EntityInterface $entity, $states = [OgMembershipInterface::STATE_ACTIVE]) { - $groups = static::getEntityGroups($entity, $states); - $group_entity_type_id = $group->getEntityTypeId(); - // We need to create a map of the group ids as Og::getEntityGroups returns a - // map of membership_id => group entity for each type. - return !empty($groups[$group_entity_type_id]) && in_array($group->id(), array_map(function($group_entity) { - return $group_entity->id(); - }, $groups[$group_entity_type_id])); + public static function isMember(EntityInterface $group, AccountInterface $user, $states = [OgMembershipInterface::STATE_ACTIVE]) { + $group_ids = static::getUserGroupIds($user, $states); + $entity_type_id = $group->getEntityTypeId(); + return !empty($group_ids[$entity_type_id]) && in_array($group->id(), $group_ids[$entity_type_id]); } /** - * Returns whether an entity belongs to a group with a pending status. + * Returns whether a user belongs to a group with a pending status. * * @param \Drupal\Core\Entity\EntityInterface $group * The group entity. - * @param \Drupal\Core\Entity\EntityInterface $entity - * The group content entity. + * @param \Drupal\Core\Session\AccountInterface $user + * The user entity. * * @return bool * True if the membership is pending. * * @see \Drupal\og\Og::isMember */ - public static function isMemberPending(EntityInterface $group, EntityInterface $entity) { - return static::isMember($group, $entity, [OgMembershipInterface::STATE_PENDING]); + public static function isMemberPending(EntityInterface $group, AccountInterface $user) { + return static::isMember($group, $user, [OgMembershipInterface::STATE_PENDING]); } /** @@ -242,7 +560,7 @@ public static function isGroup($entity_type_id, $bundle_id) { * True or false if the given entity is group content. */ public static function isGroupContent($entity_type_id, $bundle_id) { - return (bool) static::getAllGroupAudienceFields($entity_type_id, $bundle_id); + return (bool) OgGroupAudienceHelper::getAllGroupAudienceFields($entity_type_id, $bundle_id); } /** @@ -252,12 +570,9 @@ public static function isGroupContent($entity_type_id, $bundle_id) { * The entity type. * @param string $bundle_id * The bundle name. - * - * @return bool - * True or false if the action succeeded. */ public static function addGroup($entity_type_id, $bundle_id) { - return static::groupManager()->addGroup($entity_type_id, $bundle_id); + static::groupManager()->addGroup($entity_type_id, $bundle_id); } /** @@ -275,65 +590,6 @@ public static function removeGroup($entity_type_id, $bundle_id) { return static::groupManager()->removeGroup($entity_type_id, $bundle_id); } - /** - * Return TRUE if field is a group audience type. - * - * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition - * The field definition object. - * - * @return bool - * TRUE if the field is a group audience type, FALSE otherwise. - */ - public static function isGroupAudienceField(FieldDefinitionInterface $field_definition) { - return $field_definition->getType() === 'og_membership_reference'; - } - - /** - * Return all the group audience fields of a certain bundle. - * - * @param string $entity_type_id - * The entity type. - * @param string $bundle - * The bundle name to be checked. - * @param string $group_type_id - * Filter list to only include fields referencing a specific group type. - * @param string $group_bundle - * Filter list to only include fields referencing a specific group bundle. - * Fields that do not specify any bundle restrictions at all are also - * included. - * - * @return \Drupal\Core\Field\FieldDefinitionInterface[] - * An array of field definitions, keyed by field name; Or an empty array if - * none found. - */ - public static function getAllGroupAudienceFields($entity_type_id, $bundle, $group_type_id = NULL, $group_bundle = NULL) { - $return = []; - - foreach (\Drupal::entityManager()->getFieldDefinitions($entity_type_id, $bundle) as $field_definition) { - if (!static::isGroupAudienceField($field_definition)) { - // Not a group audience field. - continue; - } - - $target_type = $field_definition->getFieldStorageDefinition()->getSetting('target_type'); - - if (isset($group_type_id) && $target_type != $group_type_id) { - // Field doesn't reference this group type. - continue; - } - - $handler_settings = $field_definition->getSetting('handler_settings'); - - if (isset($group_bundle) && !empty($handler_settings['target_bundles']) && !in_array($group_bundle, $handler_settings['target_bundles'])) { - continue; - } - - $field_name = $field_definition->getName(); - $return[$field_name] = $field_definition; - } - - return $return; - } /** * Returns the group manager instance. @@ -380,11 +636,11 @@ public static function invalidateCache($group_ids = array()) { } // @todo Consider using a reset() method. - static::$entityGroupCache = []; + static::$cache = []; // Invalidate the entity property cache. \Drupal::entityTypeManager()->clearCachedDefinitions(); - \Drupal::entityManager()->clearCachedFieldDefinitions(); + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); // Let other OG modules know we invalidate cache. \Drupal::moduleHandler()->invokeAll('og_invalidate_cache', $group_ids); @@ -406,7 +662,6 @@ public static function membershipDefault() { return ['type' => OgMembershipInterface::TYPE_DEFAULT]; } - /** * Get an OG field base definition. * @@ -440,7 +695,7 @@ protected static function getFieldBaseDefinition($plugin_id) { * @throws \Exception */ public static function getSelectionHandler(FieldDefinitionInterface $field_definition, array $options = []) { - if (!static::isGroupAudienceField($field_definition)) { + if (!OgGroupAudienceHelper::isGroupAudienceField($field_definition)) { $field_name = $field_definition->getName(); throw new \Exception("The field $field_name is not an audience field."); } @@ -459,4 +714,11 @@ public static function getSelectionHandler(FieldDefinitionInterface $field_defin return \Drupal::service('plugin.manager.entity_reference_selection')->createInstance('og:default', $options); } + /** + * Resets the static cache. + */ + public static function reset() { + static::$cache = []; + } + } diff --git a/src/OgAccess.php b/src/OgAccess.php index b30858f00..94db29449 100644 --- a/src/OgAccess.php +++ b/src/OgAccess.php @@ -10,11 +10,25 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\RefinableCacheableDependencyInterface; -use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Session\AccountProxyInterface; use Drupal\user\EntityOwnerInterface; +use Drupal\user\RoleInterface; + +/** + * The service that determines if users have access to groups and group content. + */ +class OgAccess implements OgAccessInterface { -class OgAccess { + /** + * Administer permission string. + * + * @var string + */ + const ADMINISTER_GROUP_PERMISSION = 'administer group'; /** * Static cache that contains cache permissions. @@ -24,49 +38,54 @@ class OgAccess { * - alter: The permissions after altered by implementing modules. * - pre_alter: The pre-altered permissions, as read from the config. */ - protected static $permissionsCache = ['pre_alter' => [], 'post_alter' => []]; - + protected $permissionsCache = []; /** - * Administer permission string. + * The config factory. * - * @var string + * @var \Drupal\Core\Config\ConfigFactoryInterface */ - const ADMINISTER_GROUP_PERMISSION = 'administer group'; + protected $configFactory; /** - * Determines whether a user has a given privilege. + * The service that contains the current active user. * - * All permission checks in OG should go through this function. This - * way, we guarantee consistent behavior, and ensure that the superuser - * and group administrators can perform all actions. + * @var \Drupal\Core\Session\AccountProxyInterface + */ + protected $accountProxy; + + /** + * The module handler. * - * @param \Drupal\Core\Entity\EntityInterface $group - * The group entity. - * @param string $operation - * The entity operation being checked for. - * @param \Drupal\Core\Session\AccountInterface $user - * (optional) The user to check. Defaults to the current user. - * @param $skip_alter - * (optional) If TRUE then user access will not be sent to other modules - * using drupal_alter(). This can be used by modules implementing - * hook_og_user_access_alter() that still want to use og_user_access(), but - * without causing a recursion. Defaults to FALSE. - * @param $ignore_admin - * (optional) When TRUE the specific permission is checked, ignoring the - * "administer group" permission if the user has it. When FALSE, a user - * with "administer group" will be granted all permissions. - * Defaults to FALSE. + * @var \Drupal\Core\Extension\ModuleHandlerInterface + */ + protected $moduleHandler; + + /** + * Constructs an OgManager service. * - * @return \Drupal\Core\Access\AccessResult - * An access result object. + * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory + * The config factory. + * @param \Drupal\Core\Session\AccountProxyInterface $account_proxy + * The service that contains the current active user. + * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler + * The module handler. + */ + public function __construct(ConfigFactoryInterface $config_factory, AccountProxyInterface $account_proxy, ModuleHandlerInterface $module_handler) { + $this->configFactory = $config_factory; + $this->accountProxy = $account_proxy; + $this->moduleHandler = $module_handler; + } + + /** + * {@inheritdoc} */ - public static function userAccess(EntityInterface $group, $operation, AccountInterface $user = NULL, $skip_alter = FALSE, $ignore_admin = FALSE) { + public function userAccess(EntityInterface $group, $operation, AccountInterface $user = NULL, $skip_alter = FALSE, $ignore_admin = FALSE) { $group_type_id = $group->getEntityTypeId(); $bundle = $group->bundle(); // As Og::isGroup depends on this config, we retrieve it here and set it as // the minimal caching data. - $config = \Drupal::config('og.settings'); + $config = $this->configFactory->get('og.settings'); $cacheable_metadata = (new CacheableMetadata) ->addCacheableDependency($config); if (!Og::isGroup($group_type_id, $bundle)) { @@ -75,12 +94,12 @@ public static function userAccess(EntityInterface $group, $operation, AccountInt } if (!isset($user)) { - $user = \Drupal::currentUser()->getAccount(); + $user = $this->accountProxy->getAccount(); } // From this point on, every result also depends on the user so check // whether it is the current. See https://www.drupal.org/node/2628870 - if ($user->id() == \Drupal::currentUser()->id()) { + if ($user->id() == $this->accountProxy->id()) { $cacheable_metadata->addCacheContexts(['user']); } @@ -91,7 +110,7 @@ public static function userAccess(EntityInterface $group, $operation, AccountInt // Administer group permission. if (!$ignore_admin) { - $user_access = AccessResult::allowedIfHasPermission($user, static::ADMINISTER_GROUP_PERMISSION); + $user_access = AccessResult::allowedIfHasPermission($user, self::ADMINISTER_GROUP_PERMISSION); if ($user_access->isAllowed()) { return $user_access->addCacheableDependency($cacheable_metadata); } @@ -105,37 +124,52 @@ public static function userAccess(EntityInterface $group, $operation, AccountInt } } - $pre_alter_cache = static::getPermissionsCache($group, $user, TRUE); - $post_alter_cache = static::getPermissionsCache($group, $user, FALSE); + $pre_alter_cache = $this->getPermissionsCache($group, $user, TRUE); + $post_alter_cache = $this->getPermissionsCache($group, $user, FALSE); - // To reduce the number of SQL queries, we cache the user's permissions - // in a static variable. + // To reduce the number of SQL queries, we cache the user's permissions. if (!$pre_alter_cache) { - $permissions = array(); + $permissions = []; + $user_is_group_admin = FALSE; + + if ($membership = Og::getMembership($user, $group)) { + foreach ($membership->getRoles() as $role) { + // Check for the is_admin flag. + if ($role->isAdmin()) { + $user_is_group_admin = TRUE; + break; + } + + /** @var $role RoleInterface */ + $permissions = array_merge($permissions, $role->getPermissions()); + } + } - // @todo: Getting permissions from OG Roles will be added here. + $permissions = array_unique($permissions); - static::setPermissionCache($group, $user, TRUE, $permissions, $cacheable_metadata); + $this->setPermissionCache($group, $user, TRUE, $permissions, $user_is_group_admin, $cacheable_metadata); } - if (!$skip_alter && !isset($post_alter_cache[$operation])) { + if (!$skip_alter && !in_array($operation, $post_alter_cache)) { // Let modules alter the permissions. So we get the original ones, and // pass them along to the implementing modules. - $alterable_permissions = static::getPermissionsCache($group, $user, TRUE); + $alterable_permissions = $this->getPermissionsCache($group, $user, TRUE); + $context = array( 'operation' => $operation, 'group' => $group, 'user' => $user, ); - \Drupal::moduleHandler()->alter('og_user_access', $alterable_permissions, $cacheable_metadata, $context); + $this->moduleHandler->alter('og_user_access', $alterable_permissions['permissions'], $cacheable_metadata, $context); - static::setPermissionCache($group, $user, FALSE, $alterable_permissions, $cacheable_metadata); + $this->setPermissionCache($group, $user, FALSE, $alterable_permissions['permissions'], $alterable_permissions['is_admin'], $cacheable_metadata); } - $altered_permissions = static::getPermissionsCache($group, $user, FALSE); + $altered_permissions = $this->getPermissionsCache($group, $user, FALSE); + + $user_is_group_admin = !empty($altered_permissions['is_admin']); - $user_is_group_admin = !empty($altered_permissions['permissions'][static::ADMINISTER_GROUP_PERMISSION]); - if (($user_is_group_admin && !$ignore_admin) || !empty($altered_permissions['permissions'][$operation])) { + if (($user_is_group_admin && !$ignore_admin) || in_array($operation, $altered_permissions['permissions'])) { // User is a group admin, and we do not ignore this special permission // that grants access to all the group permissions. return AccessResult::allowed()->addCacheableDependency($altered_permissions['cacheable_metadata']); @@ -145,18 +179,9 @@ public static function userAccess(EntityInterface $group, $operation, AccountInt } /** - * Check if a user has access to a permission on a certain entity context. - * - * @param string $operation - * @param \Drupal\Core\Entity\EntityInterface $entity - * The entity object. - * @param \Drupal\Core\Session\AccountInterface $user - * (optional) The user object. If empty the current user will be used. - * - * @return \Drupal\Core\Access\AccessResult - * An access result object. + * {@inheritdoc} */ - public static function userAccessEntity($operation, EntityInterface $entity, AccountInterface $user = NULL) { + public function userAccessEntity($operation, EntityInterface $entity, AccountInterface $user = NULL) { $result = AccessResult::neutral(); // Entity isn't saved yet. @@ -169,7 +194,7 @@ public static function userAccessEntity($operation, EntityInterface $entity, Acc $bundle = $entity->bundle(); if (Og::isGroup($entity_type_id, $bundle)) { - $user_access = static::userAccess($entity, $operation, $user); + $user_access = $this->userAccess($entity, $operation, $user); if ($user_access->isAllowed()) { return $user_access; } @@ -186,17 +211,20 @@ public static function userAccessEntity($operation, EntityInterface $entity, Acc // @TODO: add caching on Og::isGroupContent. $is_group_content = Og::isGroupContent($entity_type_id, $bundle); $cache_tags = $entity_type->getListCacheTags(); - if ($is_group_content && $entity_groups = Og::getEntityGroups($entity)) { + + // The entity might be a user or a non-user entity. + $groups = $entity->getEntityTypeId() == 'user' ? Og::getUserGroups($entity) : Og::getGroups($entity); + + if ($is_group_content && $groups) { $forbidden = AccessResult::forbidden()->addCacheTags($cache_tags); - foreach ($entity_groups as $groups) { - foreach ($groups as $group) { - $user_access = static::userAccess($group, $operation, $user); + foreach ($groups as $entity_groups) { + foreach ($entity_groups as $group) { + $user_access = $this->userAccess($group, $operation, $user); if ($user_access->isAllowed()) { return $user_access->addCacheTags($cache_tags); } - else { - $forbidden->inheritCacheability($user_access); - } + + $forbidden->inheritCacheability($user_access); } } return $forbidden; @@ -221,16 +249,19 @@ public static function userAccessEntity($operation, EntityInterface $entity, Acc * Determines if the type of permissions is pre-alter or post-alter. * @param array $permissions * Array of permissions to set. + * @param bool @is_admin + * Whether or not the user is a group administrator. * @param \Drupal\Core\Cache\RefinableCacheableDependencyInterface $cacheable_metadata * A cacheable metadata object. */ - protected static function setPermissionCache(EntityInterface $group, AccountInterface $user, $pre_alter, array $permissions, RefinableCacheableDependencyInterface $cacheable_metadata) { + protected function setPermissionCache(EntityInterface $group, AccountInterface $user, $pre_alter, array $permissions, $is_admin, RefinableCacheableDependencyInterface $cacheable_metadata) { $entity_type_id = $group->getEntityTypeId(); $group_id = $group->id(); $user_id = $user->id(); $type = $pre_alter ? 'pre_alter' : 'post_alter'; - static::$permissionsCache[$entity_type_id][$group_id][$user_id][$type] = [ + $this->permissionsCache[$entity_type_id][$group_id][$user_id][$type] = [ + 'is_admin' => $is_admin, 'permissions' => $permissions, 'cacheable_metadata' => $cacheable_metadata, ]; @@ -249,22 +280,22 @@ protected static function setPermissionCache(EntityInterface $group, AccountInte * @return array * Array of permissions if cached, or an empty array. */ - protected static function getPermissionsCache(EntityInterface $group, AccountInterface $user, $pre_alter) { + protected function getPermissionsCache(EntityInterface $group, AccountInterface $user, $pre_alter) { $entity_type_id = $group->getEntityTypeId(); $group_id = $group->id(); $user_id = $user->id(); $type = $pre_alter ? 'pre_alter' : 'post_alter'; - return isset(static::$permissionsCache[$entity_type_id][$group_id][$user_id][$type]) ? - static::$permissionsCache[$entity_type_id][$group_id][$user_id][$type] : + return isset($this->permissionsCache[$entity_type_id][$group_id][$user_id][$type]) ? + $this->permissionsCache[$entity_type_id][$group_id][$user_id][$type] : []; } /** - * Resets the static cache. + * {@inheritdoc} */ - public static function reset() { - static::$permissionsCache = ['pre_alter' => [], 'post_alter' => []]; + public function reset() { + $this->permissionsCache = []; } } diff --git a/src/OgAccessInterface.php b/src/OgAccessInterface.php new file mode 100644 index 000000000..cc6e042d2 --- /dev/null +++ b/src/OgAccessInterface.php @@ -0,0 +1,61 @@ +entityTypeManager = $entity_type_manager; + $this->queueFactory = $queue_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('queue') + ); + } + + /** + * {@inheritdoc} + */ + public function register(EntityInterface $entity) { + foreach ($this->query($entity) as $entity_type => $orphans) { + foreach ($orphans as $orphan) { + $this->getQueue()->createItem([ + 'type' => $entity_type, + 'id'=> $orphan, + ]); + } + } + } + + /** + * Queries the registered group entity for orphaned members to delete. + * + * @param \Drupal\Core\Entity\EntityInterface $entity + * The group entity that is the basis for the query. + * + * @return array + * An associative array, keyed by entity type, each item an array of entity + * IDs to delete. + */ + protected function query(EntityInterface $entity) { + // Register orphaned group content. + $orphans = Og::getGroupContentIds($entity); + + // Register orphaned user memberships. + $membership_ids = \Drupal::entityQuery('og_membership') + ->condition('entity_type', $entity->getEntityTypeId()) + ->condition('entity_id', $entity->id()) + ->execute(); + + if (!empty($membership_ids)) { + $orphans['og_membership'] = $membership_ids; + } + + return $orphans; + } + + /** + * Deletes an orphaned group content entity if it is fully orphaned. + * + * @param string $entity_type + * The group content entity type. + * @param string $entity_id + * The group content entity ID. + */ + protected function deleteOrphan($entity_type, $entity_id) { + $entity = $this->entityTypeManager->getStorage($entity_type)->load($entity_id); + // Only delete content that is fully orphaned, i.e. it is no longer + // associated with any groups. + $group_count = Og::getGroupCount($entity); + if ($group_count == 0) { + $entity->delete(); + } + } + + /** + * Returns the queue of orphans to delete. + * + * @return \Drupal\Core\Queue\QueueInterface + * The queue. + */ + protected function getQueue() { + return $this->queueFactory->get('og_orphaned_group_content', TRUE); + } + + /** + * {@inheritdoc} + */ + public function configurationForm($form, FormStateInterface $form_state) { + return []; + } + +} diff --git a/src/OgDeleteOrphansInterface.php b/src/OgDeleteOrphansInterface.php new file mode 100644 index 000000000..0f8eb432b --- /dev/null +++ b/src/OgDeleteOrphansInterface.php @@ -0,0 +1,51 @@ +setCacheBackend($cache_backend, 'og_delete_orphans'); + $this->alterInfo('og_delete_orphans'); + } + +} diff --git a/src/OgFieldBase.php b/src/OgFieldBase.php index f378e1fed..de72ac90e 100644 --- a/src/OgFieldBase.php +++ b/src/OgFieldBase.php @@ -61,7 +61,7 @@ public function getEntityType() { * {@inheritdoc} */ public function setEntityType($entity_type) { - $field_storage = $this->getFieldStorageConfigBaseDefinition(); + $field_storage = $this->getFieldStorageBaseDefinition(); if (!empty($field_storage['entity']) && !in_array($entity_type, $field_storage['entity'])) { @@ -102,7 +102,7 @@ public function setFieldName($fieldName) { /** * {@inheritdoc} */ - public function getFieldStorageConfigBaseDefinition(array $values = array()) { + public function getFieldStorageBaseDefinition(array $values = array()) { $values += [ 'entity_type' => $this->getEntityType(), 'field_name' => $this->getFieldName(), @@ -114,7 +114,7 @@ public function getFieldStorageConfigBaseDefinition(array $values = array()) { /** * {@inheritdoc} */ - public function getFieldConfigBaseDefinition(array $values = array()) { + public function getFieldBaseDefinition(array $values = array()) { $values += [ 'bundle' => $this->getBundle(), 'entity_type' => $this->getEntityType(), diff --git a/src/OgFieldsInterface.php b/src/OgFieldsInterface.php index 296755804..f9df65447 100644 --- a/src/OgFieldsInterface.php +++ b/src/OgFieldsInterface.php @@ -75,37 +75,49 @@ public function setFieldName($fieldName); /** * Get the field storage config base definition. * - * @param array $values - * The base values, to which the entity type and field name would be added. + * @param [] $values + * Values to override the base definitions. * - * @return array + * @return [] * Array that will be used as the base values for * FieldStorageConfig::create(). */ - public function getFieldStorageConfigBaseDefinition(array $values = array()); + public function getFieldStorageBaseDefinition(array $values = []); /** * Get the field config base definition. * - * @param array $values - * The base values, to which the entity type, bundle and field name would be - * added. + * @param [] $values + * Values to override the base definitions. * - * @return array + * @return [] * Array that will be used as the base values for * FieldConfig::create(). */ - public function getFieldConfigBaseDefinition(array $values = array()); + public function getFieldBaseDefinition(array $values = []); /** - * @return array - * A widget definition for the field. + * Get the field's form display definition. + * + * @param [] $values + * Values to override the base definitions. + * + * @return [] + * Array that will be used as the base values for + * FieldConfig::create(). */ - public function widgetDefinition(); + public function getFormDisplayDefinition(array $values = []); + /** - * @return - * Return view modes entities for the field. + * Get the field's view modes definition. + * + * @param [] $values + * Values to override the base definitions. + * + * @return [] + * Array that will be used as the base values for + * FieldConfig::create(). */ - public function viewModesDefinition(); + public function getViewDisplayDefinition(array $values = []); } diff --git a/src/OgGroupAudienceHelper.php b/src/OgGroupAudienceHelper.php index 8d08b951a..f58b2a4de 100644 --- a/src/OgGroupAudienceHelper.php +++ b/src/OgGroupAudienceHelper.php @@ -8,6 +8,8 @@ namespace Drupal\og; use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldException; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\field\Entity\FieldStorageConfig; @@ -22,6 +24,30 @@ class OgGroupAudienceHelper { */ const DEFAULT_FIELD = 'og_group_ref'; + /** + * The name of the field type that references user to groups via membership. + */ + const USER_TO_GROUP_REFERENCE_FIELD_TYPE = 'og_membership_reference'; + + /** + * The name of the field type that references non-user entities to groups. + */ + const NON_USER_TO_GROUP_REFERENCE_FIELD_TYPE = 'og_standard_reference'; + + /** + * Return TRUE if field is a group audience type. + * + * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition + * The field definition object. + * + * @return bool + * TRUE if the field is a group audience type, FALSE otherwise. + */ + public static function isGroupAudienceField(FieldDefinitionInterface $field_definition) { + return in_array($field_definition->getType(), [OgGroupAudienceHelper::NON_USER_TO_GROUP_REFERENCE_FIELD_TYPE, OgGroupAudienceHelper::USER_TO_GROUP_REFERENCE_FIELD_TYPE]); + } + + /** * Return TRUE if a field can be used and has not reached maximum values. *d @@ -44,7 +70,7 @@ public static function checkFieldCardinality(ContentEntityInterface $entity, $fi throw new FieldException("No field with the name $field_name found for $bundle_id $entity_type_id entity."); } - if (!Og::isGroupAudienceField($field_definition)) { + if (!static::isGroupAudienceField($field_definition)) { throw new FieldException("$field_name field on $bundle_id $entity_type_id entity is not an audience field."); } @@ -75,7 +101,7 @@ public static function checkFieldCardinality(ContentEntityInterface $entity, $fi * found. */ public static function getMatchingField(ContentEntityInterface $entity, $group_type, $group_bundle, $check_access = TRUE) { - $fields = Og::getAllGroupAudienceFields($entity->getEntityTypeId(), $entity->bundle()); + $fields = static::getAllGroupAudienceFields($entity->getEntityTypeId(), $entity->bundle()); // Bail out if there are no group audience fields. if (!$fields) { @@ -111,4 +137,62 @@ public static function getMatchingField(ContentEntityInterface $entity, $group_t return NULL; } + /** + * Returns all the group audience fields of a certain bundle. + * + * @param string $group_content_entity_type_id + * The entity type ID of the group content for which to return audience + * fields. + * @param string $group_content_bundle_id + * The bundle name of the group content for which to return audience fields. + * @param string $group_entity_type_id + * Filter list to only include fields referencing a specific group type. If + * omitted, all fields will be returned. + * @param string $group_bundle_id + * Filter list to only include fields referencing a specific group bundle. + * Fields that do not specify any bundle restrictions at all are also + * included. If omitted, the results will not be filtered by group bundle. + * + * @return \Drupal\Core\Field\FieldDefinitionInterface[] + * An array of field definitions, keyed by field name; Or an empty array if + * none found. + */ + public static function getAllGroupAudienceFields($group_content_entity_type_id, $group_content_bundle_id, $group_entity_type_id = NULL, $group_bundle_id = NULL) { + $return = []; + $entity_type = \Drupal::entityTypeManager()->getDefinition($group_content_entity_type_id); + + if (!$entity_type->isSubclassOf(FieldableEntityInterface::class)) { + // This entity type is not fieldable. + return []; + } + + /** @var \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions */ + $field_definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($group_content_entity_type_id, $group_content_bundle_id); + + foreach ($field_definitions as $field_definition) { + if (!static::isGroupAudienceField($field_definition)) { + // Not a group audience field. + continue; + } + + $target_type = $field_definition->getFieldStorageDefinition()->getSetting('target_type'); + + if (isset($group_entity_type_id) && $target_type != $group_entity_type_id) { + // Field doesn't reference this group type. + continue; + } + + $handler_settings = $field_definition->getSetting('handler_settings'); + + if (isset($group_bundle_id) && !empty($handler_settings['target_bundles']) && !in_array($group_bundle_id, $handler_settings['target_bundles'])) { + continue; + } + + $field_name = $field_definition->getName(); + $return[$field_name] = $field_definition; + } + + return $return; + } + } diff --git a/src/OgMembershipInterface.php b/src/OgMembershipInterface.php index 2279393f5..1c9b396a6 100644 --- a/src/OgMembershipInterface.php +++ b/src/OgMembershipInterface.php @@ -7,11 +7,16 @@ namespace Drupal\og; +use Drupal\Core\Entity\ContentEntityInterface; +use Drupal\Core\Entity\EntityInterface; +use Drupal\og\Entity\OgRole; +use Drupal\user\Entity\User; + /** * Provides an interface for OG memberships. * @todo Provide some actual helpful documentation. */ -interface OgMembershipInterface { +interface OgMembershipInterface extends ContentEntityInterface { /** * Define active group content states. @@ -49,4 +54,186 @@ interface OgMembershipInterface { */ const REQUEST_FIELD = 'og_membership_request'; + /** + * Gets the membership creation timestamp. + * + * @return int + * The membership creation timestamp. + */ + public function getCreatedTime(); + + /** + * Sets the membership creation timestamp. + * + * @param int $timestamp + * The membership creation timestamp + * + * @return OgMembershipInterface + */ + public function setCreatedTime($timestamp); + + /** + * Sets the membership's owner. + * + * @param mixed $etid + * The user's ID or object. + * + * @return OgMembershipInterface + */ + public function setUser($etid); + + /** + * Gets the membership's owner. + * + * @return User + * The user object. + */ + public function getUser(); + + /** + * Sets the membership field name. + * + * A user can have two group reference fields. The field name property helps + * us to know to which field the membership belongs. + * + * @param string $fieldName + * The group reference field name. + * + * @return OgMembershipInterface + */ + public function setFieldName($fieldName); + + /** + * Gets the membership field name. + * + * @return string + * The group reference field name. + */ + public function getFieldName(); + + /** + * Sets the group entity ID. + * + * @param mixed $gid + * The group entity ID. + * + * @return OgMembershipInterface + */ + public function setEntityId($gid); + + /** + * Gets the group entity ID. + * + * @return integer + * The entity identifier. + */ + public function getEntityId(); + + /** + * Sets the group entity type ID. + * + * @param mixed $groupType + * The group entity type ID or object. + * + * @return OgMembershipInterface + */ + public function setGroupEntityType($groupType); + + /** + * Gets the group entity type ID. + * + * @return string + * The group entity type ID. + */ + public function getGroupEntityType(); + + /** + * Sets the membership state. + * + * @param bool $state + * TRUE or FALSE. + * + * @return OgMembershipInterface + */ + public function setState($state); + + /** + * Gets the membership state. + * + * @return bool + */ + public function getState(); + + /** + * Gets the membership type. + * + * @return string + * The bundle of the membership type. + */ + public function getType(); + + /** + * Sets the group's roles for the current user group membership. + * + * @param $role_ids + * List of OG roles ids. + * + * @return OgMembershipInterface + */ + public function setRoles($role_ids); + + /** + * Adds a role to the user membership. + * + * @param $role_id + * The OG role ID. + * + * @return OgMembershipInterface + */ + public function addRole($role_id); + + /** + * Revokes a role from the OG membership. + * + * @param $role_id + * The OG role ID. + * + * @return OgMembershipInterface + */ + public function revokeRole($role_id); + + /** + * Gets all the referenced OG roles. + * + * @return OgRole[] + * List of OG roles the user own for the current membership instance. + */ + public function getRoles(); + + /** + * Gets list of OG role IDs. + * + * @return array + * List of OG roles ids. + */ + public function getRolesIds(); + + /** + * Checks if the user has a permission inside the group. + * + * @param $permission + * The name of the permission. + * + * @return bool + */ + public function hasPermission($permission); + + /** + * Gets the group object. + * + * @return EntityInterface + * The group object which the membership reference to. + */ + public function getGroup(); + } diff --git a/src/OgMembershipViewsData.php b/src/OgMembershipViewsData.php new file mode 100644 index 000000000..d10e30c72 --- /dev/null +++ b/src/OgMembershipViewsData.php @@ -0,0 +1,26 @@ +groupManager = $group_manager; + $this->entityTypeManager = $entity_type_manager; + $this->entityTypeBundleInfo = $entity_type_bundle_info; + } + + /** + * {@inheritdoc} + * + * @todo Provide an alter hook. + */ + public function getPermissionList($entity_type_id, $bundle_id) { + $permissions = []; + + foreach ($this->groupManager->getGroupContentBundleIdsByGroupBundle($entity_type_id, $bundle_id) as $group_content_entity_type_id => $group_content_bundle_ids) { + foreach ($group_content_bundle_ids as $group_content_bundle_id) { + $permissions += $this->generateCrudPermissionList($group_content_entity_type_id, $group_content_bundle_id); + } + } + + return $permissions; + } + + /** + * {@inheritdoc} + */ + public function generateCrudPermissionList($group_content_entity_type_id, $group_content_bundle_id) { + $permissions = []; + + // Check if the bundle is a group content type. + if (!Og::isGroupContent($group_content_entity_type_id, $group_content_bundle_id)) { + return []; + } + + $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 + $permissions += [ + "create $group_content_bundle_id $group_content_entity_type_id" => [ + 'title' => t('Create %bundle @entity', $args), + ], + "update own $group_content_bundle_id $group_content_entity_type_id" => [ + 'title' => t('Edit own %bundle @entity', $args), + ], + "update any $group_content_bundle_id $group_content_entity_type_id" => [ + 'title' => t('Edit any %bundle @entity', $args), + ], + "delete own $group_content_bundle_id $group_content_entity_type_id" => [ + 'title' => t('Delete own %bundle @entity', $args), + ], + "delete any $group_content_bundle_id $group_content_entity_type_id" => [ + 'title' => t('Delete any %bundle @entity', $args), + ], + ]; + + // Add default permissions. + foreach ($permissions as $key => $value) { + $permissions[$key]['default role'] = [OgRoleInterface::ADMINISTRATOR]; + } + + return $permissions; + } + +} diff --git a/src/PermissionManagerInterface.php b/src/PermissionManagerInterface.php new file mode 100644 index 000000000..0b4db5711 --- /dev/null +++ b/src/PermissionManagerInterface.php @@ -0,0 +1,36 @@ +getSelectionHandler(); - $query = $selection_handler->buildEntityQuery($match, $match_operator); + $query = $this->getSelectionHandler()->buildEntityQuery($match, $match_operator); $target_type = $this->configuration['target_type']; + $entityDefinition = \Drupal::entityTypeManager()->getDefinition($target_type); - $identifier_key = \Drupal::entityTypeManager()->getDefinition($target_type)->getKey('id'); - $user_groups = $this->getUserGroups(); - $bundles = Og::groupManager()->getAllGroupBundles($target_type); - - $query->condition('type', $bundles, 'IN'); + if ($bundle_key = $entityDefinition->getKey('bundle')) { + $bundles = Og::groupManager()->getAllGroupBundles($target_type); + $query->condition($bundle_key, $bundles, 'IN'); + } + $user_groups = $this->getUserGroups(); if (!$user_groups) { return $query; } - $ids = []; + $identifier_key = $entityDefinition->getKey('id'); + $ids = []; if ($this->configuration['handler_settings']['field_mode'] == 'admin') { // Don't include the groups, the user doesn't have create permission. foreach ($user_groups as $delta => $group) { @@ -99,7 +100,6 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') foreach ($user_groups as $group) { $ids[] = $group->id(); } - if ($ids) { $query->condition($identifier_key, $ids, 'IN'); } @@ -118,7 +118,7 @@ protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') * @return ContentEntityInterface[] */ protected function getUserGroups() { - $other_groups = Og::getEntityGroups(User::load($this->currentUser->id())); + $other_groups = Og::getUserGroups(User::load($this->currentUser->id())); return isset($other_groups[$this->configuration['target_type']]) ? $other_groups[$this->configuration['target_type']] : []; } diff --git a/src/Plugin/Field/FieldType/OgMembershipReferenceItem.php b/src/Plugin/Field/FieldType/OgMembershipReferenceItem.php index 527bea13b..9b7a1b878 100644 --- a/src/Plugin/Field/FieldType/OgMembershipReferenceItem.php +++ b/src/Plugin/Field/FieldType/OgMembershipReferenceItem.php @@ -9,7 +9,6 @@ use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem; -use Drupal\Core\Form\FormStateInterface; /** * Class OgMembershipReferenceItem. @@ -17,43 +16,16 @@ * @FieldType( * id = "og_membership_reference", * label = @Translation("OG membership reference"), - * description = @Translation("An entity field containing an OG membership reference."), + * description = @Translation("An entity field containing an OG membership reference for non user based entity."), * category = @Translation("Reference"), * no_ui = TRUE, * default_widget = "og_complex", - * default_formatter = "og_complex", + * default_formatter = "entity_reference_label", * list_class = "\Drupal\og\Plugin\Field\FieldType\OgMembershipReferenceItemList", - * constraints = {"ValidReference" = {}, "ValidOgMembershipReference" = {}} + * constraints = {"ValidOgMembershipReference" = {}} * ) */ -class OgMembershipReferenceItem extends EntityReferenceItem { - - /** - * {@inheritdoc} - */ - public static function defaultFieldSettings() { - $settings = parent::defaultFieldSettings(); - $settings['access_override'] = FALSE; - - return $settings; - } - - /** - * {@inheritdoc} - */ - public function fieldSettingsForm(array $form, FormStateInterface $form_state) { - $form = parent::fieldSettingsForm($form, $form_state); - - // Field access settings. - $form['access_override'] = [ - '#title' => $this->t('Allow entity access to control field access'), - '#description' => $this->t('By default, the administer group permission is required to directly edit this field. Selecting this option will allow access to anybody with access to edit the entity.'), - '#type' => 'checkbox', - '#default_value' => $this->getSetting('access_override'), - ]; - - return $form; - } +class OgMembershipReferenceItem extends OgStandardReferenceItem { /** * {@inheritdoc} diff --git a/src/Plugin/Field/FieldType/OgMembershipReferenceItemList.php b/src/Plugin/Field/FieldType/OgMembershipReferenceItemList.php index a29a3849f..cffbfeee5 100644 --- a/src/Plugin/Field/FieldType/OgMembershipReferenceItemList.php +++ b/src/Plugin/Field/FieldType/OgMembershipReferenceItemList.php @@ -74,9 +74,8 @@ public function postSave($update) { // todo: move to API function. $membership_ids = \Drupal::entityQuery('og_membership') - ->condition('member_entity_id', $this->getEntity()->id()) - ->condition('member_entity_type', $this->getEntity()->getEntityTypeId()) - ->condition('group_entity_type', $this->getFieldDefinition()->getTargetEntityTypeId()) + ->condition('uid', $this->getEntity()->id()) + ->condition('entity_type', $this->getFieldDefinition()->getTargetEntityTypeId()) ->condition('field_name', $this->getFieldDefinition()->getName()) ->execute(); @@ -84,7 +83,7 @@ public function postSave($update) { $memberships = OgMembership::loadMultiple($membership_ids); $target_group_ids = array_map(function(OgMembership $membership) { - return $membership->getGroupEntityid(); + return $membership->getEntityId(); }, $memberships); $deprecated_membership_ids = array_diff($target_group_ids, $group_ids); @@ -129,6 +128,28 @@ public function referencedEntities() { }, $this->list)); } + /** + * {@inheritdoc} + */ + public function getValue() { + $membership_ids = \Drupal::entityQuery('og_membership') + ->condition('field_name', $this->getName()) + ->condition('uid', $this->getEntity()->id()) + ->condition('state', OgMembershipInterface::STATE_ACTIVE) + ->execute(); + + if (!$memberships = OgMembership::loadMultiple($membership_ids)) { + return []; + } + + $return = []; + foreach ($memberships as $membership) { + $return[] = ['target_id' => $membership->getEntityid()]; + } + + return $return; + } + /** * Populate reference items for active group memberships. */ @@ -145,9 +166,8 @@ protected function populateGroupsFromMembershipEntities() { $membership_ids = \Drupal::entityQuery('og_membership') ->condition('field_name', $this->getName()) - ->condition('member_entity_type', $entity->getEntityTypeId()) - ->condition('member_entity_id', $entity->id()) - ->condition('group_entity_type', $group_type) + ->condition('uid', $entity->id()) + ->condition('entity_type', $group_type) ->condition('state', OgMembershipInterface::STATE_ACTIVE) ->execute(); @@ -155,7 +175,7 @@ protected function populateGroupsFromMembershipEntities() { $memberships = OgMembership::loadMultiple($membership_ids); $group_ids = array_map(function (OgMembership $membership) { - return $membership->getGroupEntityid(); + return $membership->getEntityId(); }, $memberships); $groups = \Drupal::entityTypeManager()->getStorage($group_type)->loadMultiple($group_ids); @@ -178,15 +198,14 @@ protected function populateGroupsFromMembershipEntities() { protected function createOgMembership($group_id) { /** @var \Drupal\Core\Entity\EntityInterface $parent */ $parent_entity = $this->getEntity(); - /** @var OgMembership $membership */ + /** @var OgMembershipInterface $membership */ $membership = Og::membershipStorage()->create(Og::membershipDefault()); $membership ->setFieldName($this->getName()) - ->setMemberEntityType($parent_entity->getEntityTypeId()) - ->setMemberEntityId($parent_entity->id()) + ->setUser($parent_entity->id()) ->setGroupEntityType($this->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type')) - ->setGroupEntityid($group_id) + ->setEntityId($group_id) ->save(); return $membership; diff --git a/src/Plugin/Field/FieldType/OgStandardReferenceItem.php b/src/Plugin/Field/FieldType/OgStandardReferenceItem.php new file mode 100644 index 000000000..986b8439b --- /dev/null +++ b/src/Plugin/Field/FieldType/OgStandardReferenceItem.php @@ -0,0 +1,58 @@ + $this->t('Allow entity access to control field access'), + '#description' => $this->t('By default, the administer group permission is required to directly edit this field. Selecting this option will allow access to anybody with access to edit the entity.'), + '#type' => 'checkbox', + '#default_value' => $this->getSetting('access_override'), + ]; + + return $form; + } + +} diff --git a/src/Plugin/Field/FieldWidget/OgComplex.php b/src/Plugin/Field/FieldWidget/OgComplex.php index 552d89d0a..9d8012f1a 100644 --- a/src/Plugin/Field/FieldWidget/OgComplex.php +++ b/src/Plugin/Field/FieldWidget/OgComplex.php @@ -26,6 +26,7 @@ * label = @Translation("OG reference"), * description = @Translation("An autocompletewidget for OG"), * field_types = { + * "og_standard_reference", * "og_membership_reference" * } * ) @@ -73,8 +74,8 @@ protected function formMultipleElements(FieldItemListInterface $items, array &$f $cardinality = $this->fieldDefinition->getFieldStorageDefinition()->getCardinality(); $parents = $form['#parents']; - $target_type = $this->fieldDefinition->getTargetEntityTypeId(); - $user_groups = Og::getEntityGroups(User::load(\Drupal::currentUser()->id())); + $target_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type'); + $user_groups = Og::getUserGroups(User::load(\Drupal::currentUser()->id())); $user_groups_target_type = isset($user_groups[$target_type]) ? $user_groups[$target_type] : []; $user_group_ids = array_map(function($group) { return $group->id(); @@ -228,9 +229,9 @@ protected function otherGroupsWidget(FieldItemListInterface $items, FormStateInt $delta = 0; - $target_type = $this->fieldDefinition->getTargetEntityTypeId(); + $target_type = $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type'); - $user_groups = Og::getEntityGroups(User::load(\Drupal::currentUser()->id())); + $user_groups = Og::getUserGroups(User::load(\Drupal::currentUser()->id())); $user_groups_target_type = isset($user_groups[$target_type]) ? $user_groups[$target_type] : []; $user_group_ids = array_map(function($group) { return $group->id(); @@ -285,7 +286,7 @@ public function otherGroupsSingle($delta, EntityInterface $entity = NULL, $weigh 'target_id' => [ // @todo Allow this to be configurable with a widget setting. '#type' => 'entity_autocomplete', - '#target_type' => $this->fieldDefinition->getTargetEntityTypeId(), + '#target_type' => $this->fieldDefinition->getFieldStorageDefinition()->getSetting('target_type'), '#selection_handler' => 'og:default', '#selection_settings' => [ 'other_groups' => TRUE, @@ -306,7 +307,10 @@ public function otherGroupsSingle($delta, EntityInterface $entity = NULL, $weigh * {@inheritdoc} */ public function massageFormValues(array $values, array $form, FormStateInterface $form_state) { - $parent_values = $values; + // Remove empty values. The form fields may be empty. + $values = array_filter($values, function ($item) { + return !empty($item['target_id']); + }); // Get the groups from the other groups widget. foreach ($form[$this->fieldDefinition->getName()]['other_groups'] as $key => $value) { @@ -318,18 +322,16 @@ public function massageFormValues(array $values, array $form, FormStateInterface // be captured in it's own group, with the key 'id'. preg_match("|.+\((?[\w.]+)\)|", $value['target_id']['#value'], $matches); - if (empty($matches['id'])) { - continue; + if (!empty($matches['id'])) { + $values[] = [ + 'target_id' => $matches['id'], + '_weight' => $value['_weight']['#value'], + '_original_delta' => $value['_weight']['#delta'], + ]; } - - $parent_values[] = [ - 'target_id' => $matches['id'], - '_weight' => $value['_weight']['#value'], - '_original_delta' => $value['_weight']['#delta'], - ]; } - return $parent_values; + return $values; } /** diff --git a/src/Plugin/OgDeleteOrphans/Batch.php b/src/Plugin/OgDeleteOrphans/Batch.php new file mode 100644 index 000000000..7be53a3c6 --- /dev/null +++ b/src/Plugin/OgDeleteOrphans/Batch.php @@ -0,0 +1,74 @@ +getQueue(); + $item = $queue->claimItem(0); + $data = $item->data; + $this->deleteOrphan($data['type'], $data['id']); + $queue->deleteItem($item); + } + + /** + * {@inheritdoc} + */ + public function configurationForm($form, FormStateInterface $form_state) { + $count = $this->getQueue()->numberOfItems(); + return [ + '#type' => 'fieldset', + '#title' => $this->t('Batch options'), + 'info' => [ + '#markup' => '

' . $this->t('There are :count orphans waiting to be deleted.', [ + ':count' => $count, + ]) . '

', + ], + 'submit' => [ + '#type' => 'submit', + '#value' => $this->t('Start batch deletion'), + '#submit' => ['\Drupal\og\Plugin\OgDeleteOrphans\Batch::batchSubmit'], + '#disabled' => !(bool) $count, + ], + ]; + } + + /** + * Submit handler for ::configurationForm(). + */ + public static function batchSubmit($form, FormStateInterface $form_state) { + $batch = []; + $steps = \Drupal::queue('og_orphaned_group_content')->numberOfItems(); + for ($i = 0; $i < $steps; $i++) { + $batch['operations'][] = ['\Drupal\og\Plugin\OgDeleteOrphans\Batch::step', []]; + } + batch_set($batch); + } + + /** + * Batch step definition callback to process one queue item. + */ + public static function step($context) { + if (!empty($context['interrupted'])) { + return; + } + \Drupal::getContainer()->get('plugin.manager.og.delete_orphans')->createInstance('batch', [])->process(); + } + +} diff --git a/src/Plugin/OgDeleteOrphans/Cron.php b/src/Plugin/OgDeleteOrphans/Cron.php new file mode 100644 index 000000000..ab57ae0bb --- /dev/null +++ b/src/Plugin/OgDeleteOrphans/Cron.php @@ -0,0 +1,49 @@ +deleteOrphan($data['type'], $data['id']); + } + + /** + * {@inheritdoc} + */ + protected function getQueue() { + // By design, every QueueWorker is executed on every cron run and will + // start processing its designated queue. To make sure that our DeleteOrphan + // queue worker will not start processing orphans that have been registered + // by another plugin (e.g. the Batch plugin) we are using a dedicated queue. + return $this->queueFactory->get('og_orphaned_group_content_cron', TRUE); + } + +} diff --git a/src/Plugin/OgDeleteOrphans/Simple.php b/src/Plugin/OgDeleteOrphans/Simple.php new file mode 100644 index 000000000..ac3a6da67 --- /dev/null +++ b/src/Plugin/OgDeleteOrphans/Simple.php @@ -0,0 +1,40 @@ +process(); + } + + /** + * {@inheritdoc} + */ + public function process() { + $queue = $this->getQueue(); + while ($item = $queue->claimItem()) { + $data = $item->data; + $this->deleteOrphan($data['type'], $data['id']); + $queue->deleteItem($item); + } + } + +} diff --git a/src/Plugin/OgFields/AccessField.php b/src/Plugin/OgFields/AccessField.php index 1ba5f08a7..ce577eca7 100644 --- a/src/Plugin/OgFields/AccessField.php +++ b/src/Plugin/OgFields/AccessField.php @@ -25,8 +25,8 @@ class AccessField extends OgFieldBase implements OgFieldsInterface { /** * {@inheritdoc} */ - public function getFieldStorageConfigBaseDefinition(array $values = array()) { - $values = [ + public function getFieldStorageBaseDefinition(array $values = []) { + $values += [ 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, 'settings' => [ 'allowed_values' => [ @@ -38,14 +38,14 @@ public function getFieldStorageConfigBaseDefinition(array $values = array()) { 'type' => 'list_integer', ]; - return parent::getFieldStorageConfigBaseDefinition($values); + return parent::getFieldStorageBaseDefinition($values); } /** * {@inheritdoc} */ - public function getFieldConfigBaseDefinition(array $values = array()) { - $values = [ + public function getFieldBaseDefinition(array $values = []) { + $values += [ 'default_value' => [0 => ['value' => 0]], 'description' => $this->t('Determine if group should use default roles and permissions.'), 'display_label' => TRUE, @@ -53,32 +53,31 @@ public function getFieldConfigBaseDefinition(array $values = array()) { 'required' => TRUE, ]; - return parent::getFieldConfigBaseDefinition($values); + return parent::getFieldBaseDefinition($values); } /** * {@inheritdoc} */ - public function widgetDefinition() { - return [ + public function getFormDisplayDefinition(array $values = []) { + $values += [ 'type' => 'options_select', 'settings' => [], ]; + + + return $values; } /** * {@inheritdoc} */ - public function viewModesDefinition() { - return [ - 'default' => [ - 'type' => 'list_default', - 'label' => 'above', - ], - 'teaser' => [ - 'type' => 'list_default', - 'label' => 'above', - ], + public function getViewDisplayDefinition(array $values = []) { + $values += [ + 'type' => 'list_default', + 'label' => 'above', ]; + + return $values; } } diff --git a/src/Plugin/OgFields/AudienceField.php b/src/Plugin/OgFields/AudienceField.php index 8f31bdf44..c300f36ac 100644 --- a/src/Plugin/OgFields/AudienceField.php +++ b/src/Plugin/OgFields/AudienceField.php @@ -27,70 +27,66 @@ class AudienceField extends OgFieldBase implements OgFieldsInterface { /** * {@inheritdoc} */ - public function getFieldStorageConfigBaseDefinition(array $values = array()) { - $values = [ + public function getFieldStorageBaseDefinition(array $values = array()) { + $values += [ 'cardinality' => FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED, - 'custom_storage' => TRUE, + 'custom_storage' => $this->getEntityType() == 'user', 'settings' => [ - 'handler' => 'og', - 'handler_settings' => [ - 'target_bundles' => [], - 'membership_type' => OgMembershipInterface::TYPE_DEFAULT, - ], 'target_type' => $this->getEntityType(), ], - 'type' => 'og_membership_reference', + 'type' => $this->getEntityType() == 'user' ? OgGroupAudienceHelper::USER_TO_GROUP_REFERENCE_FIELD_TYPE : OgGroupAudienceHelper::NON_USER_TO_GROUP_REFERENCE_FIELD_TYPE, ]; - return parent::getFieldStorageConfigBaseDefinition($values); + return parent::getFieldStorageBaseDefinition($values); } /** * {@inheritdoc} */ - public function getFieldConfigBaseDefinition(array $values = array()) { - $values = [ + public function getFieldBaseDefinition(array $values = array()) { + $values += [ 'description' => $this->t('OG group audience reference field.'), 'display_label' => TRUE, 'label' => $this->t('Groups audience'), + 'settings' => [ + 'handler' => 'og', + 'handler_settings' => [], + ], ]; - return parent::getFieldConfigBaseDefinition($values); + return parent::getFieldBaseDefinition($values); } /** * {@inheritdoc} */ - public function widgetDefinition(array $widget = []) { - // Keep this until og_complex widget is back. - return [ + public function getFormDisplayDefinition(array $values = []) { + $values += [ 'type' => 'og_complex', 'settings' => [ 'match_operator' => 'CONTAINS', + 'size' => 60, + 'placeholder' => '', ], ]; + + + return $values; } /** * {@inheritdoc} */ - public function viewModesDefinition(array $view_mode = []) { - return [ - 'default' => [ - 'label' => 'above', - 'type' => 'entity_reference_label', - 'settings' => [ - 'link' => TRUE, - ] - ], - 'teaser' => [ - 'label' => 'above', - 'type' => 'entity_reference_label', - 'settings' => [ - 'link' => TRUE, - ], - ], + public function getViewDisplayDefinition(array $values = []) { + $values += [ + 'label' => 'above', + 'type' => 'entity_reference_label', + 'settings' => [ + 'link' => TRUE, + ] ]; + + return $values; } } diff --git a/src/Plugin/QueueWorker/DeleteOrphan.php b/src/Plugin/QueueWorker/DeleteOrphan.php new file mode 100644 index 000000000..e0779243a --- /dev/null +++ b/src/Plugin/QueueWorker/DeleteOrphan.php @@ -0,0 +1,64 @@ +ogDeleteOrphansPluginManager = $og_delete_orphans_plugin_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('plugin.manager.og.delete_orphans') + ); + } + + /** + * {@inheritdoc} + */ + public function processItem($data) { + $this->ogDeleteOrphansPluginManager->createInstance('cron', [])->processItem($data); + } + +} diff --git a/src/Plugin/Validation/Constraint/ValidOgMembershipReferenceConstraintValidator.php b/src/Plugin/Validation/Constraint/ValidOgMembershipReferenceConstraintValidator.php index d7a8a3c84..f3d93da6a 100644 --- a/src/Plugin/Validation/Constraint/ValidOgMembershipReferenceConstraintValidator.php +++ b/src/Plugin/Validation/Constraint/ValidOgMembershipReferenceConstraintValidator.php @@ -28,7 +28,7 @@ public function validate($value, Constraint $constraint) { } $entity = \Drupal::entityTypeManager() - ->getStorage($value->getFieldDefinition()->getTargetEntityTypeId()) + ->getStorage($value->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type')) ->load($value->get('target_id')->getValue()); if (!$entity) { diff --git a/tests/modules/og_standard_reference_test_views/og_standard_reference_test_views.info.yml b/tests/modules/og_standard_reference_test_views/og_standard_reference_test_views.info.yml new file mode 100644 index 000000000..b0c11c4cd --- /dev/null +++ b/tests/modules/og_standard_reference_test_views/og_standard_reference_test_views.info.yml @@ -0,0 +1,8 @@ +name: 'OG standard reference test views' +type: module +description: 'Provides default views for views OG standard reference tests.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - views diff --git a/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_entity_test_mul_view.yml b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_entity_test_mul_view.yml new file mode 100644 index 000000000..7cd22a910 --- /dev/null +++ b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_entity_test_mul_view.yml @@ -0,0 +1,120 @@ +langcode: en +status: true +dependencies: + module: + - entity_test +id: test_og_standard_reference_entity_test_mul_view +label: test_og_standard_reference_entity_test_mul_view +module: views +description: '' +tag: '' +base_table: entity_test_mul_property_data +base_field: id +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + quantity: 9 + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + id: + id: id + table: entity_test_mul_property_data + field: id + entity_type: entity_test_mul + entity_field: id + plugin_id: field + id_1: + id: id_1 + table: entity_test + field: id + entity_type: entity_test + entity_field: id + plugin_id: field + relationship: field_data_test + filters: { } + sorts: + id: + id: id + table: entity_test_mul_property_data + field: id + entity_type: entity_test_mul + entity_field: id + plugin_id: standard + header: { } + footer: { } + empty: { } + relationships: + field_data_test: + id: field_data_test + table: entity_test_mul__field_data_test + field: field_data_test + plugin_id: standard + arguments: { } + display_extenders: { } + cache_metadata: + contexts: + - languages + - 'languages:language_interface' + max-age: 0 diff --git a/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_entity_test_view.yml b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_entity_test_view.yml new file mode 100644 index 000000000..034a745af --- /dev/null +++ b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_entity_test_view.yml @@ -0,0 +1,121 @@ +langcode: en +status: true +dependencies: + module: + - entity_test +id: test_og_standard_reference_entity_test_view +label: test_og_standard_reference_entity_test_view +module: views +description: '' +tag: '' +base_table: entity_test +base_field: id +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + quantity: 9 + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + id: + id: id + table: entity_test + field: id + entity_type: entity_test + entity_field: id + plugin_id: field + id_1: + id: id_1 + table: entity_test_mul + field: id + entity_type: entity_test_mul + entity_field: id + plugin_id: field + relationship: field_test_data + filters: { } + sorts: + id: + id: id + table: entity_test + field: id + entity_type: entity_test + entity_field: id + plugin_id: standard + header: { } + footer: { } + empty: { } + relationships: + field_test_data: + id: field_test_data + table: entity_test__field_test_data + field: field_test_data + plugin_id: standard + arguments: { } + display_extenders: { } + cache_metadata: + contexts: + - entity_test_view_grants + - languages + - 'languages:language_interface' + max-age: 0 diff --git a/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_reverse_entity_test_mul_view.yml b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_reverse_entity_test_mul_view.yml new file mode 100644 index 000000000..07fbbf101 --- /dev/null +++ b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_reverse_entity_test_mul_view.yml @@ -0,0 +1,130 @@ +langcode: en +status: true +dependencies: + module: + - entity_test +id: test_og_standard_reference_reverse_entity_test_mul_view +label: test_og_standard_reference_reverse_entity_test_mul_view +module: views +description: '' +tag: '' +base_table: entity_test +base_field: id +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + quantity: 9 + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + id: + id: id + table: entity_test + field: id + entity_type: entity_test + entity_field: id + plugin_id: field + id_1: + id: id_1 + table: entity_test_mul_property_data + field: id + entity_type: entity_test_mul + entity_field: id + plugin_id: field + relationship: reverse__entity_test_mul__field_data_test + filters: { } + sorts: + id: + id: id + table: entity_test + field: id + entity_type: entity_test + entity_field: id + plugin_id: standard + id_1: + id: id_1 + table: entity_test_mul_property_data + field: id + entity_type: entity_test_mul + entity_field: id + plugin_id: standard + relationship: reverse__entity_test_mul__field_data_test + header: { } + footer: { } + empty: { } + relationships: + reverse__entity_test_mul__field_data_test: + id: reverse__entity_test_mul__field_data_test + table: entity_test + field: reverse__entity_test_mul__field_data_test + entity_type: entity_test + plugin_id: entity_reverse + arguments: { } + display_extenders: { } + cache_metadata: + contexts: + - entity_test_view_grants + - languages + - 'languages:language_interface' + max-age: 0 diff --git a/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_reverse_entity_test_view.yml b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_reverse_entity_test_view.yml new file mode 100644 index 000000000..eb01a89e9 --- /dev/null +++ b/tests/modules/og_standard_reference_test_views/test_views/views.view.test_og_standard_reference_reverse_entity_test_view.yml @@ -0,0 +1,129 @@ +langcode: en +status: true +dependencies: + module: + - entity_test +id: test_og_standard_reference_reverse_entity_test_view +label: test_og_standard_reference_reverse_entity_test_view +module: views +description: '' +tag: '' +base_table: entity_test_mul_property_data +base_field: id +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + access: + type: none + options: { } + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 10 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + quantity: 9 + style: + type: default + options: + grouping: { } + row_class: '' + default_row_class: true + uses_fields: false + row: + type: fields + options: + inline: { } + separator: '' + hide_empty: false + default_field_elements: true + fields: + id: + id: id + table: entity_test_mul_property_data + field: id + entity_type: entity_test_mul + entity_field: id + plugin_id: field + id_1: + id: id_1 + table: entity_test + field: id + entity_type: entity_test + entity_field: id + plugin_id: field + relationship: reverse__entity_test__field_test_data + filters: { } + sorts: + id: + id: id + table: entity_test_mul_property_data + field: id + entity_type: entity_test_mul + entity_field: id + plugin_id: standard + id_1: + id: id_1 + table: entity_test + field: id + entity_type: entity_test + entity_field: id + plugin_id: standard + relationship: reverse__entity_test__field_test_data + header: { } + footer: { } + empty: { } + relationships: + reverse__entity_test__field_test_data: + id: reverse__entity_test__field_test_data + table: entity_test_mul_property_data + field: reverse__entity_test__field_test_data + entity_type: entity_test_mul + plugin_id: entity_reverse + arguments: { } + display_extenders: { } + cache_metadata: + contexts: + - languages + - 'languages:language_interface' + max-age: 0 diff --git a/tests/modules/og_test/src/Plugin/OgFields/EntityRestrictedField.php b/tests/modules/og_test/src/Plugin/OgFields/EntityRestrictedField.php index 353a2b7c7..23802e3c8 100644 --- a/tests/modules/og_test/src/Plugin/OgFields/EntityRestrictedField.php +++ b/tests/modules/og_test/src/Plugin/OgFields/EntityRestrictedField.php @@ -19,34 +19,28 @@ class EntityRestrictedField extends OgFieldBase implements OgFieldsInterface { /** * {@inheritdoc} */ - public function getFieldStorageConfigBaseDefinition(array $values = array()) { + public function getFieldStorageBaseDefinition(array $values = []) { $values = [ // Restrict the allowed entities. 'entity' => ['node'], 'type' => 'list_integer', ]; - return parent::getFieldStorageConfigBaseDefinition($values); + return parent::getFieldStorageBaseDefinition($values); } - /** - * {@inheritdoc} - */ - public function getFieldConfigBaseDefinition(array $values = array()) { - return parent::getFieldConfigBaseDefinition($values); - } /** * {@inheritdoc} */ - public function widgetDefinition() { + public function getFormDisplayDefinition(array $values = []) { return []; } /** * {@inheritdoc} */ - public function viewModesDefinition() { + public function getViewDisplayDefinition(array $values = []) { return []; } } diff --git a/tests/og.test b/tests/og.test index 6400788a3..8bb9ed0b9 100644 --- a/tests/og.test +++ b/tests/og.test @@ -1947,44 +1947,14 @@ class OgDeleteOrphansTestCase extends DrupalWebTestCase { variable_set('og_use_queue', TRUE); } - /** - * Testing two things: - * When deleting a group, the node of the group will be deleted. - * Associated node with the deleted group and another group won't be deleted. - */ - function testDeleteGroup() { - // Creating two groups. - $first_group = $this->drupalCreateNode(array('type' => $this->group_type)); - $second_group = $this->drupalCreateNode(array('type' => $this->group_type)); - - // Create two nodes. - $first_node = $this->drupalCreateNode(array('type' => $this->node_type)); - og_group('node', $first_group, array('entity_type' => 'node', 'entity' => $first_node)); - og_group('node', $second_group, array('entity_type' => 'node', 'entity' => $first_node)); - - $second_node = $this->drupalCreateNode(array('type' => $this->node_type)); - og_group('node', $first_group, array('entity_type' => 'node', 'entity' => $second_node)); - - // Delete the group. - node_delete($first_group->nid); - - // Execute manually the queue worker. - $queue = DrupalQueue::get('og_membership_orphans'); - $item = $queue->claimItem(); - og_membership_orphans_worker($item->data); - - // Load the nodes we used during the test. - $first_node = node_load($first_node->nid); - $second_node = node_load($second_node->nid); - - // Verify the none orphan node wasn't deleted. - $this->assertTrue($first_node, "The second node is realted to another group and deleted."); - // Verify the orphan node deleted. - $this->assertFalse($second_node, "The orphan node deleted."); - } - /** * Testing the moving of the node to another group when deleting a group. + * + * @todo This functionality still needs to be ported to Drupal 8. + * + * @see og_membership_delete_by_group() + * @see og_test_entity_delete() + * @see https://github.com/amitaibu/og/issues/178 */ function testMoveOrphans() { // Creating two groups. diff --git a/tests/src/Functional/OgComplexWidgetTest.php b/tests/src/Functional/OgComplexWidgetTest.php new file mode 100644 index 000000000..c50dc0433 --- /dev/null +++ b/tests/src/Functional/OgComplexWidgetTest.php @@ -0,0 +1,123 @@ + 'group']); + Og::groupManager()->addGroup('block_content', 'group'); + + // Add a group audience field to the "post" node type, turning it into a + // group content type. + $this->createContentType(['type' => 'post']); + $settings = [ + 'field_storage_config' => [ + 'settings' => [ + 'target_type' => 'block_content', + ], + ], + ]; + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'node', 'post', $settings); + } + + /** + * Tests adding groups with the "Groups audience" and "Other Groups" fields. + * + * @dataProvider ogComplexFieldsProvider + */ + function testFields($field, $field_name) { + $admin_user = $this->drupalCreateUser(['administer group', 'access content', 'create post content']); + $group_owner = $this->drupalCreateUser(['access content', 'create post content']); + + // Create a group content type owned by the group owner. + $values = [ + 'type' => 'group', + 'uid' => $group_owner->id(), + ]; + $group = BlockContent::create($values); + $group->save(); + + // Log in as administrator. + $this->drupalLogin($admin_user); + + // Create a new post in the group by using the given field in the UI. + $edit = [ + 'title[0][value]' => "Group owner's post.", + $field_name => "group ({$group->id()})", + ]; + $this->drupalGet('node/add/post'); + $this->submitForm($edit, 'Save'); + $this->assertSession()->statusCodeEquals(200); + + // Retrieve the post that was created from the database. + /** @var QueryInterface $query */ + $query = $this->container->get('entity.query')->get('node'); + $result = $query + ->condition('type', 'post') + ->range(0, 1) + ->sort('nid', 'DESC') + ->execute(); + $post_nid = reset($result); + + /** @var NodeInterface $post */ + $post = Node::load($post_nid); + + // Check that the post references the group correctly. + /** @var OgMembershipReferenceItemList $reference_list */ + $reference_list = $post->get(OgGroupAudienceHelper::DEFAULT_FIELD); + $this->assertEquals(1, $reference_list->count(), "There is 1 reference after adding a group to the '$field' field."); + $this->assertEquals($group->id(), $reference_list->first()->getValue()['target_id'], "The '$field' field references the correct group."); + } + + /** + * Data provider for ::testFields() + * + * @return array + */ + public function ogComplexFieldsProvider() { + return [ + ['Groups audience', 'og_group_ref[0][target_id]'], + ['Other groups', 'other_groups[0][target_id]'], + ]; + } + +} diff --git a/tests/src/Kernel/Access/OgEntityAccessTest.php b/tests/src/Kernel/Access/OgEntityAccessTest.php new file mode 100644 index 000000000..1b4d22f93 --- /dev/null +++ b/tests/src/Kernel/Access/OgEntityAccessTest.php @@ -0,0 +1,264 @@ +installConfig(['og']); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installEntitySchema('entity_test'); + $this->installSchema('system', 'sequences'); + + $this->groupBundle = Unicode::strtolower($this->randomMachineName()); + + + // Create users, and make sure user ID 1 isn't used. + User::create(['name' => $this->randomString()]); + + $group_owner = User::create(['name' => $this->randomString()]); + $group_owner->save(); + + // A group member with the correct role. + $this->user1 = User::create(['name' => $this->randomString()]); + $this->user1->save(); + + // A group member without the correct role. + $this->user2 = User::create(['name' => $this->randomString()]); + $this->user2->save(); + + // A non-member. + $this->user3 = User::create(['name' => $this->randomString()]); + $this->user3->save(); + + // Admin user. + $this->adminUser = User::create(['name' => $this->randomString()]); + $this->adminUser->save(); + + + // Define the group content as group. + Og::groupManager()->addGroup('entity_test', $this->groupBundle); + + // Create a group and associate with user 1. + $this->group1 = EntityTest::create([ + 'type' => $this->groupBundle, + 'name' => $this->randomString(), + 'user_id' => $group_owner->id(), + ]); + $this->group1->save(); + + // Create another group to help test per group/per account permission + // caching. + $this->group2 = EntityTest::create([ + 'type' => $this->groupBundle, + 'name' => $this->randomString(), + 'user_id' => $group_owner->id(), + ]); + $this->group2->save(); + + /** @var OgRole ogRoleWithPermission */ + $this->ogRoleWithPermission = OgRole::create(); + $this->ogRoleWithPermission + ->setId($this->randomMachineName()) + ->setLabel($this->randomString()) + ->setGroupType($this->group1->getEntityTypeId()) + ->setGroupBundle($this->groupBundle) + // Associate an arbitrary permission with the role. + ->grantPermission('some_perm') + ->save(); + + $this->ogRoleWithPermission2 = OgRole::create(); + $this->ogRoleWithPermission2 + ->setId($this->randomMachineName()) + ->setLabel($this->randomString()) + ->setGroupType($this->group1->getEntityTypeId()) + ->setGroupBundle($this->groupBundle) + // Associate an arbitrary permission with the role. + ->grantPermission('some_perm_2') + ->save(); + + /** @var OgRole ogRoleWithoutPermission */ + $this->ogRoleWithoutPermission = OgRole::create(); + $this->ogRoleWithoutPermission + ->setId($this->randomMachineName()) + ->setLabel($this->randomString()) + ->setGroupType($this->group1->getEntityTypeId()) + ->setGroupBundle($this->groupBundle) + ->grantPermission($this->randomMachineName()) + ->save(); + + $this->ogAdminRole = OgRole::create(); + $this->ogAdminRole + ->setId($this->randomMachineName()) + ->setLabel($this->randomString()) + ->setGroupType($this->group1->getEntityTypeId()) + ->setGroupBundle($this->groupBundle) + ->setIsAdmin(TRUE) + ->save(); + + + /** @var OgMembership $membership */ + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($this->user1->id()) + ->setEntityId($this->group1->id()) + ->setGroupEntityType($this->group1->getEntityTypeId()) + ->addRole($this->ogRoleWithPermission->id()) + ->save(); + + // Also create a membership to the other group. From this we can verify that + // permissions are not bled between groups. + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($this->user1->id()) + ->setEntityId($this->group2->id()) + ->setGroupEntityType($this->group2->getEntityTypeId()) + ->addRole($this->ogRoleWithPermission2->id()) + ->save(); + + /** @var OgMembership $membership */ + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($this->user2->id()) + ->setEntityId($this->group1->id()) + ->setGroupEntityType($this->group1->getEntityTypeId()) + ->addRole($this->ogRoleWithoutPermission->id()) + ->save(); + + /** @var OgMembership $membership */ + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($this->adminUser->id()) + ->setEntityId($this->group1->id()) + ->setGroupEntityType($this->group1->getEntityTypeId()) + ->addRole($this->ogAdminRole->id()) + ->save(); + } + + /** + * Test access to an arbitrary permission. + */ + public function testAccess() { + $og_access = $this->container->get('og.access'); + + // A member user. + $this->assertTrue($og_access->userAccess($this->group1, 'some_perm', $this->user1)->isAllowed()); + // This user should not have access to 'some_perm_2' as that was only + // assigned to group 2. + $this->assertTrue($og_access->userAccess($this->group1, 'some_perm_2', $this->user1)->isForbidden()); + + $this->assertTrue($og_access->userAccess($this->group1, 'some_perm', $this->user1)->isAllowed()); + + // A member user without the correct role. + $this->assertTrue($og_access->userAccess($this->group1, 'some_perm', $this->user2)->isForbidden()); + + // A non-member user. + $this->assertTrue($og_access->userAccess($this->group1, 'some_perm', $this->user3)->isForbidden()); + + // Group admin user should have access regardless. + $this->assertTrue($og_access->userAccess($this->group1, 'some_perm', $this->adminUser)->isAllowed()); + $this->assertTrue($og_access->userAccess($this->group1, $this->randomMachineName(), $this->adminUser)->isAllowed()); + + // Add membership to user 3. + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($this->user3->id()) + ->setEntityId($this->group1->id()) + ->setGroupEntityType($this->group1->getEntityTypeId()) + ->addRole($this->ogRoleWithPermission->id()) + ->save(); + + $this->assertTrue($og_access->userAccess($this->group1, 'some_perm', $this->user3)->isAllowed()); + } + +} diff --git a/tests/src/Kernel/DefaultPermissionEventIntegrationTest.php b/tests/src/Kernel/DefaultPermissionEventIntegrationTest.php new file mode 100644 index 000000000..eecaeff16 --- /dev/null +++ b/tests/src/Kernel/DefaultPermissionEventIntegrationTest.php @@ -0,0 +1,101 @@ +eventDispatcher = $this->container->get('event_dispatcher'); + $this->ogRoleStorage = $this->container->get('entity_type.manager')->getStorage('og_role'); + + // Create a group entity type. Note that since we are using the EntityTest + // entity we don't actually need to create the group bundle. EntityTest does + // not have real bundles, it just pretends it does. + $this->groupBundleId = $this->randomMachineName(); + Og::groupManager()->addGroup('entity_test', $this->groupBundleId); + } + + /** + * Tests that OG correctly provides the group administrator default role. + */ + public function testPermissionEventIntegration() { + // Query the event listener directly to see if the administrator role is + // present. + /** @var DefaultRoleEvent $event */ + $event = $this->eventDispatcher->dispatch(DefaultRoleEventInterface::EVENT_NAME, new DefaultRoleEvent()); + $expected_roles = [ + OgRoleInterface::ADMINISTRATOR => [ + 'label' => 'Administrator', + 'role_type' => OgRoleInterface::ROLE_TYPE_STANDARD, + ], + ]; + $this->assertEquals($event->getRoles(), $expected_roles); + + // Check that the per-group-type default roles are populated. + $expected_roles = [ + OgRoleInterface::ANONYMOUS, + OgRoleInterface::AUTHENTICATED, + OgRoleInterface::ADMINISTRATOR, + ]; + $actual_roles = $this->ogRoleStorage->loadByProperties([ + 'group_type' => 'entity_test', + 'group_bundle' => $this->groupBundleId, + ]); + + $this->assertEquals(count($expected_roles), count($actual_roles)); + + foreach ($expected_roles as $expected_role) { + // The role ID consists of the entity type, bundle and role name. + $expected_key = implode('-', [ + 'entity_test', + $this->groupBundleId, + $expected_role, + ]); + $this->assertArrayHasKey($expected_key, $actual_roles); + } + } + +} diff --git a/tests/src/Kernel/Entity/EntityCreateAccessTest.php b/tests/src/Kernel/Entity/EntityCreateAccessTest.php new file mode 100644 index 000000000..8a26eeac6 --- /dev/null +++ b/tests/src/Kernel/Entity/EntityCreateAccessTest.php @@ -0,0 +1,126 @@ +installConfig(['og']); + $this->installEntitySchema('node'); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + // Create a "group" node type and turn it into a group type. + $this->groupType = NodeType::create([ + 'type' => 'group', + 'name' => $this->randomString(), + ]); + $this->groupType->save(); + Og::groupManager()->addGroup('node', 'group'); + + // Add a group audience field to the "post" node type, turning it into a + // group content type. + $this->groupContentType = NodeType::create([ + 'type' => 'post', + 'name' => $this->randomString(), + ]); + $this->groupContentType->save(); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'node', 'post'); + } + + /** + * Tests that users that can only view cannot access the entity creation form. + */ + function testViewPermissionDoesNotGrantCreateAccess() { + // Create test user. + $user = User::create(['name' => $this->randomString()]); + $user->save(); + + // Create a group. + Node::create([ + 'title' => $this->randomString(), + 'type' => 'group', + 'uid' => $user->id(), + ])->save(); + + // Make sure the anonymous user exists. This normally is created in the + // install hook of the User module, but this doesn't run in a KernelTest. + // @see user_install() + \Drupal::entityTypeManager() + ->getStorage('user') + ->create(['uid' => 0, 'status' => 0, 'name' => '']) + ->save(); + + // Grant the anonymous user permission to view published content. + /** @var Role $role */ + $role = Role::create(['id' => Role::ANONYMOUS_ID, 'label' => 'anonymous user']) + ->grantPermission('access content'); + $role->save(); + + // Verify that the user does not have access to the entity create form of + // the group content type. + /** @var \Drupal\node\Access\NodeAddAccessCheck $node_access_check */ + $node_access_check = $this->container->get('access_check.node.add'); + $result = $node_access_check->access(User::getAnonymousUser(), $this->groupContentType); + $this->assertNotInstanceOf('\Drupal\Core\Access\AccessResultAllowed', $result); + + // Test that the user can access the entity create form when the permission + // to create group content is granted. Note that node access control is + // cached, so we need to reset it when we change permissions. + $this->container->get('entity.manager')->getAccessControlHandler('node')->resetCache(); + $role->grantPermission('create post content')->trustData()->save(); + $result = $node_access_check->access(User::getAnonymousUser(), $this->groupContentType); + $this->assertInstanceOf('\Drupal\Core\Access\AccessResultAllowed', $result); + } + +} diff --git a/tests/src/Kernel/Entity/FieldAccessTest.php b/tests/src/Kernel/Entity/FieldAccessTest.php index aa6a8c3ff..e198751a9 100644 --- a/tests/src/Kernel/Entity/FieldAccessTest.php +++ b/tests/src/Kernel/Entity/FieldAccessTest.php @@ -26,7 +26,7 @@ class FieldAccessTest extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['system', 'user', 'field', 'entity_reference', 'entity_test', 'og']; + public static $modules = ['system', 'user', 'field', 'entity_test', 'og']; /** * @var string diff --git a/tests/src/Kernel/Entity/FieldCreateTest.php b/tests/src/Kernel/Entity/FieldCreateTest.php index a5297b581..c2afb4c59 100644 --- a/tests/src/Kernel/Entity/FieldCreateTest.php +++ b/tests/src/Kernel/Entity/FieldCreateTest.php @@ -24,7 +24,7 @@ class FieldCreateTest extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['user', 'field', 'entity_reference', 'node', 'og', 'og_test', 'options', 'system']; + public static $modules = ['user', 'field', 'node', 'og', 'og_test', 'options', 'system']; /** * @var Array diff --git a/tests/src/Kernel/Entity/GetBundleByBundleTest.php b/tests/src/Kernel/Entity/GetBundleByBundleTest.php new file mode 100644 index 000000000..7702d7f4a --- /dev/null +++ b/tests/src/Kernel/Entity/GetBundleByBundleTest.php @@ -0,0 +1,557 @@ +installConfig(['og']); + $this->installEntitySchema('block_content'); + $this->installEntitySchema('node'); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + $this->groupManager = $this->container->get('og.group.manager'); + + // Create four groups of two different entity types. + for ($i = 0; $i < 2; $i++) { + $bundle = "group_$i"; + NodeType::create([ + 'name' => $this->randomString(), + 'type' => $bundle, + ])->save(); + Og::groupManager()->addGroup('node', $bundle); + + BlockContentType::create(['id' => $bundle])->save(); + Og::groupManager()->addGroup('block_content', $bundle); + } + } + + /** + * Tests retrieval of bundles that are referenc[ed|ing] bundles. + * + * This tests the retrieval of the relations between groups and group content + * and vice versa. The retrieval of groups that are referenced by group + * content is done by GroupManager::getGroupBundleIdsByGroupContenBundle() + * while GroupManager::getGroupContentBundleIdsByGroupBundle() handles the + * opposite case. + * + * Both methods are tested here in a single test since they are very similar, + * and not having to set up the entire relationship structure twice reduces + * the total test running time. + * + * @param array $relationships + * An array indicating the relationships between groups and group content + * bundles that need to be set up in the test. + * @param array $expected_group_by_group_content + * An array containing the expected results for the call to + * getGroupBundleIdsByGroupContentBundle(). + * @param array $expected_group_content_by_group + * An array containing the expected results for the 4 calls to + * getGroupContentBundleIdsByGroupBundle() that will be made in the test. + * + * @covers ::getGroupBundleIdsByGroupContentBundle + * @covers ::getGroupContentBundleIdsByGroupBundle + * + * @dataProvider getBundleIdsByBundleProvider + */ + public function testGetBundleIdsByBundle(array $relationships, array $expected_group_by_group_content, array $expected_group_content_by_group) { + // Set up the relations as indicated in the test. + foreach ($relationships as $group_content_entity_type_id => $group_content_bundle_ids) { + foreach ($group_content_bundle_ids as $group_content_bundle_id => $group_audience_fields) { + switch ($group_content_entity_type_id) { + case 'node': + NodeType::create([ + 'name' => $this->randomString(), + 'type' => $group_content_bundle_id, + ])->save(); + break; + case 'block_content': + BlockContentType::create(['id' => $group_content_bundle_id])->save(); + break; + } + foreach ($group_audience_fields as $group_audience_field_key => $group_audience_field_data) { + foreach ($group_audience_field_data as $group_entity_type_id => $group_bundle_ids) { + $settings = [ + 'field_name' => 'group_audience_' . $group_audience_field_key, + 'field_storage_config' => [ + 'settings' => [ + 'target_type' => $group_entity_type_id, + ], + ], + ]; + + if (!empty($group_bundle_ids)) { + $settings['field_config'] = [ + 'settings' => [ + 'handler_settings' => [ + 'target_bundles' => array_combine($group_bundle_ids, $group_bundle_ids), + ], + ], + ]; + } + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, $group_content_entity_type_id, $group_content_bundle_id, $settings); + } + } + } + } + + // Test ::getGroupBundleIdsByGroupContentBundle(). + foreach ($expected_group_by_group_content as $group_content_entity_type_id => $group_content_bundle_ids) { + foreach ($group_content_bundle_ids as $group_content_bundle_id => $expected_result) { + $this->assertEquals($expected_result, $this->groupManager->getGroupBundleIdsByGroupContentBundle($group_content_entity_type_id, $group_content_bundle_id)); + } + } + + // Test ::getGroupContentBundleIdsByGroupBundle(). + foreach (['node', 'block_content'] as $group_entity_type_id) { + for ($i = 0; $i < 2; $i++) { + $group_bundle_id = 'group_' . $i; + + // If the expected value is omitted, we expect an empty array. + $expected_result = !empty($expected_group_content_by_group[$group_entity_type_id][$group_bundle_id]) ? $expected_group_content_by_group[$group_entity_type_id][$group_bundle_id] : []; + + $this->assertEquals($expected_result, $this->groupManager->getGroupContentBundleIdsByGroupBundle($group_entity_type_id, $group_bundle_id)); + } + } + } + + /** + * Provides test data for testGetBundleIdsByBundle(). + * + * @return array + * An array of test properties. Each property is an indexed array with the + * following items: + * - An array indicating the relationships between groups and group content + * bundles that need to be set up in the test. + * - An array containing the expected results for the call to + * getGroupBundleIdsByGroupContentBundle(). + * - An array containing the expected results for the 4 calls to + * getGroupContentBundleIdsByGroupBundle() that will be made in the test. + * If an empty array is expected to be returned, this result is omitted. + */ + public function getBundleIdsByBundleProvider() { + return [ + // Test the simplest case: a single group content type that references a + // single group type. + [ + // The first parameter sets up the relations between groups and group + // content. + [ + // Creating group content of type 'node'. + 'node' => [ + // The first of which... + 'group_content_0' => [ + // Has a single group audience field, configured to reference + // groups of type 'node', targeting bundle '0'. + ['node' => ['group_0']], + ], + ], + ], + // The second parameter contains the expected result for the call to + // getGroupBundleIdsByGroupContentBundle(). In this case we expect group + // '0' of type 'node' to be referenced. + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0'], + ], + ], + ], + // Finally, the third parameter contains all 4 expected results for the + // call to getGroupContentBundleIdsByGroupBundle(). In this test only + // node 0 should be referenced, all others should be empty. + // Note that if the result is expected to be an empty array it can be + // omitted from this list. In reality all 4 possible permutations will + // always be tested. + [ + // When calling the method with entity type 'node' and bundle '0' we + // expect an array to be returned containing group content of type + // 'node', bundle '0'. + 'node' => [ + 'group_0' => ['node' => ['group_content_0' => 'group_content_0']], + // There is no group content referencing group '1', so we expect an + // empty array. This may be omitted. + 'group_1' => [], + ], + 'block_content' => [ + // This may be omitted. + 'group_0' => [], + // This may be omitted. + 'group_1' => [], + ], + ], + ], + + // When the bundles are left empty, all bundles should be referenced. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + ['node' => []], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => ['node' => ['group_content_0' => 'group_content_0']], + 'group_1' => ['node' => ['group_content_0' => 'group_content_0']], + ], + ], + ], + + // Test having two group audience fields referencing both group types. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + ['node' => []], + ['block_content' => ['group_0', 'group_1']], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + 'block_content' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => ['node' => ['group_content_0' => 'group_content_0']], + 'group_1' => ['node' => ['group_content_0' => 'group_content_0']], + ], + 'block_content' => [ + 'group_0' => ['node' => ['group_content_0' => 'group_content_0']], + 'group_1' => ['node' => ['group_content_0' => 'group_content_0']], + ], + ], + ], + + // Test having two group audience fields, one referencing node group 0 and + // the other entity test group 1. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + ['node' => ['group_0']], + ['block_content' => ['group_1']], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0'], + 'block_content' => ['group_1' => 'group_1'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => ['node' => ['group_content_0' => 'group_content_0']], + ], + 'block_content' => [ + 'group_1' => ['node' => ['group_content_0' => 'group_content_0']], + ], + ], + ], + + // Test having two different group content entity types referencing the + // same group. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + ['node' => ['group_0']], + ], + ], + 'block_content' => [ + 'group_content_0' => [ + ['node' => ['group_0']], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0'], + ], + ], + 'block_content' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => [ + 'node' => ['group_content_0' => 'group_content_0'], + 'block_content' => ['group_content_0' => 'group_content_0'], + ], + ], + ], + ], + + // Test having two identical group audience fields on the same group + // content type. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + ['node' => ['group_0']], + ['node' => ['group_0']], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => ['node' => ['group_content_0' => 'group_content_0']], + ], + ], + ], + + // Test having two group audience fields on the same group content type, + // each referencing a different group bundle of the same type. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + ['node' => ['group_0']], + ['node' => ['group_1']], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => ['node' => ['group_content_0' => 'group_content_0']], + 'group_1' => ['node' => ['group_content_0' => 'group_content_0']], + ], + ], + ], + + // Test having two group content types referencing the same group. The + // second group content type also references another group with a second + // group audience field. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + ['node' => ['group_0']], + ], + 'group_content_1' => [ + ['node' => ['group_0']], + ['node' => ['group_1']], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0'], + ], + 'group_content_1' => [ + 'node' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => [ + 'node' => [ + 'group_content_0' => 'group_content_0', + 'group_content_1' => 'group_content_1', + ], + ], + 'group_1' => ['node' => ['group_content_1' => 'group_content_1']], + ], + ], + ], + + // Bananas. + [ + // Group to group content relationship matrix. + [ + 'node' => [ + 'group_content_0' => [ + 0 => ['node' => ['group_0']], + 1 => ['block_content' => ['group_0', 'group_1']], + ], + 'group_content_1' => [ + 2 => ['block_content' => ['group_1']], + ], + ], + 'block_content' => [ + 'group_content_2' => [ + 0 => ['node' => ['group_0']], + 1 => ['node' => ['group_0']], + 2 => ['node' => ['group_1']], + ], + 'group_content_3' => [ + 3 => ['block_content' => ['group_0', 'group_1']], + ], + 'group_content_4' => [ + 4 => ['node' => ['group_0', 'group_1']], + 5 => ['block_content' => ['group_1']], + ], + ], + ], + // Expected result for getGroupBundleIdsByGroupContentBundle(). + [ + 'node' => [ + 'group_content_0' => [ + 'node' => ['group_0' => 'group_0'], + 'block_content' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + ], + 'group_content_1' => [ + 'block_content' => ['group_1' => 'group_1'], + ], + ], + 'block_content' => [ + 'group_content_2' => [ + 'node' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + ], + 'group_content_3' => [ + 'block_content' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + ], + 'group_content_4' => [ + 'node' => ['group_0' => 'group_0', 'group_1' => 'group_1'], + 'block_content' => ['group_1' => 'group_1'], + ], + ], + ], + // Expected result for getGroupContentBundleIdsByGroupBundle(). + [ + 'node' => [ + 'group_0' => [ + 'node' => ['group_content_0' => 'group_content_0'], + 'block_content' => [ + 'group_content_2' => 'group_content_2', + 'group_content_4' => 'group_content_4', + ], + ], + 'group_1' => [ + 'block_content' => [ + 'group_content_2' => 'group_content_2', + 'group_content_4' => 'group_content_4', + ], + ], + ], + 'block_content' => [ + 'group_0' => [ + 'node' => ['group_content_0' => 'group_content_0'], + 'block_content' => ['group_content_3' => 'group_content_3'], + ], + 'group_1' => [ + 'node' => [ + 'group_content_0' => 'group_content_0', + 'group_content_1' => 'group_content_1', + ], + 'block_content' => [ + 'group_content_3' => 'group_content_3', + 'group_content_4' => 'group_content_4', + ], + ], + ], + ], + ], + ]; + } + +} diff --git a/tests/src/Kernel/Entity/GetGroupContentTest.php b/tests/src/Kernel/Entity/GetGroupContentTest.php new file mode 100644 index 000000000..0d2082ff1 --- /dev/null +++ b/tests/src/Kernel/Entity/GetGroupContentTest.php @@ -0,0 +1,292 @@ +installConfig(['og']); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('node'); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + /** @var \Drupal\Core\Entity\EntityTypeManagerInterface entityTypeManager */ + $this->entityTypeManager = $this->container->get('entity_type.manager'); + + // Create group admin user. + $this->groupAdmin = User::create(['name' => $this->randomString()]); + $this->groupAdmin->save(); + } + + /** + * Test retrieval of group content that references a single group. + */ + public function testBasicGroupReferences() { + $groups = []; + + // Create two groups of different entity types. + $bundle = Unicode::strtolower($this->randomMachineName()); + NodeType::create([ + 'name' => $this->randomString(), + 'type' => $bundle, + ])->save(); + Og::groupManager()->addGroup('node', $bundle); + + $groups['node'] = Node::create([ + 'title' => $this->randomString(), + 'type' => $bundle, + 'uid' => $this->groupAdmin->id(), + ]); + $groups['node']->save(); + + // The Entity Test entity doesn't have 'real' bundles, so we don't need to + // create one, we can just add the group to the fake bundle. + $bundle = Unicode::strtolower($this->randomMachineName()); + Og::groupManager()->addGroup('entity_test', $bundle); + + $groups['entity_test'] = EntityTest::create([ + 'type' => $bundle, + 'name' => $this->randomString(), + 'uid' => $this->groupAdmin->id(), + ]); + $groups['entity_test']->save(); + + // Create 4 group content types, two for each entity type referencing each + // group. Create a group content entity for each. + $group_content = []; + foreach (['node', 'entity_test'] as $entity_type) { + foreach (['node', 'entity_test'] as $target_group_type) { + // Create the group content bundle if it's a node. Entity Test doesn't + // have real bundles. + $bundle = Unicode::strtolower($this->randomMachineName()); + if ($entity_type === 'node') { + NodeType::create([ + 'type' => $bundle, + 'name' => $this->randomString(), + ])->save(); + } + + // Create the groups audience field. + $field_name = "og_$target_group_type"; + $settings = [ + 'field_name' => $field_name, + 'field_storage_config' => [ + 'settings' => [ + 'target_type' => $groups[$target_group_type]->getEntityTypeId(), + ], + ], + 'field_config' => [ + 'settings' => [ + 'handler_settings' => [ + 'target_bundles' => [$groups[$target_group_type]->bundle() => $groups[$target_group_type]->bundle()], + ], + ], + ], + ]; + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, $entity_type, $bundle, $settings); + + // Create the group content entity. + $label_field = $entity_type === 'node' ? 'title' : 'name'; + $entity = $this->entityTypeManager->getStorage($entity_type)->create([ + $label_field => $this->randomString(), + 'type' => $bundle, + $field_name => [['target_id' => $groups[$target_group_type]->id()]], + ]); + $entity->save(); + + $group_content[$entity_type][$target_group_type] = $entity; + } + } + + // Check that Og::getGroupContent() returns the correct group content for + // each group. + foreach (['node', 'entity_test'] as $group_type) { + $result = Og::getGroupContentIds($groups[$group_type]); + foreach (['node', 'entity_test'] as $group_content_type) { + $this->assertEquals([$group_content[$group_content_type][$group_type]->id()], $result[$group_content_type], "The correct $group_content_type group content is returned for the $group_type group."); + } + // Test that the correct results are returned when filtering by entity + // type. + foreach (['node', 'entity_test'] as $filter) { + $result = Og::getGroupContentIds($groups[$group_type], [$filter]); + $this->assertEquals(1, count($result), "Only one entity type is returned when getting $group_type results filtered by $group_content_type group content."); + $this->assertEquals([$group_content[$filter][$group_type]->id()], $result[$filter], "The correct result is returned for the $group_type group, filtered by $group_content_type group content."); + } + } + } + + /** + * Test retrieval of group content that references multiple groups. + */ + public function testMultipleGroupReferences() { + $groups = []; + + // Create two groups. + $bundle = Unicode::strtolower($this->randomMachineName()); + NodeType::create([ + 'name' => $this->randomString(), + 'type' => $bundle, + ])->save(); + Og::groupManager()->addGroup('node', $bundle); + + for ($i = 0; $i < 2; $i++) { + $groups[$i] = Node::create([ + 'title' => $this->randomString(), + 'type' => $bundle, + 'uid' => $this->groupAdmin->id(), + ]); + $groups[$i]->save(); + } + + // Create a group content type. + $bundle = Unicode::strtolower($this->randomMachineName()); + + $settings = [ + 'field_storage_config' => [ + 'settings' => [ + 'target_type' => 'node', + ], + ], + ]; + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $settings); + + // Create a group content entity that references both groups. + $group_content = $this->entityTypeManager->getStorage('entity_test')->create([ + 'name' => $this->randomString(), + 'type' => $bundle, + OgGroupAudienceHelper::DEFAULT_FIELD => [ + ['target_id' => $groups[0]->id()], + ['target_id' => $groups[1]->id()], + ], + ]); + $group_content->save(); + + // Check that Og::getGroupContent() returns the group content entity for + // both groups. + $expected = ['entity_test' => [$group_content->id()]]; + foreach ($groups as $key => $groups) { + $result = Og::getGroupContentIds($groups); + $this->assertEquals($expected, $result, "The group content entity is returned for group $key."); + } + } + + /** + * Test retrieval of group content with multiple group audience fields. + */ + public function testMultipleGroupAudienceFields() { + $groups = []; + + // Create two groups of different entity types. + $bundle = Unicode::strtolower($this->randomMachineName()); + NodeType::create([ + 'name' => $this->randomString(), + 'type' => $bundle, + ])->save(); + Og::groupManager()->addGroup('node', $bundle); + + $groups['node'] = Node::create([ + 'title' => $this->randomString(), + 'type' => $bundle, + 'uid' => $this->groupAdmin->id() + ]); + $groups['node']->save(); + + // The Entity Test entity doesn't have 'real' bundles, so we don't need to + // create one, we can just add the group to the fake bundle. + $bundle = Unicode::strtolower($this->randomMachineName()); + Og::groupManager()->addGroup('entity_test', $bundle); + + $groups['entity_test'] = EntityTest::create([ + 'type' => $bundle, + 'name' => $this->randomString(), + 'uid' => $this->groupAdmin->id(), + ]); + $groups['entity_test']->save(); + + // Create a group content type with two group audience fields, one for each + // group. + $bundle = Unicode::strtolower($this->randomMachineName()); + foreach (['entity_test', 'node'] as $target_type) { + $settings = [ + 'field_name' => 'group_audience_' . $target_type, + 'field_storage_config' => [ + 'settings' => [ + 'target_type' => $target_type, + ], + ], + ]; + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $settings); + } + + // Create a group content entity that references both groups. + $values = [ + 'name' => $this->randomString(), + 'type' => $bundle, + ]; + foreach (['entity_test', 'node'] as $target_type) { + $values['group_audience_' . $target_type] = [ + ['target_id' => $groups[$target_type]->id()], + ]; + } + + $group_content = $this->entityTypeManager->getStorage('entity_test')->create($values); + $group_content->save(); + + // Check that Og::getGroupContent() returns the group content entity for + // both groups. + $expected = ['entity_test' => [$group_content->id()]]; + foreach ($groups as $key => $groups) { + $result = Og::getGroupContentIds($groups); + $this->assertEquals($expected, $result, "The group content entity is returned for group $key."); + } + } + +} diff --git a/tests/src/Kernel/Entity/GetGroupsTest.php b/tests/src/Kernel/Entity/GetGroupsTest.php new file mode 100644 index 000000000..b56fb8aa0 --- /dev/null +++ b/tests/src/Kernel/Entity/GetGroupsTest.php @@ -0,0 +1,250 @@ +installConfig(['og']); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('node'); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + /** @var \Drupal\Core\Entity\EntityTypeManagerInterface entityTypeManager */ + $this->entityTypeManager = $this->container->get('entity_type.manager'); + + $this->groups = []; + + // Create group admin user. + $group_admin = User::create(['name' => $this->randomString()]); + $group_admin->save(); + + // Create four groups of two different entity types. + for ($i = 0; $i < 2; $i++) { + $bundle = "node_$i"; + NodeType::create([ + 'name' => $this->randomString(), + 'type' => $bundle, + ])->save(); + Og::groupManager()->addGroup('node', $bundle); + + $group = Node::create([ + 'title' => $this->randomString(), + 'type' => $bundle, + 'uid' => $group_admin->id(), + ]); + $group->save(); + $this->groups['node'][] = $group; + + // The Entity Test entity doesn't have 'real' bundles, so we don't need to + // create one, we can just add the group to the fake bundle. + $bundle = "entity_test_$i"; + Og::groupManager()->addGroup('entity_test', $bundle); + + $group = EntityTest::create([ + 'type' => $bundle, + 'name' => $this->randomString(), + 'uid' => $group_admin->id(), + ]); + $group->save(); + $this->groups['entity_test'][] = $group; + } + + // Create a group content type with two group audience fields, one for each + // group. + $bundle = Unicode::strtolower($this->randomMachineName()); + foreach (['entity_test', 'node'] as $target_type) { + $settings = [ + 'field_name' => 'group_audience_' . $target_type, + 'field_storage_config' => [ + 'settings' => [ + 'target_type' => $target_type, + ], + ], + ]; + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $settings); + } + + // Create a group content entity that references all four groups. + $values = [ + 'name' => $this->randomString(), + 'type' => $bundle, + ]; + foreach (['entity_test', 'node'] as $target_type) { + foreach ($this->groups[$target_type] as $group) { + $values['group_audience_' . $target_type][] = [ + 'target_id' => $group->id(), + ]; + } + } + + $this->groupContent = $this->entityTypeManager->getStorage('entity_test')->create($values); + $this->groupContent->save(); + } + + /** + * Tests retrieval of groups IDs that are associated with given group content. + * + * @param string $group_type_id + * Optional group type ID to be passed as an argument to the method under + * test. + * @param string $group_bundle + * Optional group bundle to be passed as an argument to the method under + * test. + * @param array $expected + * An array containing the expected results to be returned. + * + * @covers ::getGroupIds + * @dataProvider groupContentProvider + */ + public function testGetGroupIds($group_type_id, $group_bundle, array $expected) { + $result = Og::getGroupIds($this->groupContent, $group_type_id, $group_bundle); + + // Check that the correct number of results is returned. + $this->assertEquals(count($expected, COUNT_RECURSIVE), count($result, COUNT_RECURSIVE)); + + // Check that all expected results are returned. + foreach ($expected as $expected_type => $expected_keys) { + foreach ($expected_keys as $expected_key) { + $this->assertTrue(in_array($this->groups[$expected_type][$expected_key]->id(), $result[$expected_type])); + } + } + } + + /** + * Tests retrieval of groups that are associated with a given group content. + * + * @param string $group_type_id + * Optional group type ID to be passed as an argument to the method under + * test. + * @param string $group_bundle + * Optional group bundle to be passed as an argument to the method under + * test. + * @param array $expected + * An array containing the expected results to be returned. + * + * @covers ::getGroups + * @dataProvider groupContentProvider + */ + public function testGetGroups($group_type_id, $group_bundle, array $expected) { + $result = Og::getGroups($this->groupContent, $group_type_id, $group_bundle); + + // Check that the correct number of results is returned. + $this->assertEquals(count($expected, COUNT_RECURSIVE), count($result, COUNT_RECURSIVE)); + + // Check that all expected results are returned. + foreach ($expected as $expected_type => $expected_keys) { + foreach ($expected_keys as $expected_key) { + /** @var \Drupal\Core\Entity\EntityInterface $expected_group */ + $expected_group = $this->groups[$expected_type][$expected_key]; + /** @var \Drupal\Core\Entity\EntityInterface $group */ + foreach ($result[$expected_type] as $key => $group) { + if ($group->getEntityTypeId() === $expected_group->getEntityTypeId() && $group->id() === $expected_group->id()) { + // The expected result was found. Continue the test. + continue 2; + } + } + // The expected result was not found. + $this->fail("The expected group of type $expected_type and key $expected_key is found."); + } + } + } + + /** + * Tests if the number of groups associated with group content is correct. + * + * @param string $group_type_id + * Optional group type ID to be passed as an argument to the method under + * test. + * @param string $group_bundle + * Optional group bundle to be passed as an argument to the method under + * test. + * @param array $expected + * An array containing the expected results to be returned. + * + * @covers ::getGroupCount + * @dataProvider groupContentProvider + */ + public function testGetGroupCount($group_type_id, $group_bundle, array $expected) { + $result = Og::getGroupCount($this->groupContent, $group_type_id, $group_bundle); + + // Check that the correct results is returned. + $this->assertEquals(count($expected, COUNT_RECURSIVE) - count($expected), $result); + } + + /** + * Provides test data. + * + * @return array + * An array of test properties. Each property is an indexed array with the + * following items: + * - An optional string indicating the group type ID to be returned. + * - An optional string indicating the group bundle to be returned. + * - An array containing the expected results to be returned. + */ + public function groupContentProvider() { + return [ + [NULL, NULL, ['node' => [0, 1], 'entity_test' => [0, 1]]], + ['node', NULL, ['node' => [0, 1]]], + ['entity_test', NULL, ['entity_test' => [0, 1]]], + ['node', 'node_0', ['node' => [0]]], + ['entity_test', 'entity_test_1', ['entity_test' => [1]]], + ]; + } + +} diff --git a/tests/src/Kernel/Entity/GetEntityGroupsTest.php b/tests/src/Kernel/Entity/GetUserGroupsTest.php similarity index 94% rename from tests/src/Kernel/Entity/GetEntityGroupsTest.php rename to tests/src/Kernel/Entity/GetUserGroupsTest.php index fe7db91cb..2c7736e73 100644 --- a/tests/src/Kernel/Entity/GetEntityGroupsTest.php +++ b/tests/src/Kernel/Entity/GetUserGroupsTest.php @@ -20,7 +20,7 @@ * * @group og */ -class GetEntityGroupsTest extends KernelTestBase { +class GetUserGroupsTest extends KernelTestBase { /** * {@inheritdoc} @@ -115,7 +115,7 @@ protected function setUp() { * Tests group owners have the correct groups. */ public function testOwnerGroupsOnly() { - $actual = Og::getEntityGroups($this->user1); + $actual = Og::getUserGroups($this->user1); $this->assertCount(1, $actual['entity_test']); $this->assertGroupExistsInResults($this->group1, $actual); @@ -124,7 +124,7 @@ public function testOwnerGroupsOnly() { $this->assertTrue(Og::isMember($this->group1, $this->user1)); $this->assertFalse(Og::isMember($this->group1, $this->user2)); - $actual = Og::getEntityGroups($this->user2); + $actual = Og::getUserGroups($this->user2); $this->assertCount(1, $actual['entity_test']); $this->assertGroupExistsInResults($this->group2, $actual); @@ -138,8 +138,8 @@ public function testOwnerGroupsOnly() { * Tests other groups users are added to. */ public function testOtherGroups() { - // Should be a part of no groups. - $this->assertEquals([], Og::getEntityGroups($this->user3)); + // Should not be a part of any groups. + $this->assertEquals([], Og::getUserGroups($this->user3)); $this->assertFalse(Og::isMember($this->group1, $this->user3)); $this->assertFalse(Og::isMember($this->group2, $this->user3)); @@ -150,7 +150,7 @@ public function testOtherGroups() { // Add user to group 1 should now return that group only. $this->createMembership($this->user3, $this->group1); - $actual = Og::getEntityGroups($this->user3); + $actual = Og::getUserGroups($this->user3); $this->assertCount(1, $actual['entity_test']); $this->assertGroupExistsInResults($this->group1, $actual); @@ -163,7 +163,7 @@ public function testOtherGroups() { // Add to group 2 should also return that. $this->createMembership($this->user3, $this->group2); - $actual = Og::getEntityGroups($this->user3); + $actual = Og::getUserGroups($this->user3); $this->assertCount(2, $actual['entity_test']); $this->assertGroupExistsInResults($this->group1, $actual); @@ -229,9 +229,8 @@ public function testIsMemberStates() { */ protected function createMembership($user, $group, $state = OgMembershipInterface::STATE_ACTIVE) { $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]) - ->setMemberEntityId($user->id()) - ->setMemberEntityType('user') - ->setGroupEntityid($group->id()) + ->setUser($user) + ->setEntityId($group->id()) ->setGroupEntityType($group->getEntityTypeId()) ->setState($state); $membership->save(); diff --git a/tests/src/Kernel/Entity/GetUserMembershipsTest.php b/tests/src/Kernel/Entity/GetUserMembershipsTest.php new file mode 100644 index 000000000..9da9dcfd4 --- /dev/null +++ b/tests/src/Kernel/Entity/GetUserMembershipsTest.php @@ -0,0 +1,231 @@ +installConfig(['og']); + $this->installEntitySchema('node'); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + // Create group admin user. + $group_admin = User::create(['name' => $this->randomString()]); + $group_admin->save(); + + // Create two groups. + for ($i = 0; $i < 2; $i++) { + $bundle = "node_$i"; + NodeType::create([ + 'name' => $this->randomString(), + 'type' => $bundle, + ])->save(); + Og::groupManager()->addGroup('node', $bundle); + + $group = Node::create([ + 'title' => $this->randomString(), + 'type' => $bundle, + 'uid' => $group_admin->id(), + ]); + $group->save(); + $this->groups[] = $group; + } + + // Create test users with different membership statuses in the two groups. + $matrix = [ + // A user which is an active member of the first group. + [OgMembershipInterface::STATE_ACTIVE, NULL], + + // A user which is a pending member of the second group. + [NULL, OgMembershipInterface::STATE_PENDING], + + // A user which is an active member of both groups. + [OgMembershipInterface::STATE_ACTIVE, OgMembershipInterface::STATE_ACTIVE], + + // A user which is a pending member of the first group and blocked in the + // second group. + [OgMembershipInterface::STATE_PENDING, OgMembershipInterface::STATE_BLOCKED], + + // A user which is not subscribed to either of the two groups. + [NULL, NULL], + ]; + + foreach ($matrix as $user_key => $statuses) { + $user = User::create(['name' => $this->randomString()]); + $user->save(); + $this->users[$user_key] = $user; + foreach ($statuses as $group_key => $status) { + $group = $this->groups[$group_key]; + if ($status) { + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($user->id()) + ->setEntityId($group->id()) + ->setGroupEntityType($group->getEntityTypeId()) + ->setState($status) + ->save(); + } + } + } + } + + /** + * Tests retrieval of OG Membership entities associated with a given user. + * + * @param int $index + * The array index in the $this->users array of the user to test. + * @param array $states + * Array with the states to retrieve. + * @param string $field_name + * The field name associated with the group. + * @param array $expected + * An array containing the expected results to be returned. + * + * @covers ::getUserMemberships + * @dataProvider membershipDataProvider + */ + public function testGetUserMemberships($index, array $states, $field_name, array $expected) { + $result = Og::getUserMemberships($this->users[$index], $states, $field_name); + + // Check that the correct number of results is returned. + $this->assertEquals(count($expected), count($result)); + + // Inspect the results that were returned. + foreach ($result as $key => $membership) { + // Check that all result items are OgMembership objects. + $this->assertInstanceOf('Drupal\og\OgMembershipInterface', $membership); + // Check that the results are keyed by OgMembership ID. + $this->assertEquals($membership->id(), $key); + } + + // Check that all expected results are returned. + foreach ($expected as $expected_group) { + $expected_id = $this->groups[$expected_group]->id(); + foreach ($result as $membership) { + if ($membership->getEntityId() === $expected_id) { + // Test successful: the expected result was found. + continue 2; + } + } + $this->fail("The expected group with ID $expected_id was not found."); + } + } + + /** + * Provides test data to test retrieval of memberships. + * + * @return array + * An array of test properties. Each property is an indexed array with the + * following items: + * - The key of the user in the $this->users array for which to retrieve + * memberships. + * - An array of membership states to filter on. + * - The field name to filter on. + * - An array containing the expected results to be returned. + */ + public function membershipDataProvider() { + return [ + // The first user is an active member of the first group. + // Query default values. The group should be returned. + [0, [], NULL, [0]], + // Filter by active state. + [0, [OgMembershipInterface::STATE_ACTIVE], NULL, [0]], + // Filter by active + pending state. + [0, [OgMembershipInterface::STATE_ACTIVE, OgMembershipInterface::STATE_PENDING], NULL, [0]], + // Filter by blocked + pending state. Since the user is active this should + // not return any matches. + [0, [OgMembershipInterface::STATE_BLOCKED, OgMembershipInterface::STATE_PENDING], NULL, []], + // Filter by a non-existing field name. This should not return any + // matches. + [0, [], 'non_existing_field_name', []], + + // The second user is a pending member of the second group. + // Query default values. The group should be returned. + [1, [], NULL, [1]], + // Filter by pending state. + [1, [OgMembershipInterface::STATE_PENDING], NULL, [1]], + // Filter by active state. The user is pending so this should not return + // any matches. + [1, [OgMembershipInterface::STATE_ACTIVE], NULL, []], + + // The third user is an active member of both groups. + // Query default values. Both groups should be returned. + [2, [], NULL, [0, 1]], + // Filter by active state. + [2, [OgMembershipInterface::STATE_ACTIVE], NULL, [0, 1]], + // Filter by blocked state. This should not return any matches. + [2, [OgMembershipInterface::STATE_BLOCKED], NULL, []], + + // The fourth user is a pending member of the first group and blocked in + // the second group. + // Query default values. Both groups should be returned. + [3, [], NULL, [0, 1]], + // Filter by active state. No results should be returned. + [3, [OgMembershipInterface::STATE_ACTIVE], NULL, []], + // Filter by pending state. + [3, [OgMembershipInterface::STATE_PENDING], NULL, [0]], + // Filter by blocked state. + [3, [OgMembershipInterface::STATE_BLOCKED], NULL, [1]], + // Filter by combinations of states. + [3, [OgMembershipInterface::STATE_ACTIVE, OgMembershipInterface::STATE_PENDING], NULL, [0]], + [3, [OgMembershipInterface::STATE_ACTIVE, OgMembershipInterface::STATE_PENDING, OgMembershipInterface::STATE_BLOCKED], NULL, [0, 1]], + [3, [OgMembershipInterface::STATE_ACTIVE, OgMembershipInterface::STATE_BLOCKED], NULL, [1]], + [3, [OgMembershipInterface::STATE_PENDING, OgMembershipInterface::STATE_BLOCKED], NULL, [0, 1]], + + // A user which is not subscribed to either of the two groups. + [4, [], NULL, []], + [4, [OgMembershipInterface::STATE_ACTIVE], NULL, []], + [4, [OgMembershipInterface::STATE_BLOCKED], NULL, []], + [4, [OgMembershipInterface::STATE_PENDING], NULL, []], + [4, [OgMembershipInterface::STATE_ACTIVE, OgMembershipInterface::STATE_PENDING, OgMembershipInterface::STATE_BLOCKED], NULL, []], + ]; + } + +} diff --git a/tests/src/Kernel/Entity/GroupAudienceTest.php b/tests/src/Kernel/Entity/GroupAudienceTest.php index 61a331e66..af3d3a97b 100644 --- a/tests/src/Kernel/Entity/GroupAudienceTest.php +++ b/tests/src/Kernel/Entity/GroupAudienceTest.php @@ -1,37 +1,39 @@ bundles[2]; // Test no values returned for a non-group content. - $this->assertEmpty(Og::getAllGroupAudienceFields('entity_test', $bundle)); + $this->assertEmpty(OgGroupAudienceHelper::getAllGroupAudienceFields('entity_test', $bundle)); // Set bundles as group content. $field_name1 = Unicode::strtolower($this->randomMachineName()); $field_name2 = Unicode::strtolower($this->randomMachineName()); - Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, ['field_name' => $field_name1]); - Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, ['field_name' => $field_name2]); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, ['field_name' => $field_name1]); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, ['field_name' => $field_name2]); - $field_names = Og::getAllGroupAudienceFields('entity_test', $bundle); - $this->assertEquals(array($field_name1, $field_name2), array_keys($field_names)); + $field_names = OgGroupAudienceHelper::getAllGroupAudienceFields('entity_test', $bundle); + $this->assertEquals([$field_name1, $field_name2], array_keys($field_names)); // Test Og::isGroupContent method, which is just a wrapper around - // Og::getAllGroupAudienceFields. + // OgGroupAudienceHelper::getAllGroupAudienceFields. $this->assertTrue(Og::isGroupContent('entity_test', $bundle)); $bundle = $this->bundles[3]; @@ -107,14 +109,13 @@ public function testGetAllGroupAudienceFieldsFilterGroupType() { ], ], ]; - Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $overrides); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $overrides); // Add a default field, which will use the "entity_test" as target type. - Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, ['field_name' => $field_name2]); - - $field_names = Og::getAllGroupAudienceFields('entity_test', $bundle, 'entity_test'); - $this->assertEquals(array($field_name2), array_keys($field_names)); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, ['field_name' => $field_name2]); + $field_names = OgGroupAudienceHelper::getAllGroupAudienceFields('entity_test', $bundle, 'entity_test'); + $this->assertEquals([$field_name2], array_keys($field_names)); } /** @@ -145,14 +146,14 @@ public function testGetAllGroupAudienceFieldsFilterGroupBundle() { ], ], ]; - Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $overrides); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $overrides); $overrides['field_name'] = $field_name2; $overrides['field_config']['settings']['handler_settings']['target_bundles'] = [$group_bundle2 => $group_bundle2]; - Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $overrides); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $bundle, $overrides); - $field_names = Og::getAllGroupAudienceFields('entity_test', $bundle, 'entity_test', $group_bundle1); - $this->assertEquals(array($field_name1), array_keys($field_names)); + $field_names = OgGroupAudienceHelper::getAllGroupAudienceFields('entity_test', $bundle, 'entity_test', $group_bundle1); + $this->assertEquals([$field_name1], array_keys($field_names)); } } diff --git a/tests/src/Kernel/Entity/OgMembershipReferenceItemListTest.php b/tests/src/Kernel/Entity/OgMembershipReferenceItemListTest.php index 899c918e2..d38e5aabc 100644 --- a/tests/src/Kernel/Entity/OgMembershipReferenceItemListTest.php +++ b/tests/src/Kernel/Entity/OgMembershipReferenceItemListTest.php @@ -10,13 +10,12 @@ use Drupal\Component\Utility\Unicode; use Drupal\Core\Entity\EntityInterface; use Drupal\entity_test\Entity\EntityTest; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; use Drupal\KernelTests\KernelTestBase; use Drupal\og\Entity\OgMembership; use Drupal\og\Og; use Drupal\og\OgGroupAudienceHelper; use Drupal\og\OgMembershipInterface; +use Drupal\user\Entity\User; /** * Tests OgMembershipReferenceItem and OgMembershipReferenceItemList classes. @@ -46,6 +45,7 @@ protected function setUp() { $this->installEntitySchema('entity_test'); $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); // Create several bundles. for ($i = 0; $i <= 4; $i++) { @@ -66,7 +66,7 @@ protected function setUp() { } $this->fieldName = strtolower($this->randomMachineName()); - Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $this->bundles[2], ['field_name' => $this->fieldName]); + Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'user', 'user', ['field_name' => $this->fieldName]); } /** @@ -76,30 +76,33 @@ public function testMembershipSave() { $run_query = function ($id) { return $this->container->get('entity.query')->get('og_membership') ->condition('field_name', $this->fieldName) - ->condition('member_entity_type', 'entity_test') - ->condition('member_entity_id', $id) - ->condition('group_entity_type', 'entity_test') + ->condition('uid', $id) + ->condition('entity_type', 'user') ->condition('state', OgMembershipInterface::STATE_ACTIVE) ->execute(); }; - $entity = EntityTest::create([ + $entity = User::create([ 'type' => $this->bundles[2], + 'name' => $this->randomString(), ]); // Assert no membership for a group membership with no references. $this->assertSame(count($entity->{$this->fieldName}), 0); $entity->save(); $this->assertSame(count($entity->{$this->fieldName}), 0); $this->assertSame($run_query($entity->id()), []); - $member_in_single_grpup = EntityTest::create([ + $member_in_single_grpup = User::create([ 'type' => $this->bundles[2], + 'name' => $this->randomString(), $this->fieldName => [['target_id' => $this->groups[0]->id()]], ]); + // Assert group membership is found before save. $this->assertSame(count($member_in_single_grpup->{$this->fieldName}), 1); $member_in_single_grpup->save(); $this->assertSame(count($member_in_single_grpup->{$this->fieldName}), 1); $this->assertSame(count($run_query($member_in_single_grpup->id())), 1); - $member_in_two_groups = EntityTest::create([ + $member_in_two_groups = User::create([ + 'name' => $this->randomString(), 'type' => $this->bundles[2], $this->fieldName => [ ['target_id' => $this->groups[0]->id()], @@ -121,10 +124,11 @@ public function testMembershipSave() { */ public function testMembershipLoad() { $reload = function (EntityInterface &$entity) { - $entity = \Drupal::entityTypeManager()->getStorage('entity_test')->loadUnchanged($entity->id()); + $entity = \Drupal::entityTypeManager()->getStorage('user')->loadUnchanged($entity->id()); }; - $entity = EntityTest::create([ + $entity = User::create([ 'type' => $this->bundles[2], + 'name' => $this->randomString(), ]); // Assert no membership for a group membership with no references. $this->assertSame(count($entity->{$this->fieldName}), 0); @@ -133,15 +137,14 @@ public function testMembershipLoad() { $membership = OgMembership::create([ 'type' => $this->bundles[0], 'field_name' => $this->fieldName, - 'member_entity_type' => 'entity_test', - 'member_entity_id' => $entity->id(), - 'group_entity_type' => 'entity_test', - 'group_entity_id' => $this->groups[0]->id(), + 'uid' => $entity->id(), + 'entity_type' => 'user', + 'entity_id' => $this->groups[0]->id(), ]); $membership->save(); $reload($entity); // Assert membership is picked up after a load from database. - $this->assertSame(count($entity->{$this->fieldName}), 1); + $this->assertSame(count($entity->{$this->fieldName}->getValue()), 1); } } diff --git a/tests/src/Kernel/Entity/OgMembershipRoleReferenceTest.php b/tests/src/Kernel/Entity/OgMembershipRoleReferenceTest.php new file mode 100644 index 000000000..3fbecdedc --- /dev/null +++ b/tests/src/Kernel/Entity/OgMembershipRoleReferenceTest.php @@ -0,0 +1,127 @@ +installConfig(['og']); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installSchema('system', 'sequences'); + + $this->groupBundle = $this->randomMachineName(); + + $this->user = User::create(['name' => $this->randomString()]); + $this->user->save(); + + $this->group = Node::create([ + 'title' => $this->randomString(), + 'uid' => $this->user->id(), + 'type' => $this->groupBundle, + ]); + } + + /** + * Testing OG membership role referencing. + */ + public function testRoleCreate() { + // Creating a content editor role. + $content_editor = OgRole::create(); + $content_editor + ->setGroupType('node') + ->setGroupBundle('group') + ->setId('content_editor') + ->setLabel('Content editor') + ->grantPermission('administer group'); + $content_editor->save(); + + // Create a group member role. + $group_member = OgRole::create(); + $group_member + ->setGroupType('node') + ->setGroupBundle('group') + ->setId('group_member') + ->setLabel('Group member'); + $group_member->save(); + + /** @var OgMembership $membership */ + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setGroupEntityType('node') + ->setEntityId($this->group->id()) + ->setUser($this->user) + // Assign only the content editor role for now. + ->setRoles([$content_editor->id()]) + ->save(); + + $roles_ids = $membership->getRolesIds(); + $this->assertTrue(in_array($content_editor->id(), $roles_ids), 'The membership has the content editor role.'); + + // Adding another role to the membership. + $membership->addRole($group_member->id()); + $roles_ids = $membership->getRolesIds(); + + $this->assertTrue(in_array($content_editor->id(), $roles_ids), 'The membership has the content editor role.'); + $this->assertTrue(in_array($group_member->id(), $roles_ids), 'The membership has the group member role.'); + + // Remove a role. + $membership->revokeRole($content_editor->id()); + + $roles_ids = $membership->getRolesIds(); + $this->assertFalse(in_array($content_editor->id(), $roles_ids), 'The membership does not have the content editor role after is has been revoked.'); + $this->assertTrue(in_array($group_member->id(), $roles_ids), 'The membership has the group member role.'); + + // Check if the role has permission from the membership. + $this->assertFalse($membership->hasPermission('administer group'), 'The user has permission to administer groups.'); + $membership->addRole($content_editor->id()); + $this->assertTrue($membership->hasPermission('administer group'), 'The user has permission to administer groups.'); + } + +} diff --git a/tests/src/Kernel/Entity/OgMembershipTest.php b/tests/src/Kernel/Entity/OgMembershipTest.php new file mode 100644 index 000000000..6d349067f --- /dev/null +++ b/tests/src/Kernel/Entity/OgMembershipTest.php @@ -0,0 +1,131 @@ +installConfig(['og']); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + // Create a bundle and add as a group + $group = EntityTest::create([ + 'type' => Unicode::strtolower($this->randomMachineName()), + 'name' => $this->randomString(), + ]); + + $group->save(); + $this->group = $group; + + // Add that as a group. + Og::groupManager()->addGroup('entity_test', $group->id()); + + // Create test user. + $user = User::create(['name' => $this->randomString()]); + $user->save(); + + $this->user = $user; + } + + /** + * Tests getting and setting users on OgMemberships. + * + * @covers ::getUser + * @covers ::setUser + */ + public function testGetSetUser() { + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($this->user->id()) + ->setEntityId($this->group->id()) + ->setGroupEntityType($this->group->getEntityTypeId()) + ->save(); + + // Check the user is returned. + $this->assertInstanceOf(UserInterface::class, $membership->getUser()); + $this->assertEquals($this->user->id(), $membership->getUser()->id()); + + // And after re-loading. + $membership = Og::membershipStorage()->loadUnchanged($membership->id()); + + $this->assertInstanceOf(UserInterface::class, $membership->getUser()); + $this->assertEquals($this->user->id(), $membership->getUser()->id()); + } + + /** + * Tests exceptions are thrown when trying to save a membership with no, or + * anonymous user. + * + * @covers ::getUser + * @dataProvider providerTestGetSetUserException + * @expectedException \Drupal\Core\Entity\EntityStorageException + */ + public function testGetSetUserException($user_value) { + /** @var OgMembership $membership */ + $membership = OgMembership::create(['type' => OgMembershipInterface::TYPE_DEFAULT]); + $membership + ->setUser($user_value) + ->setEntityId($this->group->id()) + ->setGroupEntityType($this->group->getEntityTypeId()) + ->save(); + } + + /** + * Data provider for testGetSetUserException. + */ + public function providerTestGetSetUserException() { + return [ + [NULL], + [0] + ]; + } + +} diff --git a/tests/src/Kernel/Entity/OgStandardReferenceItemTest.php b/tests/src/Kernel/Entity/OgStandardReferenceItemTest.php new file mode 100644 index 000000000..bde71a4f5 --- /dev/null +++ b/tests/src/Kernel/Entity/OgStandardReferenceItemTest.php @@ -0,0 +1,97 @@ +installConfig(['og']); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + // Create several bundles. + for ($i = 0; $i <= 2; $i++) { + $bundle = EntityTest::create([ + 'type' => Unicode::strtolower($this->randomMachineName()), + 'name' => $this->randomString(), + ]); + + $bundle->save(); + $this->bundles[] = $bundle->id(); + } + for ($i = 0 ; $i < 2; $i++) { + $bundle = $this->bundles[$i]; + Og::groupManager()->addGroup('entity_test', $bundle); + $group = EntityTest::create(['type' => $bundle]); + $group->save(); + $this->groups[] = $group; + } + $this->fieldName = strtolower($this->randomMachineName()); + + Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', $this->bundles[2], ['field_name' => $this->fieldName]); + } + + /** + * Testing referencing of non-user entity to groups. + */ + public function testStandardReference() { + $groups_query = function($gid) { + return $this->container->get('entity.query')->get('entity_test') + ->condition($this->fieldName, $gid) + ->execute(); + }; + + $entity = EntityTest::create([ + 'type' => $this->bundles[2], + 'name' => $this->randomString(), + ]); + $entity->save(); + + $this->assertEmpty($groups_query($this->groups[0]->id())); + + $entity = EntityTest::create([ + 'type' => $this->bundles[2], + 'name' => $this->randomString(), + $this->fieldName => [['target_id' => $this->groups[1]->id()]], + ]); + $entity->save(); + + $this->assertEmpty($groups_query($this->groups[0]->id())); + $this->assertEquals(array_keys($groups_query($this->groups[1]->id())), [$entity->id()]); + } + +} diff --git a/tests/src/Kernel/Entity/ReferenceStringIdTest.php b/tests/src/Kernel/Entity/ReferenceStringIdTest.php new file mode 100644 index 000000000..e08082394 --- /dev/null +++ b/tests/src/Kernel/Entity/ReferenceStringIdTest.php @@ -0,0 +1,133 @@ +installConfig(['og']); + $this->installEntitySchema('entity_test_string_id'); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installSchema('system', 'sequences'); + + // Create two bundles, one will serve as group, the other as group content. + for ($i = 0; $i < 2; $i++) { + $bundle = EntityTestStringId::create([ + 'type' => Unicode::strtolower($this->randomMachineName()), + 'name' => $this->randomString(), + 'id' => $this->randomMachineName(), + ]); + $bundle->save(); + $this->bundles[] = $bundle->id(); + } + + // Create a group with a string as an ID. + $group = EntityTestStringId::create([ + 'type' => $this->bundles[0], + 'id' => $this->randomMachineName(), + ]); + $group->save(); + $this->group = $group; + + // Let OG mark the group entity type as a group. + Og::groupManager()->addGroup('entity_test_string_id', $this->bundles[0]); + + // Add a group audience field to the second bundle, this will turn it into a + // group content type. + $this->fieldName = strtolower($this->randomMachineName()); + Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test_string_id', $this->bundles[1], [ + 'field_name' => $this->fieldName, + ]); + + // Add a group audience field to the User entity, so that we can test if + // users can become members of the test group. + Og::CreateField(OgGroupAudienceHelper::DEFAULT_FIELD, 'user', 'user', [ + 'field_name' => $this->fieldName, + ]); + } + + /** + * Test if a group that uses a string as ID can be referenced. + */ + public function testReferencingStringIds() { + // Create a group content entity that references the group. + $entity = EntityTestStringId::create([ + 'type' => $this->bundles[1], + 'name' => $this->randomString(), + 'id' => $this->randomMachineName(), + $this->fieldName => [['target_id' => $this->group->id()]], + ]); + $entity->save(); + + // Check that the group content entity is referenced. + $references = $this->container->get('entity.query')->get('entity_test_string_id') + ->condition($this->fieldName, $this->group->id()) + ->execute(); + $this->assertEquals([$entity->id()], array_keys($references), 'The correct group is referenced.'); + + // Create a user and make it a member of the group. + $user = User::create(['name' => $this->randomString()]); + $user->save(); + $membership = OgMembership::create([ + 'uid' => $user->id(), + 'type' => 'user', + 'entity_type' => 'entity_test_string_id', + 'entity_id' => $this->group->id(), + 'field_name' => $this->fieldName, + ]); + $membership->save(); + + // Reload the user and check that its group audience field correctly + // references the entity. + $user = \Drupal::entityTypeManager()->getStorage('user')->loadUnchanged($user->id()); + $this->assertEquals($this->group->id(), $user->{$this->fieldName}->getValue()[0]['target_id']); + } + +} diff --git a/tests/src/Kernel/EntityReference/Views/OgStandardReferenceRelationshipTest.php b/tests/src/Kernel/EntityReference/Views/OgStandardReferenceRelationshipTest.php new file mode 100644 index 000000000..669754c76 --- /dev/null +++ b/tests/src/Kernel/EntityReference/Views/OgStandardReferenceRelationshipTest.php @@ -0,0 +1,234 @@ +installEntitySchema('user'); + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('entity_test_mul'); + + // Create reference from entity_test to entity_test_mul. + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test', 'entity_test', ['field_name' => 'field_test_data', 'field_storage_config' => ['settings' => ['target_type' => 'entity_test_mul']]]); + + // Create reference from entity_test_mul to entity_test. + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'entity_test_mul', 'entity_test_mul', ['field_name' => 'field_data_test', 'field_storage_config' => ['settings' => ['target_type' => 'entity_test']]]); + + ViewTestData::createTestViews(get_class($this), ['og_standard_reference_test_views']); + } + + /** + * Tests using the views relationship. + */ + public function testNoDataTableRelationship() { + + // Create some test entities which link each other. + $referenced_entity = EntityTestMul::create(); + $referenced_entity->save(); + + $entity = EntityTest::create(); + $entity->field_test_data->target_id = $referenced_entity->id(); + $entity->save(); + $this->assertEqual($entity->field_test_data[0]->entity->id(), $referenced_entity->id()); + $this->entities[] = $entity; + + $entity = EntityTest::create(); + $entity->field_test_data->target_id = $referenced_entity->id(); + $entity->save(); + $this->assertEqual($entity->field_test_data[0]->entity->id(), $referenced_entity->id()); + $this->entities[] = $entity; + + Views::viewsData()->clear(); + + // Check the generated views data. + $views_data = Views::viewsData()->get('entity_test__field_test_data'); + $this->assertEqual($views_data['field_test_data']['relationship']['id'], 'standard'); + $this->assertEqual($views_data['field_test_data']['relationship']['base'], 'entity_test_mul_property_data'); + $this->assertEqual($views_data['field_test_data']['relationship']['base field'], 'id'); + $this->assertEqual($views_data['field_test_data']['relationship']['relationship field'], 'field_test_data_target_id'); + $this->assertEqual($views_data['field_test_data']['relationship']['entity type'], 'entity_test_mul'); + + // Check the backwards reference. + $views_data = Views::viewsData()->get('entity_test_mul_property_data'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['id'], 'entity_reverse'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['base'], 'entity_test'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['base field'], 'id'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['field table'], 'entity_test__field_test_data'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['field field'], 'field_test_data_target_id'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['field_name'], 'field_test_data'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['entity_type'], 'entity_test'); + $this->assertEqual($views_data['reverse__entity_test__field_test_data']['relationship']['join_extra'][0], ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE]); + + // Check an actual test view. + $view = Views::getView('test_og_standard_reference_entity_test_view'); + $this->executeView($view); + /** @var \Drupal\views\ResultRow $row */ + foreach ($view->result as $index => $row) { + // Check that the actual ID of the entity is the expected one. + $this->assertEqual($row->id, $this->entities[$index]->id()); + + // Also check that we have the correct result entity. + $this->assertEqual($row->_entity->id(), $this->entities[$index]->id()); + + // Test the forward relationship. + $this->assertEqual($row->entity_test_mul_property_data_entity_test__field_test_data_i, 1); + + // Test that the correct relationship entity is on the row. + $this->assertEqual($row->_relationship_entities['field_test_data']->id(), 1); + $this->assertEqual($row->_relationship_entities['field_test_data']->bundle(), 'entity_test_mul'); + + } + + // Check the backwards reference view. + $view = Views::getView('test_og_standard_reference_reverse_entity_test_view'); + $this->executeView($view); + /** @var \Drupal\views\ResultRow $row */ + foreach ($view->result as $index => $row) { + $this->assertEqual($row->id, 1); + $this->assertEqual($row->_entity->id(), 1); + + // Test the backwards relationship. + $this->assertEqual($row->field_test_data_entity_test_mul_property_data_id, $this->entities[$index]->id()); + + // Test that the correct relationship entity is on the row. + $this->assertEqual($row->_relationship_entities['reverse__entity_test__field_test_data']->id(), $this->entities[$index]->id()); + $this->assertEqual($row->_relationship_entities['reverse__entity_test__field_test_data']->bundle(), 'entity_test'); + } + } + + /** + * Tests views data generated for relationship. + * + * @see entity_reference_field_views_data() + */ + public function testDataTableRelationship() { + + // Create some test entities which link each other. + $referenced_entity = EntityTest::create(); + $referenced_entity->save(); + + $entity = EntityTestMul::create(); + $entity->field_data_test->target_id = $referenced_entity->id(); + $entity->save(); + $this->assertEqual($entity->field_data_test[0]->entity->id(), $referenced_entity->id()); + $this->entities[] = $entity; + + $entity = EntityTestMul::create(); + $entity->field_data_test->target_id = $referenced_entity->id(); + $entity->save(); + $this->assertEqual($entity->field_data_test[0]->entity->id(), $referenced_entity->id()); + $this->entities[] = $entity; + + Views::viewsData()->clear(); + + // Check the generated views data. + $views_data = Views::viewsData()->get('entity_test_mul__field_data_test'); + $this->assertEqual($views_data['field_data_test']['relationship']['id'], 'standard'); + $this->assertEqual($views_data['field_data_test']['relationship']['base'], 'entity_test'); + $this->assertEqual($views_data['field_data_test']['relationship']['base field'], 'id'); + $this->assertEqual($views_data['field_data_test']['relationship']['relationship field'], 'field_data_test_target_id'); + $this->assertEqual($views_data['field_data_test']['relationship']['entity type'], 'entity_test'); + + // Check the backwards reference. + $views_data = Views::viewsData()->get('entity_test'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['id'], 'entity_reverse'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['base'], 'entity_test_mul_property_data'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['base field'], 'id'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['field table'], 'entity_test_mul__field_data_test'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['field field'], 'field_data_test_target_id'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['field_name'], 'field_data_test'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['entity_type'], 'entity_test_mul'); + $this->assertEqual($views_data['reverse__entity_test_mul__field_data_test']['relationship']['join_extra'][0], ['field' => 'deleted', 'value' => 0, 'numeric' => TRUE]); + + // Check an actual test view. + $view = Views::getView('test_og_standard_reference_entity_test_mul_view'); + $this->executeView($view); + /** @var \Drupal\views\ResultRow $row */ + foreach ($view->result as $index => $row) { + // Check that the actual ID of the entity is the expected one. + $this->assertEqual($row->id, $this->entities[$index]->id()); + + // Also check that we have the correct result entity. + $this->assertEqual($row->_entity->id(), $this->entities[$index]->id()); + + // Test the forward relationship. + $this->assertEqual($row->entity_test_entity_test_mul__field_data_test_id, 1); + + // Test that the correct relationship entity is on the row. + $this->assertEqual($row->_relationship_entities['field_data_test']->id(), 1); + $this->assertEqual($row->_relationship_entities['field_data_test']->bundle(), 'entity_test'); + + } + + // Check the backwards reference view. + $view = Views::getView('test_og_standard_reference_reverse_entity_test_mul_view'); + $this->executeView($view); + /** @var \Drupal\views\ResultRow $row */ + foreach ($view->result as $index => $row) { + $this->assertEqual($row->id, 1); + $this->assertEqual($row->_entity->id(), 1); + + // Test the backwards relationship. + $this->assertEqual($row->field_data_test_entity_test_id, $this->entities[$index]->id()); + + // Test that the correct relationship entity is on the row. + $this->assertEqual($row->_relationship_entities['reverse__entity_test_mul__field_data_test']->id(), $this->entities[$index]->id()); + $this->assertEqual($row->_relationship_entities['reverse__entity_test_mul__field_data_test']->bundle(), 'entity_test_mul'); + } + } + +} diff --git a/tests/src/Kernel/Field/AudienceFieldFormatterTest.php b/tests/src/Kernel/Field/AudienceFieldFormatterTest.php new file mode 100644 index 000000000..757d47225 --- /dev/null +++ b/tests/src/Kernel/Field/AudienceFieldFormatterTest.php @@ -0,0 +1,47 @@ +get('plugin.manager.field.formatter'); + + $expected = [ + 'entity_reference_entity_id', + 'entity_reference_entity_view', + 'entity_reference_label', + ]; + + foreach ([OgGroupAudienceHelper::NON_USER_TO_GROUP_REFERENCE_FIELD_TYPE, OgGroupAudienceHelper::USER_TO_GROUP_REFERENCE_FIELD_TYPE] as $field_type) { + $actual = array_keys($formatter_manager->getOptions($field_type)); + sort($actual); + $this->assertEquals($expected, $actual); + } + } + +} diff --git a/tests/src/Kernel/OgDeleteOrphansTest.php b/tests/src/Kernel/OgDeleteOrphansTest.php new file mode 100644 index 000000000..32861031c --- /dev/null +++ b/tests/src/Kernel/OgDeleteOrphansTest.php @@ -0,0 +1,255 @@ +installConfig(['og']); + $this->installEntitySchema('og_membership'); + $this->installEntitySchema('user'); + $this->installEntitySchema('node'); + $this->installSchema('node', 'node_access'); + $this->installSchema('system', ['queue', 'sequences']); + + /** @var \Drupal\og\OgDeleteOrphansPluginManager ogDeleteOrphansPluginManager */ + $this->ogDeleteOrphansPluginManager = \Drupal::service('plugin.manager.og.delete_orphans'); + + // Create a group entity type. + $group_bundle = Unicode::strtolower($this->randomMachineName()); + NodeType::create([ + 'type' => $group_bundle, + 'name' => $this->randomString(), + ])->save(); + Og::groupManager()->addGroup('node', $group_bundle); + + // Create a group content entity type. + $group_content_bundle = Unicode::strtolower($this->randomMachineName()); + NodeType::create([ + 'type' => $group_content_bundle, + 'name' => $this->randomString(), + ])->save(); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'node', $group_content_bundle); + + // Create group admin user. + $group_admin = User::create(['name' => $this->randomString()]); + $group_admin->save(); + + // Create a group. + $this->group = Node::create([ + 'title' => $this->randomString(), + 'type' => $group_bundle, + 'uid' => $group_admin->id(), + ]); + $this->group->save(); + + // Create a group content item. + $group_content = Node::create([ + 'title' => $this->randomString(), + 'type' => $group_content_bundle, + OgGroupAudienceHelper::DEFAULT_FIELD => [['target_id' => $this->group->id()]], + ]); + $group_content->save(); + } + + /** + * Tests that orphaned group content is deleted when the group is deleted. + * + * @param string $plugin_id + * The machine name of the plugin under test. + * @param bool $run_cron + * Whether or not cron jobs should be run as part of the test. + * @param bool $asynchronous + * Whether or not the actual deletion of the orphans happens in an + * asynchronous operation (e.g. pressing the button that launches the batch + * process). + * @param string $queue_id + * The ID of the queue that is used by the plugin under test. + * + * @dataProvider ogDeleteOrphansPluginProvider + */ + public function testDeleteOrphans($plugin_id, $run_cron, $asynchronous, $queue_id) { + // Turn on deletion of orphans in the configuration and configure the chosen + // plugin. + $this->config('og.settings') + ->set('delete_orphans', TRUE) + ->set('delete_orphans_plugin_id', $plugin_id) + ->save(); + + // Check that the queue is initially empty. + $this->assertQueueCount($queue_id, 0); + + // Check that the group owner has initially been subscribed to the group. + $this->assertUserMembershipCount(1); + + // Delete the group. + $this->group->delete(); + + // Check that 2 orphans are queued for asynchronous processing: 1 group + // content item and 1 user membership. + if ($asynchronous) { + $this->assertQueueCount($queue_id, 2); + } + + // Run cron jobs if needed. + if ($run_cron) { + $this->container->get('cron')->run(); + } + + // Simulate the initiation of the queue process by an asynchronous operation + // (such as pressing the button that starts a batch operation). + if ($asynchronous) { + $this->process($queue_id, $plugin_id); + } + + // Verify the group content is deleted. + $this->assertFalse($this->group_content, 'The orphaned node is deleted.'); + + // Verify that the user membership is now deleted. + $this->assertUserMembershipCount(0); + + // Check that the queue is now empty. + $this->assertQueueCount($queue_id, 0); + } + + /** + * Tests that orphaned content is not deleted when the option is disabled. + * + * @param string $plugin_id + * The machine name of the plugin under test. + * @param bool $run_cron + * Whether or not cron jobs should be run as part of the test. Unused in + * this test. + * @param bool $asynchronous + * Whether or not the actual deletion of the orphans happens in an + * asynchronous operation (e.g. pressing the button that launches the batch + * process). Unused in this test. + * @param string $queue_id + * The ID of the queue that is used by the plugin under test. + * + * @dataProvider ogDeleteOrphansPluginProvider + */ + function testDisabled($plugin_id, $run_cron, $asynchronous, $queue_id) { + // Disable deletion of orphans in the configuration and configure the chosen + // plugin. + $this->config('og.settings') + ->set('delete_orphans', FALSE) + ->set('delete_orphans_plugin_id', $plugin_id) + ->save(); + + // Delete the group. + $this->group->delete(); + + // Check that no orphans are queued for deletion. + $this->assertQueueCount($queue_id, 0); + } + + /** + * Provides OgDeleteOrphans plugins for the tests. + * + * @return array + * An array of test properties. Each property is an indexed array with the + * following items: + * - A string containing the plugin name being tested. + * - A boolean indicating whether or not cron jobs should be run. + * - A boolean indicating whether the deletion happens in an asynchronous + * process. + * - A string defining the queue that is used by the plugin. + */ + public function ogDeleteOrphansPluginProvider() { + return [ + ['batch', FALSE, TRUE, 'og_orphaned_group_content'], + ['cron', TRUE, FALSE, 'og_orphaned_group_content_cron'], + ['simple', FALSE, FALSE, 'og_orphaned_group_content'], + ]; + } + + /** + * Returns the number of items a given queue contains. + * + * @param string $queue_id + * The ID of the queue for which to count the items. + */ + protected function getQueueCount($queue_id) { + return $this->container->get('queue')->get($queue_id)->numberOfItems(); + } + + /** + * Checks that the given queue contains the expected number of items. + * + * @param string $queue_id + * The ID of the queue to check. + * @param int $count + * The expected number of items in the queue. + */ + protected function assertQueueCount($queue_id, $count) { + $this->assertEquals($count, $this->getQueueCount($queue_id)); + } + + /** + * Checks the number of user memberships. + * + * @param int $expected + * The expected number of user memberships. + */ + protected function assertUserMembershipCount($expected) { + $count = \Drupal::entityQuery('og_membership')->count()->execute(); + $this->assertEquals($expected, $count); + } + + /** + * Processes the given queue. + * + * @param string $queue_id + * The ID of the queue to process. + * @param string $plugin_id + * The ID of the plugin that is responsible for processing the queue. + */ + protected function process($queue_id, $plugin_id) { + /** @var \Drupal\og\OgDeleteOrphansInterface $plugin */ + $plugin = $this->ogDeleteOrphansPluginManager->createInstance($plugin_id, []); + while ($this->getQueueCount($queue_id) > 0) { + $plugin->process(); + } + } + +} diff --git a/tests/src/Kernel/PermissionEventTest.php b/tests/src/Kernel/PermissionEventTest.php new file mode 100644 index 000000000..d06bf5c8b --- /dev/null +++ b/tests/src/Kernel/PermissionEventTest.php @@ -0,0 +1,148 @@ +eventDispatcher = $this->container->get('event_dispatcher'); + + // Create a group entity type. + $this->groupBundleId = 'test_group'; + NodeType::create([ + 'type' => $this->groupBundleId, + 'name' => $this->randomString(), + ])->save(); + Og::groupManager()->addGroup('node', $this->groupBundleId); + + // Create a group content entity type. + $group_content_bundle_id = 'test_group_content'; + NodeType::create([ + 'type' => $group_content_bundle_id, + 'name' => $this->randomString(), + ])->save(); + Og::createField(OgGroupAudienceHelper::DEFAULT_FIELD, 'node', $group_content_bundle_id); + } + + /** + * Tests that the two OG modules can provide their own OG permissions. + * + * Some permissions (such as 'subscribe', 'manage members', etc.) are + * available for all group types. In addition to this there are also OG + * permissions for creating, editing and deleting the group content that + * associated with the group. + * + * In this test we will check that the correct permissions are generated for + * our test group (which includes permissions to create, edit and delete group + * content of type 'test_group_content'), as well as a control group which + * doesn't have any group content - in this case it should only return the + * default permissions that are available to all group types. + * + * @param bool $test_group_content_permissions + * TRUE to check the permissions expected for a group type that has group + * content of type 'test_group_content'. FALSE to only check for the + * expected default permissions that are valid for any group type. + * @param array $expected_permissions + * An array of permission names that are expected to be returned. + * + * @dataProvider permissionEventDataProvider + */ + public function testPermissionEventIntegration($test_group_content_permissions, $expected_permissions) { + $entity_type_id = $test_group_content_permissions ? 'node' : $this->randomMachineName(); + $bundle_id = $test_group_content_permissions ? $this->groupBundleId : $this->randomMachineName(); + + // Retrieve the permissions from the listeners. + /** @var PermissionEvent $permission_event */ + $event = new PermissionEvent($entity_type_id, $bundle_id); + $permission_event = $this->eventDispatcher->dispatch(PermissionEventInterface::EVENT_NAME, $event); + $actual_permissions = array_keys($permission_event->getPermissions()); + + // Sort the permission arrays so they can be compared. + sort($expected_permissions); + sort($actual_permissions); + + $this->assertEquals($expected_permissions, $actual_permissions); + } + + /** + * Provides expected results for the testPermissionEventIntegration test. + * + * @return array + * An array of test properties. Each property is an indexed array with the + * following items: + * - A boolean indication whether or not to request permissions for a group + * that has group content of type 'test_group_content'. If FALSE only the + * default permissions that are valid for any group type are returned. + * - An array of permission names that are expected to be returned. + */ + public function permissionEventDataProvider() { + $default_permissions = [ + 'add user', + 'administer group', + 'approve and deny subscription', + 'manage members', + 'manage permissions', + 'manage roles', + 'subscribe without approval', + 'subscribe', + 'unsubscribe', + 'update group', + ]; + $group_content_permissions = [ + 'create test_group_content node', + 'delete any test_group_content node', + 'delete own test_group_content node', + 'update any test_group_content node', + 'update own test_group_content node', + ]; + return [ + [FALSE, $default_permissions], + [TRUE, array_merge($default_permissions, $group_content_permissions)], + ]; + } + +} diff --git a/tests/src/Unit/CheckFieldCardinalityTest.php b/tests/src/Unit/CheckFieldCardinalityTest.php index e17051a4d..ac72363e6 100644 --- a/tests/src/Unit/CheckFieldCardinalityTest.php +++ b/tests/src/Unit/CheckFieldCardinalityTest.php @@ -86,7 +86,7 @@ public function testFieldCardinality($field_count, $cardinality, $expected) { ->willReturn($field_storage_definition_prophecy->reveal()) ->shouldBeCalled(); $field_definition_prophecy->getType() - ->willReturn('og_membership_reference') + ->willReturn(OgGroupAudienceHelper::NON_USER_TO_GROUP_REFERENCE_FIELD_TYPE) ->shouldBeCalled(); $entity_prophecy = $this->prophesize(ContentEntityInterface::class); diff --git a/tests/src/Unit/DefaultRoleEventTest.php b/tests/src/Unit/DefaultRoleEventTest.php new file mode 100644 index 000000000..3ce72d441 --- /dev/null +++ b/tests/src/Unit/DefaultRoleEventTest.php @@ -0,0 +1,485 @@ +setRoles($roles); + + foreach ($roles as $name => $role) { + $this->assertRoleEquals($role, $event->getRole($name)); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::getRoles + * @covers ::setRoles + * + * @dataProvider defaultRoleProvider + */ + public function testGetRoles($roles) { + $event = new DefaultRoleEvent(); + $event->setRoles($roles); + + $actual_roles = $event->getRoles(); + foreach ($roles as $name => $role) { + $this->assertRoleEquals($role, $actual_roles[$name]); + } + + $this->assertEquals(count($roles), count($actual_roles)); + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::addRole + * + * @dataProvider defaultRoleProvider + */ + public function testAddRole($roles) { + $event = new DefaultRoleEvent(); + foreach ($roles as $name => $role) { + $this->assertFalse($event->hasRole($name)); + $event->addRole($name, $role); + $this->assertRoleEquals($role, $event->getRole($name)); + + // Adding a role a second time should throw an exception. + try { + $event->addRole($name, $role); + $this->fail('It should not be possible to add a role with the same name a second time.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. + } + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::addRoles + * + * @dataProvider defaultRoleProvider + */ + public function testAddRoles($roles) { + $event = new DefaultRoleEvent(); + $event->addRoles($roles); + + $actual_roles = $event->getRoles(); + foreach ($roles as $name => $role) { + $this->assertRoleEquals($role, $actual_roles[$name]); + } + + $this->assertEquals(count($roles), count($actual_roles)); + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::setRole + * + * @dataProvider defaultRoleProvider + */ + public function testSetRole($roles) { + $event = new DefaultRoleEvent(); + foreach ($roles as $name => $role) { + $this->assertFalse($event->hasRole($name)); + $event->setRole($name, $role); + $this->assertRoleEquals($role, $event->getRole($name)); + + // Setting a role a second time should be possible. No exception should be + // thrown. + $event->setRole($name, $role); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::deleteRole + * + * @dataProvider defaultRoleProvider + */ + public function testDeleteRole($roles) { + $event = new DefaultRoleEvent(); + $event->setRoles($roles); + + foreach ($roles as $name => $role) { + $this->assertTrue($event->hasRole($name)); + $event->deleteRole($name); + $this->assertFalse($event->hasRole($name)); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::hasRole + * + * @dataProvider defaultRoleProvider + */ + public function testHasRole($roles) { + $event = new DefaultRoleEvent(); + foreach ($roles as $name => $role) { + $this->assertFalse($event->hasRole($name)); + $event->addRole($name, $role); + $this->assertTrue($event->hasRole($name)); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::offsetGet + * + * @dataProvider defaultRoleProvider + */ + public function testOffsetGet($roles) { + $event = new DefaultRoleEvent(); + $event->setRoles($roles); + + foreach ($roles as $name => $role) { + $this->assertRoleEquals($role, $event[$name]); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::offsetSet + * + * @dataProvider defaultRoleProvider + */ + public function testOffsetSet($roles) { + $event = new DefaultRoleEvent(); + + foreach ($roles as $name => $role) { + $this->assertFalse($event->hasRole($name)); + $event[$name] = $role; + $this->assertRoleEquals($role, $event->getRole($name)); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::offsetUnset + * + * @dataProvider defaultRoleProvider + */ + public function testOffsetUnset($roles) { + $event = new DefaultRoleEvent(); + $event->setRoles($roles); + + foreach ($roles as $name => $role) { + $this->assertTrue($event->hasRole($name)); + unset($event[$name]); + $this->assertFalse($event->hasRole($name)); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::offsetExists + * + * @dataProvider defaultRoleProvider + */ + public function testOffsetExists($roles) { + $event = new DefaultRoleEvent(); + foreach ($roles as $name => $role) { + $this->assertFalse(isset($event[$name])); + $event->addRole($name, $role); + $this->assertTrue(isset($event[$name])); + } + } + + /** + * @param array $roles + * An array of test default roles. + * + * @covers ::getIterator + * + * @dataProvider defaultRoleProvider + */ + public function testIteratorAggregate($roles) { + $event = new DefaultRoleEvent(); + $event->setRoles($roles); + + foreach ($event as $name => $role) { + $this->assertRoleEquals($roles[$name], $role); + unset($roles[$name]); + } + + // Verify that all roles were iterated over. + $this->assertEmpty($roles); + } + + /** + * @param array $invalid_roles + * An array of invalid test default roles. + * + * @covers ::addRole + * + * @dataProvider invalidDefaultRoleProvider + */ + public function testAddInvalidRole($invalid_roles) { + $event = new DefaultRoleEvent(); + try { + foreach ($invalid_roles as $name => $invalid_role) { + $event->addRole($name, $invalid_role); + } + $this->fail('An invalid role cannot be added.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. Do an arbitrary assertion so the test is not marked as + // risky. + $this->assertTrue(TRUE); + } + } + + /** + * @param array $invalid_roles + * An array of invalid test default roles. + * + * @covers ::addRoles + * + * @dataProvider invalidDefaultRoleProvider + */ + public function testAddInvalidRoles($invalid_roles) { + $event = new DefaultRoleEvent(); + try { + $event->addRoles($invalid_roles); + $this->fail('An array of invalid roles cannot be added.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. Do an arbitrary assertion so the test is not marked as + // risky. + $this->assertTrue(TRUE); + } + } + + /** + * @param array $invalid_roles + * An array of invalid test default roles. + * + * @covers ::setRole + * + * @dataProvider invalidDefaultRoleProvider + */ + public function testSetInvalidRole($invalid_roles) { + $event = new DefaultRoleEvent(); + try { + foreach ($invalid_roles as $name => $invalid_role) { + $event->setRole($name, $invalid_role); + } + $this->fail('An invalid role cannot be set.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. Do an arbitrary assertion so the test is not marked as + // risky. + $this->assertTrue(TRUE); + } + } + + /** + * @param array $invalid_roles + * An array of invalid test default roles. + * + * @covers ::setRoles + * + * @dataProvider invalidDefaultRoleProvider + */ + public function testSetInvalidRoles($invalid_roles) { + $event = new DefaultRoleEvent(); + try { + $event->setRoles($invalid_roles); + $this->fail('An array of invalid roles cannot be set.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. Do an arbitrary assertion so the test is not marked as + // risky. + $this->assertTrue(TRUE); + } + } + + /** + * @param array $invalid_roles + * An array of invalid test default roles. + * + * @covers ::offsetSet + * + * @dataProvider invalidDefaultRoleProvider + */ + public function testInvalidOffsetSet($invalid_roles) { + $event = new DefaultRoleEvent(); + try { + foreach ($invalid_roles as $name => $invalid_role) { + $event[$name] = $invalid_role; + } + $this->fail('An invalid role cannot be set through ArrayAccess.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. Do an arbitrary assertion so the test is not marked as + // risky. + $this->assertTrue(TRUE); + } + } + + /** + * Provides test data to test default roles. + * + * @return array + * An array of test data arrays, each test data array containing an array of + * test default roles, keyed by default role name. + */ + public function defaultRoleProvider() { + return [ + // Test adding a single administrator role with only a label. + [ + [ + OgRoleInterface::ADMINISTRATOR => ['label' => $this->t('Administrator')], + ], + ], + // Test adding a single administrator role with a label and role type. + [ + [ + OgRoleInterface::ADMINISTRATOR => [ + 'label' => $this->t('Administrator'), + 'role_type' => OgRoleInterface::ROLE_TYPE_REQUIRED, + ], + ], + ], + // Test adding multiple roles. + [ + [ + OgRoleInterface::ADMINISTRATOR => [ + 'label' => $this->t('Administrator'), + 'role_type' => OgRoleInterface::ROLE_TYPE_REQUIRED, + ], + 'moderator' => [ + 'label' => $this->t('Moderator'), + 'role_type' => OgRoleInterface::ROLE_TYPE_STANDARD, + ], + 'contributor' => [ + 'label' => $this->t('Contributor'), + ], + ], + ], + ]; + } + + /** + * Provides invalid test data to test default roles. + * + * @return array + * An array of test data arrays, each test data array containing an array of + * invalid test default roles, keyed by default role name. + */ + public function invalidDefaultRoleProvider() { + return [ + // A role with a missing name. + [ + [ + '' => ['label' => $this->t('Administrator')], + ], + ], + // A role without a label. + [ + [ + OgRoleInterface::ADMINISTRATOR => [ + 'role_type' => OgRoleInterface::ROLE_TYPE_REQUIRED, + ], + ], + ], + // A role with an invalid role type. + [ + [ + OgRoleInterface::ADMINISTRATOR => [ + 'label' => $this->t('Administrator'), + 'role_type' => 'Some non-existing role type', + ], + ], + ], + // An array of multiple correct roles, with one invalid role type sneaked + // in. + [ + [ + OgRoleInterface::ADMINISTRATOR => [ + 'label' => $this->t('Administrator'), + 'role_type' => OgRoleInterface::ROLE_TYPE_REQUIRED, + ], + 'moderator' => [ + 'label' => $this->t('Moderator'), + 'role_type' => OgRoleInterface::ROLE_TYPE_STANDARD, + ], + 'contributor' => [ + 'label' => $this->t('Contributor'), + 'role_type' => 'Some non-existing role type', + ], + ], + ], + ]; + } + + /** + * Mock translation method. + * + * @param string $string + * The string to translate. + * + * @return string + * The translated string. + */ + protected function t($string) { + // Actually translating the strings is not important for this test. + return $string; + } + + /** + * Asserts that the given role properties matches the expected result. + * + * @param array $expected + * An array of expected role properties. + * @param array $actual + * An array of actual role properties. + */ + protected function assertRoleEquals(array $expected, array $actual) { + // Provide default value for the role type. + if (empty($expected['role_type'])) { + $expected['role_type'] = OgRoleInterface::ROLE_TYPE_STANDARD; + } + $this->assertEquals($expected, $actual); + } + +} diff --git a/tests/src/Unit/GroupManagerTest.php b/tests/src/Unit/GroupManagerTest.php index fc412b319..30a6ae2d5 100644 --- a/tests/src/Unit/GroupManagerTest.php +++ b/tests/src/Unit/GroupManagerTest.php @@ -7,8 +7,21 @@ namespace Drupal\Tests\og\Unit; -use Drupal\og\GroupManager; +use Drupal\Core\Config\Config; +use Drupal\Core\Config\ConfigFactoryInterface; +use Drupal\Core\Entity\EntityStorageInterface; +use Drupal\Core\Entity\EntityTypeBundleInfoInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\State\StateInterface; +use Drupal\og\Event\DefaultRoleEvent; +use Drupal\og\Event\DefaultRoleEventInterface; use Drupal\Tests\UnitTestCase; +use Drupal\og\Entity\OgRole; +use Drupal\og\Event\PermissionEventInterface; +use Drupal\og\GroupManager; +use Drupal\og\OgRoleInterface; +use Prophecy\Argument; +use Symfony\Component\EventDispatcher\EventDispatcherInterface; /** * @group og @@ -26,34 +39,79 @@ class GroupManagerTest extends UnitTestCase { */ protected $configFactoryProphecy; + /** + * @var \Drupal\Core\Entity\EntityTypeManagerInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $entityTypeManagerProphecy; + + /** + * @var \Drupal\Core\Entity\EntityStorageInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $entityStorageProphecy; + + /** + * @var \Drupal\og\Entity\OgRole|\Prophecy\Prophecy\ObjectProphecy + */ + protected $ogRoleProphecy; + + /** + * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $entityTypeBundleInfoProphecy; + + /** + * @var \Symfony\Component\EventDispatcher\EventDispatcherInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $eventDispatcherProphecy; + + /** + * @var \Drupal\og\Event\PermissionEventInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $permissionEventProphecy; + + /** + * @var \Drupal\Core\State\StateInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $stateProphecy; + + /** + * @var \Drupal\og\Event\DefaultRoleEventInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $defaultRoleEventProphecy; + /** * {@inheritdoc} */ public function setUp() { - $this->configProphecy = $this->prophesize('Drupal\Core\Config\Config'); - $this->configFactoryProphecy = $this->prophesize('Drupal\Core\Config\ConfigFactoryInterface'); + $this->configProphecy = $this->prophesize(Config::class); + $this->configFactoryProphecy = $this->prophesize(ConfigFactoryInterface::class); + $this->entityTypeManagerProphecy = $this->prophesize(EntityTypeManagerInterface::class); + $this->entityStorageProphecy = $this->prophesize(EntityStorageInterface::class); + $this->ogRoleProphecy = $this->prophesize(OgRole::class); + $this->entityTypeBundleInfoProphecy = $this->prophesize(EntityTypeBundleInfoInterface::class); + $this->eventDispatcherProphecy = $this->prophesize(EventDispatcherInterface::class); + $this->permissionEventProphecy = $this->prophesize(PermissionEventInterface::class); + $this->stateProphecy = $this->prophesize(StateInterface::class); + $this->defaultRoleEventProphecy = $this->prophesize(DefaultRoleEvent::class); } /** * @covers ::__construct */ public function testInstance() { - $this->configProphecy->get('groups') - ->shouldBeCalled(); - - // Just creating an instance should not get the 'groups' config key. - $this->createGroupManager(); + // Just creating an instance should be lightweight, no methods should be + // called. + $group_manager = $this->createGroupManager(); + $this->assertInstanceOf(GroupManager::class, $group_manager); } /** * @covers ::getAllGroupBundles */ public function testGetAllGroupBundles() { + // It is expected that the group map will be retrieved from config. $groups = ['test_entity' => ['a', 'b']]; - - $this->configProphecy->get('groups') - ->willReturn($groups) - ->shouldBeCalled(); + $this->expectGroupMapRetrieval($groups); $manager = $this->createGroupManager(); @@ -66,11 +124,9 @@ public function testGetAllGroupBundles() { * @dataProvider providerTestIsGroup */ public function testIsGroup($entity_type_id, $bundle_id, $expected) { + // It is expected that the group map will be retrieved from config. $groups = ['test_entity' => ['a', 'b']]; - - $this->configProphecy->get('groups') - ->willReturn($groups) - ->shouldBeCalled(); + $this->expectGroupMapRetrieval($groups); $manager = $this->createGroupManager(); @@ -96,11 +152,9 @@ public function providerTestIsGroup() { * @covers ::getGroupsForEntityType */ public function testGetGroupsForEntityType() { + // It is expected that the group map will be retrieved from config. $groups = ['test_entity' => ['a', 'b']]; - - $this->configProphecy->get('groups') - ->willReturn($groups) - ->shouldBeCalled(); + $this->expectGroupMapRetrieval($groups); $manager = $this->createGroupManager(); @@ -112,24 +166,12 @@ public function testGetGroupsForEntityType() { * @covers ::addGroup */ public function testAddGroupExisting() { - $this->configFactoryProphecy->getEditable('og.settings') - ->willReturn($this->configProphecy->reveal()) - ->shouldBeCalled(); - + // It is expected that the group map will be retrieved from config. $groups_before = ['test_entity' => ['a', 'b']]; - - $this->configProphecy->get('groups') - ->willReturn($groups_before) - ->shouldBeCalled(); + $this->expectGroupMapRetrieval($groups_before); $groups_after = ['test_entity' => ['a', 'b', 'c']]; - $this->configProphecy->set('groups', $groups_after) - ->shouldBeCalled(); - - $this->configProphecy->save() - ->shouldBeCalled(); - $this->configProphecy->get('groups') ->willReturn($groups_after) ->shouldBeCalled(); @@ -137,9 +179,13 @@ public function testAddGroupExisting() { $manager = $this->createGroupManager(); // Add to existing. - $manager->addGroup('test_entity', 'c'); - $this->assertSame(['a', 'b', 'c'], $manager->getGroupsForEntityType('test_entity')); - $this->assertTrue($manager->isGroup('test_entity', 'c')); + try { + $manager->addGroup('test_entity', 'c'); + $this->fail('An entity type that was already declared as a group, was redeclared as a group'); + } catch (\InvalidArgumentException $e) { + // Expected result. An exception should be thrown when an entity type is + // redeclared as a group. + } } /** @@ -150,26 +196,28 @@ public function testAddGroupNew() { ->willReturn($this->configProphecy->reveal()) ->shouldBeCalled(); + // It is expected that the group map will be retrieved from config. $groups_before = []; - - $this->configProphecy->get('groups') - ->willReturn($groups_before) - ->shouldBeCalled(); + $this->expectGroupMapRetrieval($groups_before); $groups_after = ['test_entity_new' => ['a']]; + $config_prophecy = $this->configProphecy; $this->configProphecy->set('groups', $groups_after) + ->will(function () use ($groups_after, $config_prophecy) { + $config_prophecy->get('groups') + ->willReturn($groups_after) + ->shouldBeCalled(); + }) ->shouldBeCalled(); $this->configProphecy->save() ->shouldBeCalled(); - $this->configProphecy->get('groups') - ->willReturn($groups_after) - ->shouldBeCalled(); - $manager = $this->createGroupManager(); + $this->expectDefaultRoleCreation('test_entity_new', 'a'); + // Add a new entity type. $manager->addGroup('test_entity_new', 'a'); $this->assertSame(['a'], $manager->getGroupsForEntityType('test_entity_new')); @@ -184,11 +232,9 @@ public function testRemoveGroup() { ->willReturn($this->configProphecy->reveal()) ->shouldBeCalled(); + // It is expected that the group map will be retrieved from config. $groups_before = ['test_entity' => ['a', 'b']]; - - $this->configProphecy->get('groups') - ->willReturn($groups_before) - ->shouldBeCalled(); + $this->expectGroupMapRetrieval($groups_before); $groups_after = ['test_entity' => ['a']]; @@ -202,6 +248,8 @@ public function testRemoveGroup() { ->willReturn($groups_after) ->shouldBeCalled(); + $this->expectRoleRemoval('test_entity', 'b'); + $manager = $this->createGroupManager(); // Add to existing. @@ -217,11 +265,129 @@ public function testRemoveGroup() { * @return \Drupal\og\GroupManager */ protected function createGroupManager() { + // It is expected that the role storage will be initialized. + $this->entityTypeManagerProphecy->getStorage('og_role') + ->willReturn($this->entityStorageProphecy->reveal()) + ->shouldBeCalled(); + + return new GroupManager( + $this->configFactoryProphecy->reveal(), + $this->entityTypeManagerProphecy->reveal(), + $this->entityTypeBundleInfoProphecy->reveal(), + $this->eventDispatcherProphecy->reveal(), + $this->stateProphecy->reveal() + ); + } + + /** + * Sets up an expectation that the group map will be retrieved from config. + * + * @param array $groups + * The expected group map that will be returned by the mocked config. + */ + protected function expectGroupMapRetrieval($groups = []) { $this->configFactoryProphecy->get('og.settings') ->willReturn($this->configProphecy->reveal()) ->shouldBeCalled(); - return new GroupManager($this->configFactoryProphecy->reveal()); + $this->configProphecy->get('groups') + ->willReturn($groups) + ->shouldBeCalled(); + } + + /** + * Mocked method calls when system under test should create default roles. + * + * @param string $entity_type + * The entity type for which default roles should be created. + * @param string $bundle + * The bundle for which default roles should be created. + */ + protected function expectDefaultRoleCreation($entity_type, $bundle) { + // In order to populate the default roles for a new group type, it is + // expected that the list of default roles to populate will be retrieved + // from the event listener. + $this->eventDispatcherProphecy->dispatch(DefaultRoleEventInterface::EVENT_NAME, Argument::type(DefaultRoleEvent::class)) + ->willReturn($this->defaultRoleEventProphecy->reveal()) + ->shouldBeCalled(); + $this->defaultRoleEventProphecy->getRoles() + ->willReturn([]) + ->shouldBeCalled(); + + foreach ([OgRoleInterface::ANONYMOUS, OgRoleInterface::AUTHENTICATED] as $role_name) { + $this->addNewDefaultRole($entity_type, $bundle, $role_name); + } + } + + /** + * Expected method calls when creating a new default role. + * + * @param string $entity_type + * The entity type for which the default role should be created. + * @param string $bundle + * The bundle for which the default role should be created. + * @param string $role_name + * The name of the role being created. + */ + protected function addNewDefaultRole($entity_type, $bundle, $role_name) { + // It is expected that the OG permissions that need to be populated on the + // new role will be requested from the PermissionEvent listener. + $this->eventDispatcherProphecy->dispatch(PermissionEventInterface::EVENT_NAME, Argument::type('\Drupal\og\Event\PermissionEvent')) + ->willReturn($this->permissionEventProphecy->reveal()) + ->shouldBeCalled(); + $this->permissionEventProphecy->filterByDefaultRole($role_name) + ->willReturn([]) + ->shouldBeCalled(); + + // It is expected that the role will be created with default properties. + $properties = [ + 'group_type' => $entity_type, + 'group_bundle' => $bundle, + 'role_type' => OgRole::getRoleTypeByName($role_name), + 'id' => $role_name, + 'permissions' => [], + ]; + $this->entityStorageProphecy->create($properties + OgRole::getDefaultRoles()[$role_name]) + ->willReturn($this->ogRoleProphecy->reveal()) + ->shouldBeCalled(); + + // The role is expected to be saved. + $this->ogRoleProphecy->save() + ->willReturn(1) + ->shouldBeCalled(); + } + + /** + * Expected method calls when deleting roles after a group is deleted. + * + * @param string $entity_type_id + * The entity type for which the roles should be deleted. + * @param string $bundle_id + * The bundle for which the roles should be deleted. + */ + protected function expectRoleRemoval($entity_type_id, $bundle_id) { + // It is expected that a call is done to retrieve all roles associated with + // the group. This will return the 3 default role entities. + $this->entityTypeManagerProphecy->getStorage('og_role') + ->willReturn($this->entityStorageProphecy->reveal()) + ->shouldBeCalled(); + + $properties = [ + 'group_type' => $entity_type_id, + 'group_bundle' => $bundle_id, + ]; + $this->entityStorageProphecy->loadByProperties($properties) + ->willReturn([ + $this->ogRoleProphecy->reveal(), + $this->ogRoleProphecy->reveal(), + $this->ogRoleProphecy->reveal(), + ]) + ->shouldBeCalled(); + + // It is expected that all roles will be deleted, so three delete() calls + // will be made. + $this->ogRoleProphecy->delete() + ->shouldBeCalledTimes(3); } } diff --git a/tests/src/Unit/OgAccessEntityTest.php b/tests/src/Unit/OgAccessEntityTest.php index 194730e48..9e85d3db4 100644 --- a/tests/src/Unit/OgAccessEntityTest.php +++ b/tests/src/Unit/OgAccessEntityTest.php @@ -20,11 +20,15 @@ class OgAccessEntityTest extends OgAccessEntityTestBase { * @coversDefaultmethod ::userAccessEntity * @dataProvider operationProvider */ - public function testDefaultForbidden($operation) { + public function testAccessByOperation($operation) { $group_entity = $this->groupEntity(); $group_entity->isNew()->willReturn(FALSE); - $user_access = OgAccess::userAccessEntity($operation, $this->entity->reveal(), $this->user->reveal()); - $this->assertTrue($user_access->isForbidden()); + $user_access = $this->ogAccess->userAccessEntity($operation, $this->entity->reveal(), $this->user->reveal()); + + // We populate the allowed permissions cache in + // OgAccessEntityTestBase::setup(). + $condition = $operation == 'update group' ? $user_access->isAllowed() : $user_access->isForbidden(); + $this->assertTrue($condition); } /** @@ -34,7 +38,7 @@ public function testDefaultForbidden($operation) { public function testEntityNew($operation) { $group_entity = $this->groupEntity(); $group_entity->isNew()->willReturn(TRUE); - $user_access = OgAccess::userAccessEntity($operation, $group_entity->reveal(), $this->user->reveal()); + $user_access = $this->ogAccess->userAccessEntity($operation, $group_entity->reveal(), $this->user->reveal()); $this->assertTrue($user_access->isNeutral()); } @@ -44,7 +48,7 @@ public function testEntityNew($operation) { */ public function testGetEntityGroups($operation) { $this->user->hasPermission(OgAccess::ADMINISTER_GROUP_PERMISSION)->willReturn(TRUE); - $user_entity_access = OgAccess::userAccessEntity($operation, $this->entity->reveal(), $this->user->reveal()); + $user_entity_access = $this->ogAccess->userAccessEntity($operation, $this->entity->reveal(), $this->user->reveal()); $this->assertTrue($user_entity_access->isAllowed()); } diff --git a/tests/src/Unit/OgAccessEntityTestBase.php b/tests/src/Unit/OgAccessEntityTestBase.php index 0fefac478..479598e33 100644 --- a/tests/src/Unit/OgAccessEntityTestBase.php +++ b/tests/src/Unit/OgAccessEntityTestBase.php @@ -8,10 +8,16 @@ namespace Drupal\Tests\og\Unit; use Drupal\Core\Entity\ContentEntityInterface; -use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Entity\EntityFieldManagerInterface; +use Drupal\Core\Entity\EntityStorageInterface; use Drupal\Core\Entity\EntityTypeInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Entity\FieldableEntityInterface; use Drupal\Core\Field\FieldDefinitionInterface; use Drupal\Core\Field\FieldStorageDefinitionInterface; +use Drupal\og\OgAccessInterface; +use Drupal\og\OgGroupAudienceHelper; +use Prophecy\Argument; class OgAccessEntityTestBase extends OgAccessTestBase { @@ -21,7 +27,7 @@ public function setup() { parent::setUp(); $field_definition = $this->prophesize(FieldDefinitionInterface::class); - $field_definition->getType()->willReturn('og_membership_reference'); + $field_definition->getType()->willReturn(OgGroupAudienceHelper::NON_USER_TO_GROUP_REFERENCE_FIELD_TYPE); $field_definition->getFieldStorageDefinition() ->willReturn($this->prophesize(FieldStorageDefinitionInterface::class)->reveal()); $field_definition->getSetting("handler_settings")->willReturn([]); @@ -29,10 +35,13 @@ public function setup() { $entity_type_id = $this->randomMachineName(); $bundle = $this->randomMachineName(); - $entity_id = mt_rand(20, 30); + + // Just a random entity ID. + $entity_id = 20; $entity_type = $this->prophesize(EntityTypeInterface::class); $entity_type->getListCacheTags()->willReturn([]); + $entity_type->isSubclassOf(FieldableEntityInterface::class)->willReturn(TRUE); $entity_type->id()->willReturn($entity_type_id); $this->entity = $this->prophesize(ContentEntityInterface::class); @@ -44,15 +53,24 @@ public function setup() { $this->groupManager->isGroup($entity_type_id, $bundle)->willReturn(FALSE); - $entity_manager = $this->prophesize(EntityManagerInterface::class); - $entity_manager->getFieldDefinitions($entity_type_id, $bundle)->willReturn([$field_definition->reveal()]); - \Drupal::getContainer()->set('entity.manager', $entity_manager->reveal()); + $entity_field_manager = $this->prophesize(EntityFieldManagerInterface::class); + $entity_field_manager->getFieldDefinitions($entity_type_id, $bundle)->willReturn([$field_definition->reveal()]); + + $group_type_id = $this->group->getEntityTypeId(); + + $storage = $this->prophesize(EntityStorageInterface::class); - // Mock the results of Og::getEntityGroups(). - $r = new \ReflectionClass('Drupal\og\Og'); - $reflection_property = $r->getProperty('entityGroupCache'); - $reflection_property->setAccessible(TRUE); - $reflection_property->setValue(["$entity_type_id:$entity_id:1:" => [[$this->groupEntity()->reveal()]]]); + $entity_type_manager = $this->prophesize(EntityTypeManagerInterface::class); + $entity_type_manager->getDefinition($entity_type_id)->willReturn($entity_type->reveal()); + $entity_type_manager->getStorage($group_type_id)->willReturn($storage->reveal()); + + $container = \Drupal::getContainer(); + $container->set('entity_type.manager', $entity_type_manager->reveal()); + $container->set('entity_field.manager', $entity_field_manager->reveal()); + + // Mock the results of Og::getGroups(). + $storage->loadMultiple(Argument::type('array'))->willReturn([$this->group]); } + } diff --git a/tests/src/Unit/OgAccessTest.php b/tests/src/Unit/OgAccessTest.php index 615777f51..bfa569035 100644 --- a/tests/src/Unit/OgAccessTest.php +++ b/tests/src/Unit/OgAccessTest.php @@ -21,7 +21,7 @@ class OgAccessTest extends OgAccessTestBase { */ public function testUserAccessNotAGroup($operation) { $this->groupManager->isGroup($this->entityTypeId, $this->bundle)->willReturn(FALSE); - $user_access = OgAccess::userAccess($this->groupEntity()->reveal(), $operation); + $user_access = $this->ogAccess->userAccess($this->group, $operation); $this->assertTrue($user_access->isNeutral()); } @@ -29,9 +29,14 @@ public function testUserAccessNotAGroup($operation) { * @coversDefaultmethod ::userAccess * @dataProvider operationProvider */ - public function testUserAccessForbiddenByDefault($operation) { - $user_access = OgAccess::userAccess($this->groupEntity()->reveal(), $operation, $this->user->reveal()); - $this->assertTrue($user_access->isForbidden()); + public function testAccessByOperation($operation) { + $user_access = $this->ogAccess->userAccess($this->group, $operation, $this->user->reveal()); + + // We populate the allowed permissions cache in + // OgAccessTestBase::setup(). + $condition = $operation == 'update group' ? $user_access->isAllowed() : $user_access->isForbidden(); + + $this->assertTrue($condition); } /** @@ -40,7 +45,7 @@ public function testUserAccessForbiddenByDefault($operation) { */ public function testUserAccessUser1($operation) { $this->user->id()->willReturn(1); - $user_access = OgAccess::userAccess($this->groupEntity()->reveal(), $operation, $this->user->reveal()); + $user_access = $this->ogAccess->userAccess($this->group, $operation, $this->user->reveal()); $this->assertTrue($user_access->isAllowed()); } @@ -50,7 +55,7 @@ public function testUserAccessUser1($operation) { */ public function testUserAccessAdminPermission($operation) { $this->user->hasPermission(OgAccess::ADMINISTER_GROUP_PERMISSION)->willReturn(TRUE); - $user_access = OgAccess::userAccess($this->groupEntity()->reveal(), $operation, $this->user->reveal()); + $user_access = $this->ogAccess->userAccess($this->group, $operation, $this->user->reveal()); $this->assertTrue($user_access->isAllowed()); } @@ -60,30 +65,8 @@ public function testUserAccessAdminPermission($operation) { */ public function testUserAccessOwner($operation) { $this->config->get('group_manager_full_access')->willReturn(TRUE); - $user_access = OgAccess::userAccess($this->groupEntity(TRUE)->reveal(), $operation, $this->user->reveal()); + $user_access = $this->ogAccess->userAccess($this->groupEntity(TRUE)->reveal(), $operation, $this->user->reveal()); $this->assertTrue($user_access->isAllowed()); } - /** - * @coversDefaultmethod ::userAccess - * @dataProvider operationProvider - */ - public function testUserAccessOgUserAccessAlter($operation) { - $permissions[OgAccess::ADMINISTER_GROUP_PERMISSION] = TRUE; - \Drupal::getContainer()->set('module_handler', new OgAccessTestAlter($permissions)); - $group_entity = $this->groupEntity(); - $group_entity->id()->willReturn(mt_rand(5, 10)); - $user_access = OgAccess::userAccess($group_entity->reveal(), $operation, $this->user->reveal()); - $this->assertTrue($user_access->isAllowed()); - } - -} - -class OgAccessTestAlter { - public function __construct($data) { - $this->data = $data; - } - public function alter($op, &$data) { - $data = $this->data; - } } diff --git a/tests/src/Unit/OgAccessTestBase.php b/tests/src/Unit/OgAccessTestBase.php index 6291b43e5..e8ba2ecde 100644 --- a/tests/src/Unit/OgAccessTestBase.php +++ b/tests/src/Unit/OgAccessTestBase.php @@ -7,17 +7,19 @@ namespace Drupal\Tests\og\Unit; -use Drupal\user\EntityOwnerInterface; -use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Cache\Context\CacheContextsManager; use Drupal\Core\Config\Config; -use Drupal\Core\Config\ConfigFactory; +use Drupal\Core\Config\ConfigFactoryInterface; use Drupal\Core\DependencyInjection\ContainerBuilder; -use Drupal\og\OgAccess; -use Drupal\Tests\UnitTestCase; +use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Session\AccountInterface; +use Drupal\Core\Session\AccountProxyInterface; +use Drupal\Tests\UnitTestCase; use Drupal\og\GroupManager; +use Drupal\og\OgAccess; +use Drupal\og\OgMembershipInterface; +use Drupal\user\EntityOwnerInterface; use Prophecy\Argument; class OgAccessTestBase extends UnitTestCase { @@ -39,11 +41,23 @@ class OgAccessTestBase extends UnitTestCase { */ protected $bundle; + protected $group; + /** * @var \Drupal\og\GroupManager */ protected $groupManager; + /** + * The OgAccess class, this is the system under test. + * + * @var \Drupal\og\OgAccessInterface + */ + protected $ogAccess; + + /** + * {@inheritdoc} + */ public function setUp() { $this->entityTypeId = $this->randomMachineName(); $this->bundle = $this->randomMachineName(); @@ -54,12 +68,25 @@ public function setUp() { $cache_contexts_manager = $this->prophesize(CacheContextsManager::class); $cache_contexts_manager->assertValidTokens(Argument::any())->willReturn(TRUE); + // It is expected that any access check will retrieve the settings, because + // it contains an option to give full access to to the group manager. $this->config = $this->addCache($this->prophesize(Config::class)); $this->config->get('group_manager_full_access')->willReturn(FALSE); - $config_factory = $this->prophesize(ConfigFactory::class); + // Whether or not the user has access to a certain operation depends in part + // on the 'group_manager_full_access' setting which is stored in config. + // Since the access is cached, this means that from the point of view from + // the caching system this access varies by the 'og.settings' config object + // that contains this setting. It is hence expected that the cacheability + // metadata is retrieved from the config object so it can be attached to the + // access result object. + $config_factory = $this->prophesize(ConfigFactoryInterface::class); $config_factory->get('og.settings')->willReturn($this->config); + $this->config->getCacheContexts()->willReturn([]); + $this->config->getCacheTags()->willReturn([]); + $this->config->getCacheMaxAge()->willReturn(0); + $this->user = $this->prophesize(AccountInterface::class); $this->user->isAuthenticated()->willReturn(TRUE); $this->user->id()->willReturn(2); @@ -73,6 +100,65 @@ public function setUp() { // This is for caching purposes only. $container->set('current_user', $this->user->reveal()); \Drupal::setContainer($container); + + $this->group = $this->groupEntity()->reveal(); + $group_type_id = $this->group->getEntityTypeId(); + + $entity_id = 20; + + $account_proxy = $this->prophesize(AccountProxyInterface::class); + $module_handler = $this->prophesize(ModuleHandlerInterface::class); + + // Instantiate the system under test. + $this->ogAccess = new OgAccess($config_factory->reveal(), $account_proxy->reveal(), $module_handler->reveal()); + + // Set the Og::cache property values, to skip calculations. + $values = []; + + $r = new \ReflectionClass('Drupal\og\Og'); + $reflection_property = $r->getProperty('cache'); + $reflection_property->setAccessible(TRUE); + + // Mock the results of Og::getGroupIds(). + $identifier = [ + 'Drupal\og\Og::getGroupIds', + $entity_id, + NULL, + NULL, + ]; + + $identifier = implode(':', $identifier); + + $group_ids = [$group_type_id => [$this->group->id()]]; + $values[$identifier] = $group_ids; + + // Mock the results of Og::getUserMemberships(). + $identifier = [ + 'Drupal\og\Og::getUserMemberships', + 2, + OgMembershipInterface::STATE_ACTIVE, + // The field name. + NULL, + ]; + $identifier = implode(':', $identifier); + + // The cache is supposed to be holding the OG memberships, however it is not + // used in the tests, so we just set an empty array. + $values[$identifier] = []; + + $reflection_property->setValue($values); + + // Set the allowed permissions cache. + $r = new \ReflectionClass('Drupal\og\OgAccess'); + $reflection_property = $r->getProperty('permissionsCache'); + $reflection_property->setAccessible(TRUE); + + $values = []; + foreach (['pre_alter', 'post_alter'] as $key) { + $values[$group_type_id][$this->group->id()][2][$key] = ['permissions' => ['update group'], 'is_admin' => FALSE]; + } + + $reflection_property->setValue($this->ogAccess, $values); } /** @@ -104,8 +190,10 @@ protected function addCache($prophecy) { public function operationProvider() { return [ - ['view'], - ['update'], + // In the unit tests we don't really care about the permission name - it + // can be an arbitrary string; except for OgAccessTest::testUserAccessAdminPermission + // test which checks for "administer group" permission. + ['update group'], ['administer group'], ]; } diff --git a/tests/src/Unit/PermissionEventTest.php b/tests/src/Unit/PermissionEventTest.php new file mode 100644 index 000000000..0c6f2092c --- /dev/null +++ b/tests/src/Unit/PermissionEventTest.php @@ -0,0 +1,464 @@ + $permission) { + try { + $event->getPermission($name); + $this->fail('Calling ::getPermission() on a non-existing permission throws an exception.'); + } catch (\InvalidArgumentException $e) { + // Expected result. + } + } + + // Test that it can retrieve the permissions correctly after they are set. + $event->setPermissions($permissions); + + foreach ($permissions as $name => $permission) { + $this->assertEquals($permission, $event->getPermission($name)); + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::getPermissions + * @covers ::setPermissions + * + * @dataProvider permissionsProvider + */ + public function testGetPermissions($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $event->setPermissions($permissions); + + $this->assertEquals($permissions, $event->getPermissions()); + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::setPermission + * + * @dataProvider permissionsProvider + */ + public function testSetPermission($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + + // Test that an exception is thrown when setting a nameless permission. + try { + $event->setPermission('', ['title' => 'A permission without a name']); + $this->fail('An exception is thrown when a nameless permission is set.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. + } + + // Test that an exception is thrown when setting permission without a title. + try { + $event->setPermission('an-invalid-permission', []); + $this->fail('An exception is thrown when a permission without a title is set.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. + } + + foreach ($permissions as $name => $permission) { + $event->setPermission($name, $permission); + } + + $this->assertEquals($permissions, $event->getPermissions()); + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::deletePermission + * + * @dataProvider permissionsProvider + */ + public function testDeletePermission($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $event->setPermissions($permissions); + + foreach ($permissions as $name => $permission) { + // Before we delete the permission, it should still be there. + $this->assertTrue($event->hasPermission($name)); + + // After we delete the permission, it should be gone. + $event->deletePermission($name); + $this->assertFalse($event->hasPermission($name)); + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::hasPermission + * + * @dataProvider permissionsProvider + */ + public function testHasPermission($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + + foreach ($permissions as $name => $permission) { + $this->assertFalse($event->hasPermission($name)); + $event->setPermission($name, $permission); + $this->assertTrue($event->hasPermission($name)); + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::getEntityTypeId + * + * @dataProvider permissionsProvider + */ + public function testGetEntityTypeId($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $this->assertEquals($entity_type_id, $event->getEntityTypeId()); + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::getBundleId + * + * @dataProvider permissionsProvider + */ + public function testGetBundleId($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $this->assertEquals($bundle_id, $event->getBundleId()); + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::filterByDefaultRole + * + * @dataProvider permissionsProvider + */ + public function testFilterByDefaultRole($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $event->setPermissions($permissions); + + $default_roles = [ + OgRoleInterface::ANONYMOUS, + OgRoleInterface::AUTHENTICATED, + OgRoleInterface::ADMINISTRATOR, + ]; + foreach ($default_roles as $default_role) { + $expected = array_filter($permissions, function ($permission) use ($default_role) { + return !empty($permission['default roles']) && in_array($default_role, $permission['default roles']); + }); + $this->assertEquals($expected, $event->filterByDefaultRole($default_role)); + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::offsetGet + * + * @dataProvider permissionsProvider + */ + public function testOffsetGet($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $event->setPermissions($permissions); + + foreach ($permissions as $name => $permission) { + $this->assertEquals($permission, $event[$name]); + } + + // Test that an exception is thrown when requesting a non-existing + // permission. + try { + $event['some-non-existing-permission']; + $this->fail('An exception is thrown when a non-existing permission is requested through ArrayAccess.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::offsetSet + * + * @dataProvider permissionsProvider + */ + public function testOffsetSet($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + + // Test that an exception is thrown when setting a nameless permission. + try { + $event[] = ['title' => 'A permission without a name']; + $this->fail('An exception is thrown when a nameless permission is set through ArrayAccess.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. + } + + // Test that an exception is thrown when setting permission without a title. + try { + $event['an-invalid-permission'] = []; + $this->fail('An exception is thrown when a permission without a title is set through ArrayAccess.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. + } + + foreach ($permissions as $name => $permission) { + $this->assertFalse($event->hasPermission($name)); + $event[$name] = $permission; + $this->assertEquals($permission, $event->getPermission($name)); + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::offsetUnset + * + * @dataProvider permissionsProvider + */ + public function testOffsetUnset($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $event->setPermissions($permissions); + + foreach ($permissions as $name => $permission) { + $this->assertTrue($event->hasPermission($name)); + unset($event[$name]); + $this->assertFalse($event->hasPermission($name)); + } + + // @todo + // Test that an exception is thrown when unsetting a non-existing + // permission. + try { + $event['some-non-existing-permission']; + $this->fail('An exception is thrown when a non-existing permission is requested through ArrayAccess.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::offsetExists + * + * @dataProvider permissionsProvider + */ + public function testOffsetExists($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + + foreach ($permissions as $name => $permission) { + $this->assertFalse(isset($event[$name])); + $event->setPermission($name, $permission); + $this->assertTrue(isset($event[$name])); + } + } + + /** + * @param array $permissions + * An array of test permissions. + * @param string $entity_type_id + * The entity type ID of the group type to which the permissions apply. + * @param string $bundle_id + * The bundle ID of the group type to which the permissions apply. + * + * @covers ::getIterator + * + * @dataProvider permissionsProvider + */ + public function testIteratorAggregate($permissions, $entity_type_id, $bundle_id) { + $event = new PermissionEvent($entity_type_id, $bundle_id); + $event->setPermissions($permissions); + + foreach ($event as $name => $permission) { + $expected_permission = reset($permissions); + $expected_name = key($permissions); + $this->assertEquals($expected_name, $name); + $this->assertEquals($expected_permission, $permission); + array_shift($permissions); + } + + // Check that the iterator has looped over all permissions correctly. + $this->assertEmpty($permissions); + } + + /** + * Data provider to test permissions. + * + * @return array + * An array of test data arrays, each test data array containing: + * - An array of test permissions, keyed by permission ID. + * - The entity type ID of the group type to which these permissions apply. + * - The bundle ID of the group type to which these permissions apply. + */ + public function permissionsProvider() { + $permissions = [ + // A simple permission with only the required option. + [ + [ + 'appreciate nature' => [ + 'title' => $this->t('Allows the member to go outdoors and appreciate the landscape.'), + ], + ], + ], + // A single permission with restricted access and a default role. + [ + [ + 'administer group' => [ + 'title' => $this->t('Administer group'), + 'description' => $this->t('Manage group members and content in the group.'), + 'default roles' => [OgRoleInterface::ADMINISTRATOR], + 'restrict access' => TRUE, + ], + ], + ], + // A permission restricted to a specific role, and having a default role. + [ + [ + 'unsubscribe' => [ + 'title' => $this->t('Unsubscribe from group'), + 'description' => $this->t('Allow members to unsubscribe themselves from a group, removing their membership.'), + 'roles' => [OgRoleInterface::AUTHENTICATED], + 'default roles' => [OgRoleInterface::AUTHENTICATED], + ], + ], + ], + // Simulate a subscriber providing multiple permissions. + [ + [ + '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], + ], + '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], + ], + 'unsubscribe' => [ + 'title' => $this->t('Unsubscribe from group'), + 'description' => $this->t('Allow members to unsubscribe themselves from a group, removing their membership.'), + 'roles' => [OgRoleInterface::AUTHENTICATED], + 'default roles' => [OgRoleInterface::AUTHENTICATED], + ], + ], + ], + ]; + + // Supply a random entity type ID and bundle ID for each data set. + foreach ($permissions as &$item) { + $item[] = $this->randomMachineName(); + $item[] = $this->randomMachineName(); + } + + return $permissions; + } + + /** + * Mock translation method. + * + * @param string $string + * The string to translate. + * + * @return string + * The translated string. + */ + protected function t($string) { + // Actually translating the strings is not important for this test. + return $string; + } + +} +