diff --git a/.travis.yml b/.travis.yml index f121c28ab..c9a113c28 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,30 +2,27 @@ language: php sudo: true php: - - 5.6 - - 7.0 - 7.1 - 7.2 + - 7.3 env: global: - COMPOSER_MEMORY_LIMIT=2G matrix: - - TEST_SUITE=8.5.x - - TEST_SUITE=8.6.x + - TEST_SUITE=8.7.x + - TEST_SUITE=8.8.x - TEST_SUITE=PHP_CodeSniffer # Only run the coding standards check once. matrix: exclude: - - php: 5.6 - env: TEST_SUITE=PHP_CodeSniffer - - php: 7.0 - env: TEST_SUITE=PHP_CodeSniffer - php: 7.1 env: TEST_SUITE=PHP_CodeSniffer + - php: 7.2 + env: TEST_SUITE=PHP_CodeSniffer allow_failures: - - env: TEST_SUITE=8.6.x + - env: TEST_SUITE=8.8.x mysql: database: og @@ -44,9 +41,6 @@ before_script: # Remember the current directory for later use in the Drupal installation. - MODULE_DIR=$(pwd) - # Install Composer dependencies for OG. - - composer install - # Navigate out of module directory to prevent blown stack by recursive module # lookup. - cd .. diff --git a/composer.json b/composer.json index 86b654504..2719c766d 100644 --- a/composer.json +++ b/composer.json @@ -10,9 +10,10 @@ "source": "https://cgit.drupalcode.org/og" }, "require": { - "drupal/core": "~8.5" + "drupal/core": "~8.7" }, "require-dev": { "drupal/coder": "~8.2" - } + }, + "minimum-stability": "RC" } diff --git a/config/install/core.entity_form_display.og_membership.default.default.yml b/config/install/core.entity_form_display.og_membership.default.default.yml index 08b77f3bf..0332b58f8 100644 --- a/config/install/core.entity_form_display.og_membership.default.default.yml +++ b/config/install/core.entity_form_display.og_membership.default.default.yml @@ -18,4 +18,17 @@ content: rows: 2 placeholder: '' third_party_settings: { } + roles: + type: options_buttons + weight: 0 + settings: { } + third_party_settings: { } + uid: + type: og_autocomplete + weight: -1 + settings: + match_operator: CONTAINS + size: 60 + placeholder: '' + third_party_settings: { } hidden: { } diff --git a/config/install/core.entity_view_display.og_membership.default.default.yml b/config/install/core.entity_view_display.og_membership.default.default.yml index 4d19f8b4e..468f4dd18 100644 --- a/config/install/core.entity_view_display.og_membership.default.default.yml +++ b/config/install/core.entity_view_display.og_membership.default.default.yml @@ -14,7 +14,21 @@ content: og_membership_request: label: above type: basic_string - weight: 0 + weight: 2 settings: { } third_party_settings: { } + roles: + type: entity_reference_label + weight: 1 + label: above + settings: + link: false + third_party_settings: { } + uid: + type: entity_reference_label + weight: 0 + label: above + settings: + link: true + third_party_settings: { } hidden: { } diff --git a/config/optional/views.view.og_members_overview.yml b/config/optional/views.view.og_members_overview.yml index ad531e3ab..cf158dae4 100644 --- a/config/optional/views.view.og_members_overview.yml +++ b/config/optional/views.view.og_members_overview.yml @@ -3,6 +3,7 @@ status: true dependencies: module: - og + - options - user _core: default_config_hash: RnChWtrF4u0Q13iQMFypL0Uz6IsB4NY6ZTk_LorSbJg @@ -22,8 +23,9 @@ display: position: 0 display_options: access: - type: none - options: { } + type: perm + options: + perm: 'access user profiles' cache: type: none options: { } @@ -46,12 +48,17 @@ display: sort_asc_label: Asc sort_desc_label: Desc pager: - type: mini + type: full options: - items_per_page: 10 + items_per_page: 50 offset: 0 id: 0 total_pages: null + tags: + previous: ‹‹ + next: ›› + first: '« First' + last: 'Last »' expose: items_per_page: false items_per_page_label: 'Items per page' @@ -60,9 +67,7 @@ display: items_per_page_options_all_label: '- All -' offset: false offset_label: Offset - tags: - previous: ‹‹ - next: ›› + quantity: 9 style: type: table options: @@ -356,10 +361,10 @@ display: entity_type: og_membership entity_field: state plugin_id: field - roles: - id: roles + roles_target_id: + id: roles_target_id table: og_membership__roles - field: roles + field: roles_target_id relationship: none group_type: group admin_label: '' @@ -421,6 +426,57 @@ display: entity_type: og_membership entity_field: roles plugin_id: field + operations: + id: operations + table: og_membership + field: operations + relationship: none + group_type: group + admin_label: '' + label: 'Operations links' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + destination: true + entity_type: og_membership + plugin_id: entity_operations filters: { } sorts: { } header: { } diff --git a/config/schema/og.schema.yml b/config/schema/og.schema.yml index 4f08e369b..aa919cd2b 100644 --- a/config/schema/og.schema.yml +++ b/config/schema/og.schema.yml @@ -193,3 +193,18 @@ condition.plugin.og_group_type: type: sequence sequence: type: string + +# Copied and adapted from core.entity.schema.yml +field.widget.settings.og_autocomplete: + type: mapping + label: 'OG context based entity reference autocomplete with display format settings' + 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 d657d4709..80ad218cf 100644 --- a/og.install +++ b/og.install @@ -28,9 +28,14 @@ function og_update_8001(&$sandbox) { \Drupal::entityDefinitionUpdateManager()->installFieldStorageDefinition('entity_bundle', 'og_membership', 'og', $storage_definition); $sandbox['#finished'] = 0; + $sandbox['batch_size'] = 500; $sandbox['current'] = 0; $sandbox['total'] = $storage->getQuery()->count()->execute(); - $sandbox['batch_size'] = 500; + + if (!$sandbox['total']) { + $sandbox['#finished'] = 1; + return t('No OG memberships found.'); + } } // Update the existing memberships to include the group bundle ID. diff --git a/og.links.action.yml b/og.links.action.yml new file mode 100644 index 000000000..c70c87d8a --- /dev/null +++ b/og.links.action.yml @@ -0,0 +1,8 @@ +og_membership.add: + deriver: \Drupal\og\Plugin\Derivative\OgActionLink + +og_membership.type_add: + route_name: og_membership.type_add + title: 'Add membership type' + appears_on: + - entity.og_membership_type.collection diff --git a/og.links.menu.yml b/og.links.menu.yml new file mode 100644 index 000000000..b91d6607f --- /dev/null +++ b/og.links.menu.yml @@ -0,0 +1,5 @@ +entity.og_membership_type.collection: + title: 'Membership types' + parent: system.admin_structure + description: 'Create and manage fields, forms, and display settings for OG memberships.' + route_name: entity.og_membership_type.collection diff --git a/og.links.task.yml b/og.links.task.yml index 854c7df83..2b013250d 100644 --- a/og.links.task.yml +++ b/og.links.task.yml @@ -1,2 +1,15 @@ og.og_admin_routes: deriver: \Drupal\og\Plugin\Derivative\OgLocalTask +entity.og_membership.canonical: + route_name: entity.og_membership.canonical + base_route: entity.og_membership.canonical + title: View +entity.og_membership.edit_form: + route_name: entity.og_membership.edit_form + base_route: entity.og_membership.canonical + title: Edit +entity.og_membership.delete_form: + route_name: entity.og_membership.delete_form + base_route: entity.og_membership.canonical + title: Delete + weight: 10 diff --git a/og.module b/og.module index d4bf89737..28da5518f 100755 --- a/og.module +++ b/og.module @@ -17,6 +17,7 @@ use Drupal\og\Entity\OgRole; use Drupal\og\Og; use Drupal\og\OgGroupAudienceHelperInterface; use Drupal\og\OgMembershipInterface; +use Drupal\og\OgMembershipTypeInterface; use Drupal\og\OgRoleInterface; use Drupal\system\Entity\Action; use Drupal\user\EntityOwnerInterface; @@ -53,11 +54,7 @@ function og_entity_insert(EntityInterface $entity) { // Other modules that implement hook_entity_insert() might already have // created a membership ahead of us. - if (!Og::getMembership($entity, $entity->getOwner(), [ - OgMembershipInterface::STATE_ACTIVE, - OgMembershipInterface::STATE_PENDING, - OgMembershipInterface::STATE_BLOCKED, - ])) { + if (!Og::getMembership($entity, $entity->getOwner(), OgMembershipInterface::ALL_STATES)) { $membership = Og::createMembership($entity, $entity->getOwner()); $membership->save(); } @@ -177,6 +174,47 @@ function og_entity_access(EntityInterface $entity, $operation, AccountInterface return AccessResult::forbidden(); } +/** + * Implements hook_ENTITY_TYPE_access(). + */ +function og_og_membership_type_access(OgMembershipTypeInterface $entity, $operation, AccountInterface $account) { + // Do not allow deleting the default membership type. + if ($operation === 'delete' && $entity->id() === OgMembershipInterface::TYPE_DEFAULT) { + return AccessResult::forbidden(); + } + + // If the user has permission to administer all groups, allow access. + if ($account->hasPermission('administer group')) { + return AccessResult::allowed(); + } + + return AccessResult::forbidden(); +} + +/** + * Implements hook_ENTITY_TYPE_access(). + */ +function og_og_membership_access(OgMembershipInterface $entity, $operation, AccountInterface $account) { + $group = $entity->getGroup(); + + // If there's a group owner, don't let them leave. + if ( + isset($group_fields['uid']) + && $operation === 'delete' + && $group_fields['uid']->entity->id() === $entity->getOwner()->id() + ) { + return AccessResult::forbidden(); + } + + // Ensure that there's at least one member in the group. + if ($operation === 'delete' && \Drupal::service('og.membership_manager')->getGroupMembershipCount($group) === 1) { + return AccessResult::forbidden(); + } + + return \Drupal::service('og.access') + ->userAccess($entity->getGroup(), 'manage members'); +} + /** * Implements hook_entity_create_access(). */ @@ -378,6 +416,11 @@ function og_og_role_insert(OgRoleInterface $role) { return; } + // Do not create config while config import is in progress. + if (\Drupal::isConfigSyncing()) { + return; + } + $add_id = 'og_membership_add_single_role_action.' . $role->getName(); if (!Action::load($add_id)) { $action = Action::create([ diff --git a/og.routing.yml b/og.routing.yml index 06a78558e..df629a9e6 100644 --- a/og.routing.yml +++ b/og.routing.yml @@ -1,11 +1,11 @@ # Routes for Organic groups. og.subscribe: - path: 'group/{entity_type_id}/{group}/subscribe/{membership_type}' + path: 'group/{entity_type_id}/{group}/subscribe/{og_membership_type}' defaults: _controller: '\Drupal\og\Controller\SubscriptionController::subscribe' _title: 'Join Group' - membership_type: default + og_membership_type: default requirements: # Only authenticated users can subscribe to group, but we do allow anonymous # users to reach this route. They will be redirect to login page or be given @@ -15,8 +15,6 @@ og.subscribe: parameters: group: type: entity:{entity_type_id} - membership_type: - type: entity:og_membership_type og.unsubscribe: path: 'group/{entity_type_id}/{group}/unsubscribe' @@ -43,3 +41,99 @@ og.remove_multiple_roles_confirm: _form: '\Drupal\og\Form\OgRemoveMultipleRolesForm' requirements: _custom_access: '\Drupal\og\Form\OgRemoveMultipleRolesForm::access' + +og.entity_autocomplete: + path: '/group/{entity_type_id}/{group}/autocomplete/{target_type}/{selection_handler}/{selection_settings_key}' + defaults: + _controller: '\Drupal\og\Controller\OgAutocompleteController:handleAutocomplete' + requirements: + _access: 'TRUE' + options: + parameters: + group: + type: entity:{entity_type_id} + +# OG Membership entity routes +entity.og_membership.add_form: + path: 'group/{entity_type_id}/{group}/admin/members/add/{og_membership_type}' + defaults: + _controller: '\Drupal\og\Controller\OgAdminMembersController::addForm' + _title: 'Add member' + requirements: + _og_membership_add_access: 'TRUE' + options: + _admin_route: 'TRUE' + parameters: + group: + type: entity:{entity_type_id} + +# The canonical route is the same as the edit-form route because we need a +# canonical route for various functionality to work properly, but a standard +# entity view for OG memberships tends to feel quite stub-like. +entity.og_membership.canonical: + path: 'group/{entity_type_id}/{group}/admin/members/{og_membership}/edit' + defaults: + _entity_form: 'og_membership.edit' + options: + _admin_route: 'TRUE' + parameters: + group: + type: entity:{entity_type_id} + requirements: + _entity_access: 'og_membership.edit' + +entity.og_membership.edit_form: + path: 'group/{entity_type_id}/{group}/admin/members/{og_membership}/edit' + defaults: + _entity_form: 'og_membership.edit' + options: + _admin_route: 'TRUE' + parameters: + group: + type: entity:{entity_type_id} + requirements: + _entity_access: 'og_membership.edit' + +entity.og_membership.delete_form: + path: 'group/{entity_type_id}/{group}/admin/members/{og_membership}/delete' + defaults: + _entity_form: 'og_membership.delete' + options: + _admin_route: 'TRUE' + parameters: + group: + type: entity:{entity_type_id} + requirements: + _entity_access: 'og_membership.delete' + +# OG Membership type entity routes +entity.og_membership_type.collection: + path: '/admin/structure/membership-types' + defaults: + _entity_list: 'og_membership_type' + _title: 'Membership types' + requirements: + _permission: 'administer group' + +entity.og_membership_type.edit_form: + path: '/admin/structure/membership-types/manage/{og_membership_type}' + defaults: + _entity_form: 'og_membership_type.edit' + requirements: + _permission: 'administer group' + +entity.og_membership_type.delete_form: + path: '/admin/structure/membership-types/manage/{og_membership_type}/delete' + defaults: + _entity_form: 'og_membership_type.delete' + _title: 'Delete' + requirements: + _permission: 'og_membership_type.delete' + +og_membership.type_add: + path: '/admin/structure/membership-types/add' + defaults: + _entity_form: 'og_membership_type.add' + _title: 'Add membership type' + requirements: + _permission: 'administer group' diff --git a/og.services.yml b/og.services.yml index 34a31b286..b1d13e80c 100644 --- a/og.services.yml +++ b/og.services.yml @@ -4,6 +4,11 @@ services: arguments: ['@entity_type.manager', '@og.access'] tags: - { name: access_check, applies_to: _og_user_access_group } + access_check.og.membership.add: + class: Drupal\og\Access\OgMembershipAddAccessCheck + arguments: ['@entity_type.manager', '@og.access'] + tags: + - { name: access_check, applies_to: _og_membership_add_access } cache_context.og_group_context: class: 'Drupal\og\Cache\Context\OgGroupContextCacheContext' arguments: ['@og.context'] @@ -42,10 +47,10 @@ services: arguments: ['@entity_type.manager', '@entity_field.manager'] og.group_type_manager: class: Drupal\og\GroupTypeManager - arguments: ['@config.factory', '@entity_type.manager', '@entity_type.bundle.info', '@event_dispatcher', '@state', '@og.permission_manager', '@og.role_manager', '@router.builder', '@og.group_audience_helper'] + arguments: ['@config.factory', '@entity_type.manager', '@entity_type.bundle.info', '@event_dispatcher', '@cache.data', '@og.permission_manager', '@og.role_manager', '@router.builder', '@og.group_audience_helper'] og.membership_manager: class: Drupal\og\MembershipManager - arguments: ['@entity_type.manager', '@og.group_audience_helper'] + arguments: ['@entity_type.manager', '@og.group_audience_helper', '@database'] og.permission_manager: class: Drupal\og\PermissionManager arguments: ['@event_dispatcher'] diff --git a/og_ui/og_ui.module b/og_ui/og_ui.module index f319c4ed0..f09da9234 100644 --- a/og_ui/og_ui.module +++ b/og_ui/og_ui.module @@ -19,10 +19,16 @@ use Drupal\og_ui\BundleFormAlter; * Implements hook_form_alter(). */ function og_ui_form_alter(array &$form, FormStateInterface $form_state, $form_id) { - if ($form_state->getFormObject() instanceof BundleEntityFormBase) { - (new BundleFormAlter($form_state->getFormObject()->getEntity())) - ->formAlter($form, $form_state); + if (!$form_state->getFormObject() instanceof BundleEntityFormBase) { + return; } + + $entity_type = $form_state->getFormObject()->getEntity(); + if ($entity_type->getEntityTypeId() === 'og_membership_type') { + return; + } + + (new BundleFormAlter($entity_type))->formAlter($form, $form_state); } /** diff --git a/og_ui/src/Form/AdminSettingsForm.php b/og_ui/src/Form/AdminSettingsForm.php index ef8d86cf0..57ba41f70 100644 --- a/og_ui/src/Form/AdminSettingsForm.php +++ b/og_ui/src/Form/AdminSettingsForm.php @@ -145,7 +145,6 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $this->config('og.settings') ->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('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/tests/src/Functional/BundleFormAlterTest.php b/og_ui/tests/src/Functional/BundleFormAlterTest.php index 8f326fcdf..1427f0cbf 100644 --- a/og_ui/tests/src/Functional/BundleFormAlterTest.php +++ b/og_ui/tests/src/Functional/BundleFormAlterTest.php @@ -18,7 +18,16 @@ class BundleFormAlterTest extends BrowserTestBase { /** * {@inheritdoc} */ - public static $modules = ['block_content', 'entity_test', 'node', 'og_ui']; + public static $modules = [ + 'block_content', + 'entity_test', + 'node', + 'og_ui', + 'system', + 'og', + 'options', + 'field', + ]; /** * An administrator user. diff --git a/scripts/travis-ci/run-test.sh b/scripts/travis-ci/run-test.sh index aafaab4a7..07797a6a9 100755 --- a/scripts/travis-ci/run-test.sh +++ b/scripts/travis-ci/run-test.sh @@ -11,7 +11,7 @@ mysql_to_ramdisk() { sudo service mysql start } -TEST_DIRS=($MODULE_DIR/tests $MODULE_DIR/og_ui/tests) +TEST_DIRS=($DRUPAL_DIR/modules/og/tests $DRUPAL_DIR/modules/og/og_ui/tests) case "$1" in PHP_CodeSniffer) diff --git a/src/Access/GroupCheck.php b/src/Access/GroupCheck.php index 002797b84..22f7195b8 100644 --- a/src/Access/GroupCheck.php +++ b/src/Access/GroupCheck.php @@ -83,6 +83,9 @@ public function access(AccountInterface $user, Route $route, RouteMatchInterface if (!$group = $route_match->getParameter($parameter_name)) { return AccessResult::forbidden(); } + if (is_numeric($group)) { + $group = $this->entityTypeManager->getStorage($parameter_name)->load($group); + } $entity_type_id = $group->getEntityTypeId(); } diff --git a/src/Access/OgMembershipAddAccessCheck.php b/src/Access/OgMembershipAddAccessCheck.php new file mode 100644 index 000000000..922543e5b --- /dev/null +++ b/src/Access/OgMembershipAddAccessCheck.php @@ -0,0 +1,77 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * Checks access to create the entity type and bundle for the given route. + * + * @param \Drupal\Core\Routing\RouteMatchInterface $route_match + * The parametrized route. + * @param \Drupal\Core\Session\AccountInterface $account + * The currently logged in account. + * @param \Drupal\Core\Entity\EntityInterface $group + * The group entity. + * @param \Drupal\og\OgMembershipTypeInterface $og_membership_type + * The membership type entity. + * + * @return \Drupal\Core\Access\AccessResultInterface + * The access result. + */ + public function access(RouteMatchInterface $route_match, AccountInterface $account, EntityInterface $group = NULL, OgMembershipTypeInterface $og_membership_type = NULL) { + // The $group param will be null if it is from the + // Drupal\og\Event\OgAdminRoutesEvent rather than the routing.yml version. + if (is_null($group)) { + $entity_type_id = $route_match->getRouteObject() + ->getOption('_og_entity_type_id'); + $group = $route_match->getParameter($entity_type_id); + } + + if (!Og::isGroup($group->getEntityTypeId(), $group->bundle())) { + return AccessResult::forbidden(); + } + + $membership_type_id = OgMembershipInterface::TYPE_DEFAULT; + if (!is_null($og_membership_type)) { + $membership_type_id = $og_membership_type->id(); + } + + $context = ['group' => $group]; + + return $this->entityTypeManager + ->getAccessControlHandler('og_membership') + ->createAccess($membership_type_id, $account, $context, TRUE); + } + +} diff --git a/src/Cache/Context/OgMembershipStateCacheContext.php b/src/Cache/Context/OgMembershipStateCacheContext.php index a3b0b7f6c..c3c1b36a9 100644 --- a/src/Cache/Context/OgMembershipStateCacheContext.php +++ b/src/Cache/Context/OgMembershipStateCacheContext.php @@ -76,14 +76,8 @@ public function getContext() { return self::NO_CONTEXT; } - $states = [ - OgMembershipInterface::STATE_ACTIVE, - OgMembershipInterface::STATE_PENDING, - OgMembershipInterface::STATE_BLOCKED, - ]; - /** @var \Drupal\og\OgMembershipInterface $membership */ - $membership = $this->membershipManager->getMembership($group, $this->user, $states); + $membership = $this->membershipManager->getMembership($group, $this->user, OgMembershipInterface::ALL_STATES); return $membership ? $membership->getState() : self::NO_CONTEXT; } diff --git a/src/Controller/OgAdminMembersController.php b/src/Controller/OgAdminMembersController.php index 9eb723aea..0fc4b48bf 100644 --- a/src/Controller/OgAdminMembersController.php +++ b/src/Controller/OgAdminMembersController.php @@ -3,14 +3,47 @@ namespace Drupal\og\Controller; use Drupal\Core\Controller\ControllerBase; +use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Entity\EntityTypeManagerInterface; +use Drupal\Core\Link; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\og\Entity\OgMembership; +use Drupal\og\OgMembershipInterface; +use Drupal\og\OgMembershipTypeInterface; use Drupal\views\Views; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * OgAdminMembersController class. */ class OgAdminMembersController extends ControllerBase { + /** + * The entity manager. + * + * @var \Drupal\Core\Entity\EntityTypeManagerInterface + */ + protected $entityTypeManager; + + /** + * Constructs a new EntityController. + * + * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager + * The entity type manager. + */ + public function __construct(EntityTypeManagerInterface $entity_type_manager) { + $this->entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager') + ); + } + /** * Display list of members that belong to the group. * @@ -21,13 +54,82 @@ class OgAdminMembersController extends ControllerBase { * The members overview View. */ public function membersList(RouteMatchInterface $route_match) { - $parameter_name = $route_match->getRouteObject()->getOption('_og_entity_type_id'); - - /** @var \Drupal\Core\Entity\EntityInterface $group */ - $group = $route_match->getParameter($parameter_name); - + $group_type_id = $route_match->getRouteObject()->getOption('_og_entity_type_id'); + $group = $route_match->getParameter($group_type_id); $arguments = [$group->getEntityTypeId(), $group->id()]; return Views::getView('og_members_overview')->executeDisplay('default', $arguments); } + /** + * Displays add membership links for available membership types. + * + * Returns default membership type if that's all that exists. + * + * @return array + * A render array for a list of the og membership types that can be added; + * however, if there is only one og membership type defined for the site, + * the function will return the default add member form. + */ + public function addPage(RouteMatchInterface $route_match) { + $entity_type_id = $route_match->getRouteObject() + ->getOption('_og_entity_type_id'); + + $group = $route_match->getParameter($entity_type_id); + + $membership_types = $this->entityTypeManager + ->getStorage('og_membership_type') + ->loadMultiple(); + + if ($membership_types && count($membership_types) == 1) { + return $this->addForm($group, $membership_types[OgMembershipInterface::TYPE_DEFAULT]); + } + + $build = [ + '#theme' => 'entity_add_list', + '#bundles' => [], + ]; + + $build['#cache']['tags'] = $this->entityTypeManager + ->getDefinition('og_membership_type') + ->getListCacheTags(); + + $add_link_params = [ + 'group' => $group->id(), + 'entity_type_id' => $group->getEntityType()->id(), + ]; + + foreach ($membership_types as $membership_type_id => $og_membership_type) { + $add_link_params['og_membership_type'] = $membership_type_id; + $build['#bundles'][$membership_type_id] = [ + 'label' => $og_membership_type->label(), + 'description' => NULL, + 'add_link' => Link::createFromRoute($og_membership_type->label(), 'entity.og_membership.add_form', $add_link_params), + ]; + } + + return $build; + } + + /** + * Provides the add member submission form. + * + * @param \Drupal\Core\Entity\EntityInterface $group + * The group entity. + * @param \Drupal\og\OgMembershipTypeInterface $og_membership_type + * The membership type entity. + * + * @return array + * The member add form. + */ + public function addForm(EntityInterface $group, OgMembershipTypeInterface $og_membership_type) { + /** @var \Drupal\og\Entity\OgMembership $og_membership */ + $og_membership = OgMembership::create([ + 'type' => $og_membership_type->id(), + 'entity_type' => $group->getEntityType()->id(), + 'entity_id' => $group->id(), + ]); + + return $this->entityFormBuilder()->getForm($og_membership, 'add'); + } + } diff --git a/src/Controller/OgAutocompleteController.php b/src/Controller/OgAutocompleteController.php new file mode 100644 index 000000000..7f976976e --- /dev/null +++ b/src/Controller/OgAutocompleteController.php @@ -0,0 +1,113 @@ +matcher = $matcher; + $this->keyValue = $key_value; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.autocomplete_matcher'), + $container->get('keyvalue')->get('entity_autocomplete') + ); + } + + /** + * Autocomplete the label of an entity. + * + * @param \Symfony\Component\HttpFoundation\Request $request + * The request object that contains the typed tags. + * @param \Drupal\Core\Entity\EntityInterface $group + * The group context for this autocomplete. + * @param string $target_type + * The ID of the target entity type. + * @param string $selection_handler + * The plugin ID of the entity reference selection handler. + * @param string $selection_settings_key + * The hashed key of the key/value entry that holds the selection handler + * settings. + * + * @return \Symfony\Component\HttpFoundation\JsonResponse + * The matched entity labels as a JSON response. + * + * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException + * Thrown if the selection settings key is not found in the key/value store + * or if it does not match the stored data. + */ + public function handleAutocomplete(Request $request, EntityInterface $group, $target_type, $selection_handler, $selection_settings_key) { + $matches = []; + // Get the typed string from the URL, if it exists. + if ($input = $request->query->get('q')) { + $typed_string = Tags::explode($input); + $typed_string = Unicode::strtolower(array_pop($typed_string)); + + // Selection settings are passed in as a hashed key of a serialized array + // stored in the key/value store. + $selection_settings = $this->keyValue->get($selection_settings_key, FALSE); + if ($selection_settings !== FALSE) { + $selection_settings_hash = Crypt::hmacBase64(serialize($selection_settings) . $target_type . $selection_handler, Settings::getHashSalt()); + if ($selection_settings_hash !== $selection_settings_key) { + // Disallow access when the selection settings hash does not match the + // passed-in key. + throw new AccessDeniedHttpException('Invalid selection settings key.'); + } + } + else { + // Disallow access when the selection settings key is not found in the + // key/value store. + throw new AccessDeniedHttpException(); + } + + $selection_settings['group'] = $group; + $matches = $this->matcher->getMatches($target_type, $selection_handler, $selection_settings, $typed_string); + } + + return new JsonResponse($matches); + } + +} diff --git a/src/Controller/SubscriptionController.php b/src/Controller/SubscriptionController.php index a320562d9..fca8dd4e2 100644 --- a/src/Controller/SubscriptionController.php +++ b/src/Controller/SubscriptionController.php @@ -5,7 +5,9 @@ use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityInterface; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Url; +use Drupal\og\Og; use Drupal\og\OgAccessInterface; use Drupal\og\OgMembershipInterface; use Drupal\og\OgMembershipTypeInterface; @@ -14,14 +16,12 @@ use Symfony\Component\DependencyInjection\ContainerInterface; use Symfony\Component\HttpFoundation\RedirectResponse; use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException; -use Drupal\og\Og; /** * Controller for OG subscription routes. */ class SubscriptionController extends ControllerBase { - /** * OG access service. * @@ -29,14 +29,24 @@ class SubscriptionController extends ControllerBase { */ protected $ogAccess; + /** + * The messenger service. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + /** * Constructs a SubscriptionController object. * * @param \Drupal\og\OgAccessInterface $og_access * The OG access service. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger service. */ - public function __construct(OgAccessInterface $og_access) { + public function __construct(OgAccessInterface $og_access, MessengerInterface $messenger) { $this->ogAccess = $og_access; + $this->messenger = $messenger; } /** @@ -44,7 +54,8 @@ public function __construct(OgAccessInterface $og_access) { */ public static function create(ContainerInterface $container) { return new static( - $container->get('og.access') + $container->get('og.access'), + $container->get('messenger') ); } @@ -55,14 +66,14 @@ public static function create(ContainerInterface $container) { * The entity type of the group entity. * @param \Drupal\Core\Entity\EntityInterface $group * The entity ID of the group entity. - * @param \Drupal\og\OgMembershipTypeInterface $membership_type + * @param \Drupal\og\OgMembershipTypeInterface $og_membership_type * The membership type to be used for creating the membership. * * @return mixed * Redirect user or show access denied if they are not allowed to subscribe, * otherwise provide a subscribe confirmation form. */ - public function subscribe($entity_type_id, EntityInterface $group, OgMembershipTypeInterface $membership_type) { + public function subscribe($entity_type_id, EntityInterface $group, OgMembershipTypeInterface $og_membership_type) { if (!$group instanceof ContentEntityInterface) { // Not a valid entity. throw new AccessDeniedHttpException(); @@ -83,12 +94,12 @@ public function subscribe($entity_type_id, EntityInterface $group, OgMembershipT if ($this->config('user.settings')->get('register') === USER_REGISTER_ADMINISTRATORS_ONLY) { $params = [':login' => $user_login_url]; - drupal_set_message($this->t('In order to join any group, you must login. After you have successfully done so, you will need to request membership again.', $params)); + $this->messenger->addMessage($this->t('In order to join any group, you must login. After you have successfully done so, you will need to request membership again.', $params)); } else { $user_register_url = Url::fromRoute('user.register', [], $destination)->toString(); $params = [':register' => $user_register_url, ':login' => $user_login_url]; - drupal_set_message($this->t('In order to join any group, you must login or register a new account. After you have successfully done so, you will need to request membership again.', $params)); + $this->messenger->addMessage($this->t('In order to join any group, you must login or register a new account. After you have successfully done so, you will need to request membership again.', $params)); } return new RedirectResponse(Url::fromRoute('user.page')->setAbsolute(TRUE)->toString()); @@ -119,7 +130,7 @@ public function subscribe($entity_type_id, EntityInterface $group, OgMembershipT } if ($redirect) { - drupal_set_message($message, 'warning'); + $this->messenger->addMessage($message, 'warning'); return new RedirectResponse($group->toUrl()->setAbsolute(TRUE)->toString()); } @@ -127,7 +138,7 @@ public function subscribe($entity_type_id, EntityInterface $group, OgMembershipT throw new AccessDeniedHttpException(); } - $membership = Og::createMembership($group, $user, $membership_type->id()); + $membership = Og::createMembership($group, $user, $og_membership_type->id()); $form = $this->entityFormBuilder()->getForm($membership, 'subscribe'); return $form; @@ -146,13 +157,7 @@ public function subscribe($entity_type_id, EntityInterface $group, OgMembershipT public function unsubscribe(ContentEntityInterface $group) { $user = $this->currentUser(); - $states = [ - OgMembershipInterface::STATE_ACTIVE, - OgMembershipInterface::STATE_PENDING, - OgMembershipInterface::STATE_BLOCKED, - ]; - - if (!$membership = Og::getMembership($group, $user, $states)) { + if (!$membership = Og::getMembership($group, $user, OgMembershipInterface::ALL_STATES)) { // User is not a member. throw new AccessDeniedHttpException(); } @@ -164,7 +169,7 @@ public function unsubscribe(ContentEntityInterface $group) { if ($group instanceof EntityOwnerInterface && $group->getOwnerId() == $user->id()) { // The user is the manager of the group. - drupal_set_message($this->t('As the manager of %group, you can not leave the group.', ['%group' => $group->label()])); + $this->messenger->addMessage($this->t('As the manager of %group, you can not leave the group.', ['%group' => $group->label()])); return new RedirectResponse($group->toUrl() ->setAbsolute() diff --git a/src/Element/OgAutocomplete.php b/src/Element/OgAutocomplete.php new file mode 100644 index 000000000..9047564b8 --- /dev/null +++ b/src/Element/OgAutocomplete.php @@ -0,0 +1,61 @@ +id(); + } + + // Store the selection settings in the key/value store and pass a hashed key + // in the route parameters. + $selection_settings = isset($element['#selection_settings']) ? $element['#selection_settings'] : []; + $data = serialize($selection_settings) . $element['#target_type'] . $element['#selection_handler']; + $selection_settings_key = Crypt::hmacBase64($data, Settings::getHashSalt()); + + $key_value_storage = \Drupal::keyValue('entity_autocomplete'); + if (!$key_value_storage->has($selection_settings_key)) { + $key_value_storage->set($selection_settings_key, $selection_settings); + } + + $element['#autocomplete_route_name'] = 'og.entity_autocomplete'; + $element['#autocomplete_route_parameters'] = [ + 'target_type' => $element['#target_type'], + 'selection_handler' => $element['#selection_handler'], + 'selection_settings_key' => $selection_settings_key, + 'entity_type_id' => $element['#og_group']->getEntityTypeId(), + 'group' => $element['#og_group']->id(), + ]; + + return $element; + } + +} diff --git a/src/Entity/OgMembership.php b/src/Entity/OgMembership.php index c9ccf9788..30fa58d4e 100644 --- a/src/Entity/OgMembership.php +++ b/src/Entity/OgMembership.php @@ -59,6 +59,7 @@ * fieldable = TRUE, * bundle_entity_type = "og_membership_type", * entity_keys = { + * "uuid" = "uuid", * "id" = "id", * "bundle" = "type", * }, @@ -66,16 +67,38 @@ * "bundle" = "type", * }, * handlers = { + * "access" = "Drupal\og\OgMembershipAccessControlHandler", * "views_data" = "Drupal\og\OgMembershipViewsData", + * "list_builder" = "Drupal\Core\Entity\EntityListBuilder", + * "view_builder" = "Drupal\Core\Entity\EntityViewBuilder", * "form" = { * "subscribe" = "Drupal\og\Form\GroupSubscribeForm", * "unsubscribe" = "Drupal\og\Form\GroupUnsubscribeConfirmForm", + * "add" = "Drupal\og\Form\OgMembershipForm", + * "edit" = "Drupal\og\Form\OgMembershipForm", + * "delete" = "Drupal\og\Form\OgMembershipDeleteForm", * }, - * } + * }, + * links = { + * "edit-form" = "/group/{entity_type_id}/{group}/admin/members/{og_membership}/edit", + * "delete-form" = "/group/{entity_type_id}/{group}/admin/members/{og_membership}/delete", + * "canonical" = "/group/{entity_type_id}/{group}/admin/members/{og_membership}/edit" + * }, + * field_ui_base_route = "entity.og_membership_type.edit_form" * ) */ class OgMembership extends ContentEntityBase implements OgMembershipInterface { + /** + * {@inheritdoc} + */ + protected function urlRouteParameters($rel) { + $uri_route_parameters = parent::urlRouteParameters($rel); + $uri_route_parameters['entity_type_id'] = $this->getGroupEntityType(); + $uri_route_parameters['group'] = $this->getGroupId(); + return $uri_route_parameters; + } + /** * {@inheritdoc} */ @@ -356,9 +379,23 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setSetting('target_type', 'og_membership_type'); $fields['uid'] = BaseFieldDefinition::create('entity_reference') - ->setLabel(t('Member User ID')) + ->setLabel(t('Username')) ->setDescription(t('The user ID of the member.')) - ->setSetting('target_type', 'user'); + ->setSetting('target_type', 'user') + ->setSetting('handler', 'og:user') + ->setConstraints(['UniqueOgMembership' => []]) + ->setDisplayOptions('form', [ + 'type' => 'og_autocomplete', + 'weight' => -1, + 'settings' => [ + 'match_operator' => 'CONTAINS', + 'size' => 60, + 'placeholder' => '', + ], + ]) + ->setDisplayConfigurable('view', TRUE) + ->setDisplayConfigurable('form', TRUE) + ->setRequired(TRUE); $fields['entity_type'] = BaseFieldDefinition::create('string') ->setLabel(t('Group entity type')) @@ -372,21 +409,38 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { ->setLabel(t('Group entity ID')) ->setDescription(t('The entity ID of the group.')); - $fields['state'] = BaseFieldDefinition::create('string') + $fields['state'] = BaseFieldDefinition::create('list_string') ->setLabel(t('State')) ->setDescription(t('The user membership state: active, pending, or blocked.')) - ->setDefaultValue(OgMembershipInterface::STATE_ACTIVE); + ->setDefaultValue(OgMembershipInterface::STATE_ACTIVE) + ->setSettings([ + 'allowed_values' => [ + OgMembershipInterface::STATE_ACTIVE => t('Active'), + OgMembershipInterface::STATE_PENDING => t('Pending'), + OgMembershipInterface::STATE_BLOCKED => t('Blocked'), + ], + ]) + ->setDisplayOptions('form', [ + 'type' => 'options_buttons', + 'weight' => 0, + ]) + ->setDisplayConfigurable('view', TRUE) + ->setDisplayConfigurable('form', TRUE) + ->setRequired(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) - ->setDisplayOptions('view', [ - 'label' => 'hidden', - 'type' => 'entity_reference_label', + ->setSetting('target_type', 'og_role') + ->setSetting('handler', 'og:og_role') + ->setConstraints(['ValidOgRole' => []]) + ->setDisplayOptions('form', [ + 'type' => 'options_buttons', 'weight' => 0, ]) - ->setSetting('target_type', 'og_role'); + ->setDisplayConfigurable('view', TRUE) + ->setDisplayConfigurable('form', TRUE); $fields['created'] = BaseFieldDefinition::create('created') ->setLabel(t('Create')) @@ -463,7 +517,7 @@ public function preSave(EntityStorageInterface $storage) { ->execute(); if ($count) { - throw new \LogicException(sprintf('An OG membership already exists for group of entity-type %s and ID: %s', $entity_type_id, $this->getGroup()->id())); + throw new \LogicException(sprintf('An OG membership already exists for uid %s in group of entity-type %s and ID: %s', $this->get('uid')->target_id, $entity_type_id, $this->getGroup()->id())); } parent::preSave($storage); diff --git a/src/Entity/OgMembershipType.php b/src/Entity/OgMembershipType.php index 821b29966..295c80f2f 100644 --- a/src/Entity/OgMembershipType.php +++ b/src/Entity/OgMembershipType.php @@ -3,6 +3,8 @@ namespace Drupal\og\Entity; use Drupal\Core\Config\Entity\ConfigEntityBase; +use Drupal\field\Entity\FieldConfig; +use Drupal\og\OgMembershipInterface; use Drupal\og\OgMembershipTypeInterface; /** @@ -20,11 +22,31 @@ * @ConfigEntityType( * id = "og_membership_type", * label = @Translation("OG membership type"), + * handlers = { + * "access" = "Drupal\Core\Entity\EntityAccessControlHandler", + * "form" = { + * "add" = "Drupal\og\Form\OgMembershipTypeForm", + * "edit" = "Drupal\og\Form\OgMembershipTypeForm", + * "delete" = "Drupal\Core\Entity\EntityDeleteForm" + * }, + * "list_builder" = "Drupal\og\OgMembershipTypeListBuilder" + * }, + * admin_permission = "administer group", * config_prefix = "og_membership_type", * bundle_of = "og_membership", * entity_keys = { * "id" = "type", * "label" = "name" + * }, + * config_export = { + * "type", + * "name", + * "description" + * }, + * links = { + * "edit-form" = "/admin/structure/membership-types/manage/{membership_type}", + * "delete-form" = "/admin/structure/membership-types/manage/{membership_type}/delete", + * "collection" = "/admin/structure/membership-types", * } * ) */ @@ -42,4 +64,40 @@ public function id() { return $this->type; } + /** + * {@inheritdoc} + */ + public function save() { + $status = parent::save(); + + if (\Drupal::isConfigSyncing()) { + // Do not create config while config import is in progress. + return; + } + + if ($status === SAVED_NEW) { + FieldConfig::create([ + 'field_name' => 'og_membership_request', + 'entity_type' => 'og_membership', + 'bundle' => $this->id(), + 'label' => 'Request Membership', + 'description' => 'Explain the motivation for your request to join this group.', + 'translatable' => TRUE, + 'settings' => [], + ])->save(); + } + + return $status; + } + + /** + * {@inheritdoc} + */ + public function delete() { + if ($this->id() === OgMembershipInterface::TYPE_DEFAULT && !\Drupal::isConfigSyncing()) { + throw new \Exception("The default OG membership type cannot be deleted."); + } + parent::delete(); + } + } diff --git a/src/Event/OgAdminRoutesEvent.php b/src/Event/OgAdminRoutesEvent.php index 575ccf90c..4ae012172 100644 --- a/src/Event/OgAdminRoutesEvent.php +++ b/src/Event/OgAdminRoutesEvent.php @@ -2,6 +2,7 @@ namespace Drupal\og\Event; +use Drupal\Component\Utility\NestedArray; use Symfony\Component\EventDispatcher\Event; /** @@ -40,8 +41,9 @@ public function getRoutes($entity_type_id) { $routes_info[$name] = $route_info; - // Add default values. - $routes_info[$name] += [ + // Add default values. NestedArray::mergeDeep allows deep data to not be + // overwritten with the defaults. + $defaults = [ 'description' => '', 'requirements' => [ @@ -64,6 +66,8 @@ public function getRoutes($entity_type_id) { '_title' => $route_info['title'], ], ]; + + $routes_info[$name] = NestedArray::mergeDeep($defaults, $routes_info[$name]); } return $routes_info; diff --git a/src/EventSubscriber/OgEventSubscriber.php b/src/EventSubscriber/OgEventSubscriber.php index 3463cd3f2..b0f495890 100644 --- a/src/EventSubscriber/OgEventSubscriber.php +++ b/src/EventSubscriber/OgEventSubscriber.php @@ -357,12 +357,21 @@ public function provideOgAdminRoutes(OgAdminRoutesEventInterface $event) { 'description' => 'Manage members', 'path' => 'members', 'requirements' => [ - '_og_user_access_group' => 'administer group', + '_og_user_access_group' => 'administer group|manage members', // Views module must be enabled. '_module_dependencies' => 'views', ], ]; + $routes_info['add_membership_page'] = [ + 'controller' => '\Drupal\og\Controller\OgAdminMembersController::addPage', + 'title' => 'Add member', + 'path' => 'members/add', + 'requirements' => [ + '_og_membership_add_access' => 'TRUE', + ], + ]; + $event->setRoutesInfo($routes_info); } diff --git a/src/Form/GroupSubscribeForm.php b/src/Form/GroupSubscribeForm.php index d77fc69a4..03c16e8bd 100644 --- a/src/Form/GroupSubscribeForm.php +++ b/src/Form/GroupSubscribeForm.php @@ -11,6 +11,7 @@ use Drupal\og\OgAccessInterface; use Drupal\og\OgMembershipInterface; use Symfony\Component\DependencyInjection\ContainerInterface; +use Drupal\Core\Messenger\MessengerInterface; /** * Provides a form for subscribing to a group. @@ -30,6 +31,13 @@ class GroupSubscribeForm extends ContentEntityForm { */ protected $ogAccess; + /** + * The messenger. + * + * @var \Drupal\Core\Messenger\MessengerInterface + */ + protected $messenger; + /** * Constructs a GroupSubscribeForm. * @@ -41,6 +49,8 @@ class GroupSubscribeForm extends ContentEntityForm { * The entity type bundle service. * @param \Drupal\Component\Datetime\TimeInterface $time * The time service. + * @param \Drupal\Core\Messenger\MessengerInterface $messenger + * The messenger. * * @todo Set the `EntityRepositoryInterface` type hint on the second argument * once Drupal 8.6.0 is released. It is currently omitted to preserve @@ -48,9 +58,16 @@ class GroupSubscribeForm extends ContentEntityForm { * * @see https://github.com/Gizra/og/issues/397 */ - public function __construct(OgAccessInterface $og_access, $entity_repository, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL) { + public function __construct( + OgAccessInterface $og_access, + EntityRepositoryInterface $entity_repository, + EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, + TimeInterface $time = NULL, + MessengerInterface $messenger + ) { parent::__construct($entity_repository, $entity_type_bundle_info, $time); $this->ogAccess = $og_access; + $this->messenger = $messenger; } /** @@ -72,7 +89,8 @@ public static function create(ContainerInterface $container) { $container->get('og.access'), $container->get($entity_repository_service), $container->get('entity_type.bundle.info'), - $container->get('datetime.time') + $container->get('datetime.time'), + $container->get('messenger') ); } @@ -234,8 +252,8 @@ public function submitForm(array &$form, FormStateInterface $form_state) { /** @var EntityInterface $group */ $group = $membership->getGroup(); - $message = $membership->isActive() ? $this->t('Your are now subscribed to the group.') : $this->t('Your subscription request was sent.'); - drupal_set_message($message); + $message = $membership->isActive() ? $this->t('You are now subscribed to the group.') : $this->t('Your subscription request has been sent.'); + $this->messenger()->addMessage($message); // User doesn't have access to the group entity, so redirect to front page, // otherwise back to the group entity. diff --git a/src/Form/GroupUnsubscribeConfirmForm.php b/src/Form/GroupUnsubscribeConfirmForm.php index 08f4fa0aa..10767431a 100644 --- a/src/Form/GroupUnsubscribeConfirmForm.php +++ b/src/Form/GroupUnsubscribeConfirmForm.php @@ -4,6 +4,7 @@ use Drupal\Core\Entity\ContentEntityDeleteForm; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Messenger\MessengerTrait; use Drupal\Core\Url; /** @@ -11,6 +12,8 @@ */ class GroupUnsubscribeConfirmForm extends ContentEntityDeleteForm { + use MessengerTrait; + /** * {@inheritdoc} */ @@ -62,7 +65,7 @@ public function submitForm(array &$form, FormStateInterface $form_state) { $form_state->setRedirectUrl($redirect); $membership->delete(); - drupal_set_message($this->t('You have unsubscribed from the group.')); + $this->messenger()->addMessage($this->t('You have unsubscribed from the group.')); } } diff --git a/src/Form/OgMembershipDeleteForm.php b/src/Form/OgMembershipDeleteForm.php new file mode 100644 index 000000000..eaa049d8c --- /dev/null +++ b/src/Form/OgMembershipDeleteForm.php @@ -0,0 +1,53 @@ +getEntity(); + + return $this->t("%user has been unsubscribed from %group.", [ + '%user' => $membership->getOwner()->getDisplayName(), + '%group' => $membership->getGroup()->label(), + ]); + } + + /** + * {@inheritdoc} + */ + protected function logDeletionMessage() { + /** @var \Drupal\og\Entity\OgMembership $entity */ + $membership = $this->getEntity(); + + $this->logger('og')->notice("OG Membership: deleted the @membership_type membership for the user uid: @uid to the group of the entity-type @group_type and ID: @gid", [ + '@membership_type' => $membership->getType(), + '@uid' => $membership->getOwner()->id(), + '@group_type' => $membership->getGroupEntityType(), + '@gid' => $membership->getGroupId(), + ]); + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + /** @var \Drupal\og\Entity\OgMembership $entity */ + $membership = $this->getEntity(); + + return $this->t("Are you sure you want to unsubscribe %user from %group?", [ + '%user' => $membership->getOwner()->getDisplayName(), + '%group' => $membership->getGroup()->label(), + ]); + } + +} diff --git a/src/Form/OgMembershipForm.php b/src/Form/OgMembershipForm.php new file mode 100644 index 000000000..73dcd959c --- /dev/null +++ b/src/Form/OgMembershipForm.php @@ -0,0 +1,150 @@ +ogAccess = $og_access; + $this->messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.manager'), + $container->get('entity_type.bundle.info'), + $container->get('datetime.time'), + $container->get('og.access'), + $container->get('messenger') + ); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + /** @var \Drupal\og\Entity\OgMembership $entity */ + $entity = $this->getEntity(); + /** @var \Drupal\Core\Entity\ContentEntityInterface $group */ + $group = $entity->getGroup(); + + $form = parent::form($form, $form_state); + $form['#title'] = $this->t('Add member to %group', ['%group' => $group->label()]); + $form['entity_type'] = ['#value' => $entity->getEntityType()->id()]; + $form['entity_id'] = ['#value' => $group->id()]; + + if ($entity->getType() != OgMembershipInterface::TYPE_DEFAULT) { + $form['membership_type'] = [ + '#title' => $this->t('Membership type'), + '#type' => 'item', + '#plain_text' => $entity->type->entity->label(), + '#weight' => -2, + ]; + } + + if ($this->operation == 'edit') { + $form['#title'] = $this->t('Edit membership in %group', ['%group' => $group->label()]); + $form['uid']['#access'] = FALSE; + $form['member'] = [ + '#title' => $this->t('Member name'), + '#type' => 'item', + '#markup' => $entity->getOwner()->getDisplayName(), + '#weight' => -10, + ]; + } + + // Require the 'manage members' permission to be able to edit roles. + $form['roles']['#access'] = $this->ogAccess + ->userAccess($group, 'manage members') + ->isAllowed(); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $membership = $this->entity; + $insert = $membership->isNew(); + $membership->save(); + + $membership_link = $membership->link($this->t('View')); + + $context = [ + '@membership_type' => $membership->getType(), + '@uid' => $membership->getOwner()->id(), + '@group_type' => $membership->getGroupEntityType(), + '@gid' => $membership->getGroupId(), + 'link' => $membership_link, + ]; + + $t_args = [ + '%user' => $membership->getOwner()->link(), + '%group' => $membership->getGroup()->link(), + ]; + + if ($insert) { + $this->logger('og')->notice('OG Membership: added the @membership_type membership for the use uid @uid to the group of the entity-type @group_type and ID @gid.', $context); + $this->messenger->addMessage($this->t('Added %user to %group.', $t_args)); + return; + } + + $this->logger('og')->notice('OG Membership: updated the @membership_type membership for the use uid @uid to the group of the entity-type @group_type and ID @gid.', $context); + $this->messenger->addMessage($this->t('Updated the membership for %user to %group.', $t_args)); + } + +} diff --git a/src/Form/OgMembershipTypeForm.php b/src/Form/OgMembershipTypeForm.php new file mode 100644 index 000000000..540970ec6 --- /dev/null +++ b/src/Form/OgMembershipTypeForm.php @@ -0,0 +1,138 @@ +entityTypeManager = $entity_manager; + $this->messenger = $messenger; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager'), + $container->get('messenger') + ); + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + $type = $this->entity; + if ($this->operation == 'add') { + $form['#title'] = $this->t('Add membership type'); + } + else { + $form['#title'] = $this->t('Edit %label membership type', ['%label' => $type->label()]); + } + + $form['name'] = [ + '#title' => $this->t('Name'), + '#type' => 'textfield', + '#default_value' => $type->label(), + '#description' => $this->t('The human-readable name of this membership type.'), + '#required' => TRUE, + '#size' => 30, + ]; + + $form['type'] = [ + '#type' => 'machine_name', + '#default_value' => $type->id(), + '#maxlength' => EntityTypeInterface::BUNDLE_MAX_LENGTH, + '#machine_name' => [ + 'exists' => ['Drupal\og\Entity\OgMembershipType', 'load'], + 'source' => ['name'], + ], + '#description' => $this->t('A unique machine-readable name for this membership type. It must only contain lowercase letters, numbers, and underscores.'), + ]; + return $this->protectBundleIdElement($form); + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#value'] = $this->t('Save membership type'); + $actions['delete']['#value'] = $this->t('Delete membership type'); + return $actions; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + $id = trim($form_state->getValue('type')); + // '0' is invalid, since elsewhere we check it using empty(). + if ($id == '0') { + $form_state->setErrorByName('type', $this->t("Invalid machine-readable name. Enter a name other than %invalid.", ['%invalid' => $id])); + } + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $type = $this->entity; + $type->set('type', trim($type->id())); + $type->set('name', trim($type->label())); + + $status = $type->save(); + + $t_args = ['%name' => $type->label()]; + + if ($status == SAVED_UPDATED) { + $this->messenger->addMessage($this->t('The membership type %name has been updated.', $t_args)); + } + elseif ($status == SAVED_NEW) { + $this->messenger->addMessage($this->t('The membership type %name has been added.', $t_args)); + $context = array_merge($t_args, ['link' => $type->link($this->t('View'), 'collection')]); + $this->logger('og')->notice('Added membership type %name.', $context); + } + + $this->entityTypeManager->clearCachedFieldDefinitions(); + $form_state->setRedirectUrl($type->urlInfo('collection')); + } + +} diff --git a/src/GroupTypeManager.php b/src/GroupTypeManager.php index adc596830..5fc6cec1b 100644 --- a/src/GroupTypeManager.php +++ b/src/GroupTypeManager.php @@ -6,7 +6,7 @@ use Drupal\Core\Entity\EntityTypeBundleInfoInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Routing\RouteBuilderInterface; -use Drupal\Core\State\StateInterface; +use Drupal\Core\Cache\CacheBackendInterface; use Drupal\og\Event\GroupCreationEvent; use Drupal\og\Event\GroupCreationEventInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; @@ -57,11 +57,11 @@ class GroupTypeManager implements GroupTypeManagerInterface { protected $eventDispatcher; /** - * The state service. + * The cache backend. * - * @var \Drupal\Core\State\StateInterface + * @var \Drupal\Core\Cache\CacheBackendInterface */ - protected $state; + protected $cache; /** * The OG permission manager. @@ -142,8 +142,8 @@ class GroupTypeManager implements GroupTypeManagerInterface { * The service providing information about bundles. * @param \Symfony\Component\EventDispatcher\EventDispatcherInterface $event_dispatcher * The event dispatcher. - * @param \Drupal\Core\State\StateInterface $state - * The state service. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache + * The cache backend. * @param \Drupal\og\PermissionManagerInterface $permission_manager * The OG permission manager. * @param \Drupal\og\OgRoleManagerInterface $og_role_manager @@ -153,11 +153,11 @@ class GroupTypeManager implements GroupTypeManagerInterface { * @param \Drupal\og\OgGroupAudienceHelperInterface $group_audience_helper * The OG group audience helper. */ - public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EventDispatcherInterface $event_dispatcher, StateInterface $state, PermissionManagerInterface $permission_manager, OgRoleManagerInterface $og_role_manager, RouteBuilderInterface $route_builder, OgGroupAudienceHelperInterface $group_audience_helper) { + public function __construct(ConfigFactoryInterface $config_factory, EntityTypeManagerInterface $entity_type_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info, EventDispatcherInterface $event_dispatcher, CacheBackendInterface $cache, PermissionManagerInterface $permission_manager, OgRoleManagerInterface $og_role_manager, RouteBuilderInterface $route_builder, OgGroupAudienceHelperInterface $group_audience_helper) { $this->configFactory = $config_factory; $this->entityTypeBundleInfo = $entity_type_bundle_info; $this->eventDispatcher = $event_dispatcher; - $this->state = $state; + $this->cache = $cache; $this->permissionManager = $permission_manager; $this->ogRoleManager = $og_role_manager; $this->routeBuilder = $route_builder; @@ -251,6 +251,9 @@ public function getGroupBundleIdsByGroupContentBundle($group_content_entity_type */ public function getGroupContentBundleIdsByGroupBundle($group_entity_type_id, $group_bundle_id) { $group_relation_map = $this->getGroupRelationMap(); + if (!is_array($group_relation_map) && property_exists($group_relation_map,'data')) { + $group_relation_map = $group_relation_map->data; + } return isset($group_relation_map[$group_entity_type_id][$group_bundle_id]) ? $group_relation_map[$group_entity_type_id][$group_bundle_id] : []; } @@ -331,7 +334,7 @@ public function resetGroupMap() { */ public function resetGroupRelationMap() { $this->groupRelationMap = []; - $this->state->delete(self::GROUP_RELATION_MAP_CACHE_KEY); + $this->cache->delete(self::GROUP_RELATION_MAP_CACHE_KEY); } /** @@ -370,7 +373,7 @@ protected function refreshGroupMap() { */ 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)) { + if ($group_relation_map = $this->cache->get(self::GROUP_RELATION_MAP_CACHE_KEY)) { $this->groupRelationMap = $group_relation_map; return; } @@ -395,7 +398,7 @@ protected function refreshGroupRelationMap() { } } // Cache the map. - $this->state->set(self::GROUP_RELATION_MAP_CACHE_KEY, $this->groupRelationMap); + $this->cache->set(self::GROUP_RELATION_MAP_CACHE_KEY, $this->groupRelationMap); } } diff --git a/src/MembershipManager.php b/src/MembershipManager.php index 7677d35f4..a8743634e 100644 --- a/src/MembershipManager.php +++ b/src/MembershipManager.php @@ -2,6 +2,7 @@ namespace Drupal\og; +use Drupal\Core\Database\Connection; use Drupal\Component\Utility\NestedArray; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; @@ -36,6 +37,13 @@ class MembershipManager implements MembershipManagerInterface { */ protected $groupAudienceHelper; + /** + * The database service. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + /** * Constructs a MembershipManager object. * @@ -43,10 +51,13 @@ class MembershipManager implements MembershipManagerInterface { * The entity type manager. * @param \Drupal\og\OgGroupAudienceHelperInterface $group_audience_helper * The OG group audience helper. + * @param \Drupal\Core\Database\Connection $database + * The database service. */ - public function __construct(EntityTypeManagerInterface $entity_type_manager, OgGroupAudienceHelperInterface $group_audience_helper) { + public function __construct(EntityTypeManagerInterface $entity_type_manager, OgGroupAudienceHelperInterface $group_audience_helper, Connection $database) { $this->entityTypeManager = $entity_type_manager; $this->groupAudienceHelper = $group_audience_helper; + $this->database = $database; } /** @@ -81,54 +92,122 @@ public function getUserGroups(AccountInterface $user, array $states = [OgMembers * {@inheritdoc} */ public function getMemberships(AccountInterface $user, array $states = [OgMembershipInterface::STATE_ACTIVE]) { - // Get a string identifier of the states, so we can retrieve it from cache. - sort($states); - $states_identifier = implode('|', array_unique($states)); + // When an empty array is passed, retrieve memberships with all possible + // states. + $states = $this->prepareConditionArray($states, OgMembership::ALL_STATES); $identifier = [ __METHOD__, 'user', $user->id(), - $states_identifier, + implode('|', $states), ]; $identifier = implode(':', $identifier); - // Return cached result if it exists. - if (isset($this->cache[$identifier])) { - return $this->cache[$identifier]; + // Use cached result if it exists. + if (!isset($this->cache[$identifier])) { + $query = $this->entityTypeManager + ->getStorage('og_membership') + ->getQuery() + ->condition('uid', $user->id()) + ->condition('state', $states, 'IN'); + + $this->cache[$identifier] = $query->execute(); + } + + return $this->loadMemberships($this->cache[$identifier]); + } + + /** + * {@inheritdoc} + */ + public function getMembership(EntityInterface $group, AccountInterface $user, array $states = [OgMembershipInterface::STATE_ACTIVE]) { + foreach ($this->getMemberships($user, $states) as $membership) { + if ($membership->getGroupEntityType() === $group->getEntityTypeId() && $membership->getGroupId() === $group->id()) { + return $membership; + } } + // No membership matches the request. + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getGroupMembershipCount(EntityInterface $group, array $states = [OgMembershipInterface::STATE_ACTIVE]) { $query = $this->entityTypeManager ->getStorage('og_membership') ->getQuery() - ->condition('uid', $user->id()); + ->condition('entity_id', $group->id()); if ($states) { $query->condition('state', $states, 'IN'); } - $results = $query->execute(); - - /** @var \Drupal\og\Entity\OgMembership[] $memberships */ - $this->cache[$identifier] = $this->entityTypeManager - ->getStorage('og_membership') - ->loadMultiple($results); - - return $this->cache[$identifier]; + $query->count(); + return $query->execute(); } /** * {@inheritdoc} */ - public function getMembership(EntityInterface $group, AccountInterface $user, array $states = [OgMembershipInterface::STATE_ACTIVE]) { - foreach ($this->getMemberships($user, $states) as $membership) { - if ($membership->getGroupEntityType() === $group->getEntityTypeId() && $membership->getGroupId() === $group->id()) { - return $membership; + public function getGroupMembershipIdsByRoleNames(EntityInterface $group, array $role_names, array $states = [OgMembershipInterface::STATE_ACTIVE]) { + if (empty($role_names)) { + throw new \InvalidArgumentException('The array of role names should not be empty.'); + } + + // In case the 'member' role is one of the requested roles, we just need to + // return all memberships. We can safely ignore all other roles. + $retrieve_all_memberships = FALSE; + if (in_array(OgRoleInterface::AUTHENTICATED, $role_names)) { + $retrieve_all_memberships = TRUE; + $role_names = [OgRoleInterface::AUTHENTICATED]; + } + + $role_names = $this->prepareConditionArray($role_names); + $states = $this->prepareConditionArray($states, OgMembership::ALL_STATES); + + $identifier = [ + __METHOD__, + $group->id(), + implode('|', $role_names), + implode('|', $states), + ]; + $identifier = implode(':', $identifier); + + // Only query the database if no cached result exists. + if (!isset($this->cache[$identifier])) { + $entity_type_id = $group->getEntityTypeId(); + + $query = $this->entityTypeManager + ->getStorage('og_membership') + ->getQuery() + ->condition('entity_type', $entity_type_id) + ->condition('entity_id', $group->id()) + ->condition('state', $states, 'IN'); + + if (!$retrieve_all_memberships) { + $bundle_id = $group->bundle(); + $role_ids = array_map(function ($role_name) use ($entity_type_id, $bundle_id) { + return implode('-', [$entity_type_id, $bundle_id, $role_name]); + }, $role_names); + + $query->condition('roles', $role_ids, 'IN'); } + + $this->cache[$identifier] = $query->execute(); } - // No membership matches the request. - return NULL; + return $this->cache[$identifier]; + } + + /** + * {@inheritdoc} + */ + public function getGroupMembershipsByRoleNames(EntityInterface $group, array $role_names, array $states = [OgMembershipInterface::STATE_ACTIVE]) { + $ids = $this->getGroupMembershipIdsByRoleNames($group, $role_names, $states); + return $this->loadMemberships($ids); } /** @@ -188,7 +267,9 @@ public function getGroupIds(EntityInterface $entity, $group_type_id = NULL, $gro // Compile a list of group target IDs. $target_ids = array_map(function ($value) { - return $value['target_id']; + if (isset($value['target_id'])) { + return $value['target_id']; + } }, $entity->get($field->getName())->getValue()); if (empty($target_ids)) { @@ -322,4 +403,69 @@ public function reset() { $this->cache = []; } + /** + * Prepares a conditional array for use in a cache identifier and query. + * + * This will filter out any duplicate values from the array and sort the + * values so that a consistent cache identifier can be generated. Optionally + * it can substitute an empty array with a default value. + * + * @param array $value + * The array to prepare. + * @param array|null $default + * An optional default value to use in case the passed in value is empty. If + * set to NULL this will be ignored. + * + * @return array + * The prepared array. + */ + protected function prepareConditionArray(array $value, array $default = NULL) { + // Fall back to the default value if the passed in value is empty and a + // default value is given. + if (empty($value) && $default !== NULL) { + $value = $default; + } + sort($value); + return array_unique($value); + } + + /** + * Returns the full membership entities with the given memberships IDs. + * + * @param array $ids + * The IDs of the memberships to load. + * + * @return \Drupal\Core\Entity\EntityInterface[] + * The membership entities. + */ + protected function loadMemberships(array $ids) { + if (empty($ids)) { + return []; + } + + return $this->entityTypeManager + ->getStorage('og_membership') + ->loadMultiple($ids); + } + + /** + * {@inheritdoc} + */ + public function getGroupMemberships(EntityInterface $group, array $states = [OgMembershipInterface::STATE_ACTIVE], $range = NULL) { + $query = $this->database->select('og_membership', 'ogm') + ->fields('ogm', ['uid']) + ->condition('entity_id', $group->id()); + + if ($states) { + $query->condition('state', $states, 'IN'); + } + if ($range) { + $query->range(0, $range); + } + $result = $query->execute()->fetchAllAssoc('uid', 'PDO::FETCH_ASSOC'); + $member_ids = array_keys($result); + $members = $this->entityTypeManager->getStorage('user')->loadMultiple($member_ids); + return $members; + } + } diff --git a/src/MembershipManagerInterface.php b/src/MembershipManagerInterface.php index d117645bb..78d46d70e 100644 --- a/src/MembershipManagerInterface.php +++ b/src/MembershipManagerInterface.php @@ -61,13 +61,28 @@ public function getUserGroups(AccountInterface $user, array $states = [OgMembers * @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. + * (optional) Array with the states to return. Defaults to only returning + * active memberships. In order to retrieve all memberships regardless of + * state, pass `OgMembershipInterface::ALL_STATES`. * * @return \Drupal\og\OgMembershipInterface[] * An array of OgMembership entities, keyed by ID. */ public function getMemberships(AccountInterface $user, array $states = [OgMembershipInterface::STATE_ACTIVE]); + /** + * Returns the number of group memberships for a given group. + * + * @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. + * + * @return int + * The number of memberships for the group. + */ + public function getGroupMembershipCount(EntityInterface $group, array $states = [OgMembershipInterface::STATE_ACTIVE]); + /** * Returns the group membership for a given user and group. * @@ -76,7 +91,9 @@ public function getMemberships(AccountInterface $user, array $states = [OgMember * @param \Drupal\Core\Session\AccountInterface $user * The user to get the membership for. * @param array $states - * (optional) Array with the state to return. Defaults to active. + * (optional) Array with the states to return. Defaults to only returning + * active memberships. In order to retrieve all memberships regardless of + * state, pass `OgMembershipInterface::ALL_STATES`. * * @return \Drupal\og\OgMembershipInterface|null * The OgMembership entity. NULL will be returned if no membership is @@ -84,6 +101,42 @@ public function getMemberships(AccountInterface $user, array $states = [OgMember */ public function getMembership(EntityInterface $group, AccountInterface $user, array $states = [OgMembershipInterface::STATE_ACTIVE]); + /** + * Returns the membership IDs of the given group filtered by role names. + * + * @param \Drupal\Core\Entity\EntityInterface $group + * The group entity for which to return the memberships. + * @param array $role_names + * An array of role names to filter by. In order to retrieve a list of all + * membership IDs, pass `[OgRoleInterface::AUTHENTICATED]`. + * @param array $states + * (optional) Array with the states to return. Defaults to only returning + * active membership IDs. In order to retrieve all membership IDs regardless + * of state, pass `OgMembershipInterface::ALL_STATES`. + * + * @return \Drupal\Core\Entity\EntityInterface[] + * The membership entities. + */ + public function getGroupMembershipIdsByRoleNames(EntityInterface $group, array $role_names, array $states = [OgMembershipInterface::STATE_ACTIVE]); + + /** + * Returns the memberships of the given group filtered by role name. + * + * @param \Drupal\Core\Entity\EntityInterface $group + * The group entity for which to return the memberships. + * @param array $role_names + * An array of role names to filter by. In order to retrieve a list of all + * memberships, pass `[OgRoleInterface::AUTHENTICATED]`. + * @param array $states + * (optional) Array with the states to return. Defaults to only returning + * active memberships. In order to retrieve all memberships regardless of + * state, pass `OgMembershipInterface::ALL_STATES`. + * + * @return \Drupal\Core\Entity\EntityInterface[] + * The membership entities. + */ + public function getGroupMembershipsByRoleNames(EntityInterface $group, array $role_names, array $states = [OgMembershipInterface::STATE_ACTIVE]); + /** * Creates an OG membership. * @@ -238,4 +291,19 @@ public function isMemberBlocked(EntityInterface $group, AccountInterface $user); */ public function reset(); + /** + * Returns all users that are active members of the group. + * + * @param \Drupal\Core\Entity\EntityInterface $group + * The group entity. + * @param array $states + * The member states. + * @param int $range + * The number of members to get. + * + * @return array + * Group members + */ + public function getGroupMemberships(EntityInterface $group, array $states = [OgMembershipInterface::STATE_ACTIVE], $range = NULL); + } diff --git a/src/Og.php b/src/Og.php index 1aa2361e5..da330c192 100644 --- a/src/Og.php +++ b/src/Og.php @@ -133,9 +133,11 @@ public static function createField($plugin_id, $entity_type, $bundle, array $set * @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. + * (optional) Array with the states to return. Defaults to only returning + * active memberships. In order to retrieve all memberships regardless of + * state, pass `OgMembershipInterface::ALL_STATES`. * - * @return \Drupal\og\Entity\OgMembership[] + * @return \Drupal\og\OgMembershipInterface[] * An array of OgMembership entities, keyed by ID. */ public static function getMemberships(AccountInterface $user, array $states = [OgMembershipInterface::STATE_ACTIVE]) { @@ -152,9 +154,11 @@ public static function getMemberships(AccountInterface $user, array $states = [O * @param \Drupal\Core\Session\AccountInterface $user * The user to get the membership for. * @param array $states - * (optional) Array with the state to return. Defaults to active. + * (optional) Array with the states to return. Defaults to only returning + * active memberships. In order to retrieve all memberships regardless of + * state, pass `OgMembershipInterface::ALL_STATES`. * - * @return \Drupal\og\Entity\OgMembership|null + * @return \Drupal\og\OgMembershipInterface|null * The OgMembership entity. NULL will be returned if no membership is * available that matches the passed in $states. */ @@ -164,6 +168,23 @@ public static function getMembership(EntityInterface $group, AccountInterface $u return $membership_manager->getMembership($group, $user, $states); } + /** + * Returns the group memberships for a given group. + * + * @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. + * + * @return \Drupal\og\OgMembershipInterface[] + * An array of OgMembership entities, keyed by ID. + */ + public static function getGroupMemberships(EntityInterface $group, array $states = [OgMembershipInterface::STATE_ACTIVE]) { + /** @var \Drupal\og\MembershipManagerInterface $membership_manager */ + $membership_manager = \Drupal::service('og.membership_manager'); + return $membership_manager->getGroupMemberships($group, $states); + } + /** * Creates an OG membership. * @@ -333,6 +354,8 @@ public static function invalidateCache() { static::$cache = []; // Invalidate the entity property cache. + // @todo We should not clear the entity type and field definition caches. + // @see https://github.com/Gizra/og/issues/219 \Drupal::entityTypeManager()->clearCachedDefinitions(); \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); diff --git a/src/OgMembershipAccessControlHandler.php b/src/OgMembershipAccessControlHandler.php new file mode 100644 index 000000000..d9b352dc1 --- /dev/null +++ b/src/OgMembershipAccessControlHandler.php @@ -0,0 +1,155 @@ +entityTypeId = $entity_type->id(); + $this->entityType = $entity_type; + $this->ogAccess = $og_access; + } + + /** + * {@inheritdoc} + */ + public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) { + return new static( + $entity_type, + $container->get('og.access') + ); + } + + /** + * {@inheritdoc} + */ + protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) { + $group = $entity->getGroup(); + + // Do not allow deleting the group owner's membership. + if (($operation === 'delete') && ($group instanceof EntityOwnerInterface) && ($group->getOwnerId() == $entity->getOwner()->id())) { + return AccessResult::forbidden(); + } + + // If the user has permission to administer all groups, allow access. + if ($account->hasPermission('administer group')) { + return AccessResult::allowed(); + } + + $permissions = [OgAccess::ADMINISTER_GROUP_PERMISSION, 'manager members']; + foreach ($permissions as $permission) { + $result = $this->ogAccess->userAccess($group, $permission, $account); + if ($result->isAllowed()) { + return $result; + } + } + + return AccessResult::neutral(); + + } + + /** + * {@inheritdoc} + */ + public function createAccess($entity_bundle = NULL, AccountInterface $account = NULL, array $context = [], $return_as_object = FALSE) { + $account = $this->prepareUser($account); + $context += [ + 'entity_type_id' => $this->entityTypeId, + 'langcode' => LanguageInterface::LANGCODE_DEFAULT, + ]; + + $cid = 'create:' . $context['group']->getEntityTypeId() . ':' . $context['group']->id(); + if ($entity_bundle) { + $cid .= ':' . $entity_bundle; + } + + if (($access = $this->getCache($cid, 'create', $context['langcode'], $account)) !== NULL) { + // Cache hit, no work necessary. + return $return_as_object ? $access : $access->isAllowed(); + } + + // Invoke hook_entity_create_access() and hook_ENTITY_TYPE_create_access(). + // Hook results take precedence over overridden implementations of + // EntityAccessControlHandler::checkCreateAccess(). Entities that have + // checks that need to be done before the hook is invoked should do so by + // overriding this method. + // We grant access to the entity if both of these conditions are met: + // - No modules say to deny access. + // - At least one module says to grant access. + $args = [$account, $context, $entity_bundle]; + $access = array_merge( + $this->moduleHandler()->invokeAll('entity_create_access', $args), + $this->moduleHandler()->invokeAll($this->entityTypeId . '_create_access', $args) + ); + + $return = $this->processAccessHookResults($access); + + // Also execute the default access check except when the access result is + // already forbidden, as in that case, it can not be anything else. + if (!$return->isForbidden()) { + $return = $return->orIf($this->checkCreateAccess($account, $context, $entity_bundle)); + } + $result = $this->setCache($return, $cid, 'create', $context['langcode'], $account); + return $return_as_object ? $result : $result->isAllowed(); + } + + /** + * {@inheritdoc} + */ + protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { + // If the user has permission to administer all groups, allow access. + if ($account->hasPermission('administer group')) { + return AccessResult::allowed(); + } + + $group = $context['group']; + + // If we don't have a group, we can't really determine access other than + // checking global account permissions. + if ($group === NULL) { + return AccessResult::neutral(); + } + + $permissions = [ + OgAccess::ADMINISTER_GROUP_PERMISSION, + 'add user', + 'manager members', + ]; + foreach ($permissions as $permission) { + $result = $this->ogAccess->userAccess($group, $permission, $account); + if ($result->isAllowed()) { + return $result; + } + } + + return AccessResult::neutral(); + } + +} diff --git a/src/OgMembershipInterface.php b/src/OgMembershipInterface.php index cecd1808b..2f6a4911f 100644 --- a/src/OgMembershipInterface.php +++ b/src/OgMembershipInterface.php @@ -38,6 +38,15 @@ interface OgMembershipInterface extends ContentEntityInterface, EntityOwnerInter */ const STATE_BLOCKED = 'blocked'; + /** + * An array containing all possible group membership states. + */ + const ALL_STATES = [ + self::STATE_ACTIVE, + self::STATE_PENDING, + self::STATE_BLOCKED, + ]; + /** * The default group membership type that is the bundle of group membership. */ diff --git a/src/OgMembershipTypeListBuilder.php b/src/OgMembershipTypeListBuilder.php new file mode 100644 index 000000000..47f992ab4 --- /dev/null +++ b/src/OgMembershipTypeListBuilder.php @@ -0,0 +1,47 @@ + $entity->label(), + 'class' => ['menu-label'], + ]; + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function getDefaultOperations(EntityInterface $entity) { + $operations = parent::getDefaultOperations($entity); + // Place the edit operation after the operations added by field_ui.module + // which have the weights 15, 20, 25. + if (isset($operations['edit'])) { + $operations['edit']['weight'] = 30; + } + return $operations; + } + +} diff --git a/src/Plugin/Action/AddSingleOgMembershipRole.php b/src/Plugin/Action/AddSingleOgMembershipRole.php index 0283ca106..67dc79045 100644 --- a/src/Plugin/Action/AddSingleOgMembershipRole.php +++ b/src/Plugin/Action/AddSingleOgMembershipRole.php @@ -26,7 +26,7 @@ public function execute(OgMembership $membership = NULL) { $role_name = $this->configuration['role_name']; $role_id = implode('-', [ $membership->getGroupEntityType(), - $membership->getGroup()->bundle(), + $membership->getGroupBundle(), $role_name, ]); // Only add the role if it is valid and doesn't exist yet. diff --git a/src/Plugin/Action/RemoveSingleOgMembershipRole.php b/src/Plugin/Action/RemoveSingleOgMembershipRole.php index c269bc3e2..53c70a6c3 100644 --- a/src/Plugin/Action/RemoveSingleOgMembershipRole.php +++ b/src/Plugin/Action/RemoveSingleOgMembershipRole.php @@ -25,7 +25,7 @@ public function execute(OgMembership $membership = NULL) { $role_name = $this->configuration['role_name']; $role_id = implode('-', [ $membership->getGroupEntityType(), - $membership->getGroup()->bundle(), + $membership->getGroupBundle(), $role_name, ]); // Skip removing the role from the membership if it doesn't have it. diff --git a/src/Plugin/Condition/GroupType.php b/src/Plugin/Condition/GroupType.php index f6a3b64bd..a80f9a982 100644 --- a/src/Plugin/Condition/GroupType.php +++ b/src/Plugin/Condition/GroupType.php @@ -16,7 +16,7 @@ * @Condition( * id = "og_group_type", * label = @Translation("Group type"), - * context = { + * context_definitions = { * "og" = @ContextDefinition("entity", label = @Translation("Group")) * } * ) diff --git a/src/Plugin/Derivative/OgActionLink.php b/src/Plugin/Derivative/OgActionLink.php new file mode 100644 index 000000000..cd2ca2f73 --- /dev/null +++ b/src/Plugin/Derivative/OgActionLink.php @@ -0,0 +1,84 @@ +groupTypeManager = $group_type_manager; + $this->routeProvider = $route_provider; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, $base_plugin_id) { + return new static( + $container->get('og.group_type_manager'), + $container->get('router.route_provider') + ); + } + + /** + * {@inheritdoc} + */ + public function getDerivativeDefinitions($base_plugin_definition) { + $derivatives = []; + + foreach (array_keys($this->groupTypeManager->getGroupMap()) as $entity_type_id) { + $route_name = "entity.$entity_type_id.og_admin_routes.add_membership_page"; + + if (!$this->routeProvider->getRoutesByNames([$route_name])) { + // Route not found. + continue; + } + + $derivatives["og_membership.$entity_type_id.add"] = [ + 'route_name' => $route_name, + 'title' => $this->t('Add a member'), + 'appears_on' => ["entity.$entity_type_id.og_admin_routes.members"], + ]; + } + + foreach ($derivatives as &$entry) { + $entry += $base_plugin_definition; + } + + return $derivatives; + } + +} diff --git a/src/Plugin/Derivative/OgLocalTask.php b/src/Plugin/Derivative/OgLocalTask.php index c861065c4..dd1019f80 100644 --- a/src/Plugin/Derivative/OgLocalTask.php +++ b/src/Plugin/Derivative/OgLocalTask.php @@ -28,7 +28,7 @@ class OgLocalTask extends DeriverBase implements ContainerDeriverInterface { * * @var \Drupal\Core\Routing\RouteProvider */ - protected $routProvider; + protected $routeProvider; /** * Creates an OgLocalTask object. @@ -40,7 +40,7 @@ class OgLocalTask extends DeriverBase implements ContainerDeriverInterface { */ public function __construct(GroupTypeManagerInterface $group_type_manager, RouteProvider $route_provider) { $this->groupTypeManager = $group_type_manager; - $this->routProvider = $route_provider; + $this->routeProvider = $route_provider; } /** @@ -62,7 +62,7 @@ public function getDerivativeDefinitions($base_plugin_definition) { foreach (array_keys($this->groupTypeManager->getGroupMap()) as $entity_type_id) { $route_name = "entity.$entity_type_id.og_admin_routes"; - if (!$this->routProvider->getRoutesByNames([$route_name])) { + if (!$this->routeProvider->getRoutesByNames([$route_name])) { // Route not found. continue; } diff --git a/src/Plugin/EntityReferenceSelection/OgRoleSelection.php b/src/Plugin/EntityReferenceSelection/OgRoleSelection.php new file mode 100644 index 000000000..c6f2a52f8 --- /dev/null +++ b/src/Plugin/EntityReferenceSelection/OgRoleSelection.php @@ -0,0 +1,64 @@ + 'og_role', + ]; + return \Drupal::service('plugin.manager.entity_reference_selection')->getInstance($options); + } + + /** + * {@inheritdoc} + */ + protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') { + $query = parent::buildEntityQuery($match, $match_operator); + + // @todo implement an easier, more consistent way to get the group type. At + // the moment, this works either for checkboxes or OG Autocomplete widget + // types on entities that have a getGroup() method. It also does not work + // properly every time; for example during validation. + $group = NULL; + if (isset($this->configuration['entity'])) { + $entity = $this->configuration['entity']; + $group = is_callable([$entity, 'getGroup']) ? $entity->getGroup() : NULL; + } + + if (isset($this->configuration['handler_settings']['group'])) { + $group = $this->configuration['handler_settings']['group']; + } + + if ($group === NULL) { + return $query; + } + + $query->condition('group_type', $group->getEntityTypeId(), '='); + $query->condition('group_bundle', $group->bundle(), '='); + $query->condition($query->orConditionGroup() + ->condition('role_type', NULL, 'IS NULL') + ->condition('role_type', 'required', '<>')); + return $query; + } + +} diff --git a/src/Plugin/EntityReferenceSelection/OgUserSelection.php b/src/Plugin/EntityReferenceSelection/OgUserSelection.php new file mode 100644 index 000000000..e66edd5d6 --- /dev/null +++ b/src/Plugin/EntityReferenceSelection/OgUserSelection.php @@ -0,0 +1,194 @@ +connection = $connection; + $this->userStorage = $entity_manager->getStorage('user'); + $this->membershipManager = $membership_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity.manager'), + $container->get('module_handler'), + $container->get('current_user'), + $container->get('database'), + $container->get('og.membership_manager') + ); + } + + /** + * Get the selection handler of the field. + * + * @return Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection + * Returns the selection handler. + */ + public function getSelectionHandler() { + $options = [ + 'target_type' => 'user', + ]; + return \Drupal::service('plugin.manager.entity_reference_selection')->getInstance($options); + } + + /** + * {@inheritdoc} + */ + protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') { + $query = parent::buildEntityQuery($match, $match_operator); + + // Anon can't be a group member. + $query->condition('uid', 0, '<>'); + + // The user entity doesn't have a label column. + if (isset($match)) { + $query->condition('name', $match, $match_operator); + } + + // Adding the permission check is sadly insufficient for users: core + // requires us to also know about the concept of 'blocked' and 'active'. + if (!$this->currentUser->hasPermission('administer users')) { + $query->condition('status', 1); + } + + return $query; + } + + /** + * {@inheritdoc} + */ + public function entityQueryAlter(SelectInterface $query) { + // Exclude users who are already in the current group. + // This has to be done on the SQL query rather than the entity query, + // because a reverse relationship to the OG membership entity is needed. + // @todo implement an easier, more consistent way to get the group type. At + // the moment, this works either for checkboxes or OG Autocomplete widget + // types on entities that have a getGroup() method. It also does not work + // properly every time; for example during validation. + $group = NULL; + if (isset($this->configuration['entity'])) { + $entity = $this->configuration['entity']; + $group = is_callable([$entity, 'getGroup']) ? $entity->getGroup() : NULL; + } + + if (isset($this->configuration['handler_settings']['group'])) { + $group = $this->configuration['handler_settings']['group']; + } + + if ($group === NULL) { + return $query; + } + + // Left join to the OG membership base table. + $query->leftJoin('og_membership', 'ogm', "base_table.uid = ogm.uid AND ogm.entity_type = :entity_type AND ogm.entity_id = :entity_id", [ + ':entity_type' => $group->getEntityTypeId(), + ':entity_id' => $group->id(), + ]); + + // Exclude any users who are in the current group. + $query->isNull('ogm.id'); + } + + /** + * {@inheritdoc} + */ + public function createNewEntity($entity_type_id, $bundle, $label, $uid) { + $user = parent::createNewEntity($entity_type_id, $bundle, $label, $uid); + + // In order to create a referenceable user, it needs to be active. + if (!$this->currentUser->hasPermission('administer users')) { + /** @var \Drupal\user\UserInterface $user */ + $user->activate(); + } + + return $user; + } + + /** + * {@inheritdoc} + */ + public function validateReferenceableNewEntities(array $entities) { + $entities = parent::validateReferenceableNewEntities($entities); + // Mirror the conditions checked in buildEntityQuery(). + if (!$this->currentUser->hasPermission('administer users')) { + $entities = array_filter($entities, function ($user) { + /** @var \Drupal\user\UserInterface $user */ + return $user->isActive(); + }); + } + return $entities; + } + +} diff --git a/src/Plugin/Field/FieldFormatter/GroupSubscribeFormatter.php b/src/Plugin/Field/FieldFormatter/GroupSubscribeFormatter.php index ab3431c3d..ca19e00d4 100644 --- a/src/Plugin/Field/FieldFormatter/GroupSubscribeFormatter.php +++ b/src/Plugin/Field/FieldFormatter/GroupSubscribeFormatter.php @@ -141,6 +141,7 @@ public function viewElements(FieldItemListInterface $items, $langcode) { $parameters = [ 'entity_type_id' => $group->getEntityTypeId(), 'group' => $group->id(), + 'og_membership_type' => OgMembershipInterface::TYPE_DEFAULT, ]; $url = Url::fromRoute('og.subscribe', $parameters); diff --git a/src/Plugin/Field/FieldWidget/OgAutocomplete.php b/src/Plugin/Field/FieldWidget/OgAutocomplete.php new file mode 100644 index 000000000..0a2aa4042 --- /dev/null +++ b/src/Plugin/Field/FieldWidget/OgAutocomplete.php @@ -0,0 +1,48 @@ +getEntity(); + if (!is_callable([$entity, 'getGroup'])) { + return $element; + } + + $element['target_id']['#type'] = 'og_autocomplete'; + $element['target_id']['#og_group'] = $entity->getGroup(); + + return $element; + } + + /** + * {@inheritdoc} + */ + public function errorElement(array $element, ConstraintViolationInterface $error, array $form, FormStateInterface $form_state) { + return $element; + } + +} diff --git a/src/Plugin/Validation/Constraint/UniqueOgMembershipConstraint.php b/src/Plugin/Validation/Constraint/UniqueOgMembershipConstraint.php new file mode 100644 index 000000000..905bc6a28 --- /dev/null +++ b/src/Plugin/Validation/Constraint/UniqueOgMembershipConstraint.php @@ -0,0 +1,26 @@ +getEntity(); + + // Only applicable to new memberships. + if (!$entity->isNew()) { + return; + } + + // The default entity reference constraint adds a violation in this case. + $value = $value->getValue(); + if (!isset($value[0]) || !isset($value[0]['target_id'])) { + return; + } + + $new_member_uid = $value[0]['target_id']; + + $query = \Drupal::service('entity_type.manager') + ->getStorage('og_membership') + ->getQuery() + ->condition('entity_type', $entity->getGroupEntityType()) + ->condition('entity_id', $entity->getGroupId()) + ->condition('uid', $new_member_uid); + $membership_ids = $query->execute(); + + if ($membership_ids) { + $user = \Drupal::service('entity_type.manager')->getStorage('user')->load($new_member_uid); + $this->context->addViolation($constraint->NotUniqueMembership, ['%user' => $user->getDisplayName()]); + return; + } + } + +} diff --git a/src/Plugin/Validation/Constraint/ValidOgRoleConstraint.php b/src/Plugin/Validation/Constraint/ValidOgRoleConstraint.php new file mode 100644 index 000000000..6fdbc8bdf --- /dev/null +++ b/src/Plugin/Validation/Constraint/ValidOgRoleConstraint.php @@ -0,0 +1,24 @@ +getEntity(); + if (!$entity) { + // Entity with that entity ID does not exists. This could happen if a + // stale entity is passed for validation. + return; + } + + $group_type = $entity->getGroup()->getEntityTypeId(); + $group_bundle = $entity->getGroup()->bundle(); + + foreach ($value->referencedEntities() as $og_role) { + if ($og_role->getGroupType() !== $group_type || $og_role->getGroupBundle() !== $group_bundle) { + $this->context->addViolation($constraint->NotValidRole); + } + } + + } + +} diff --git a/tests/src/Functional/GroupSubscribeFormatterTest.php b/tests/src/Functional/GroupSubscribeFormatterTest.php index 04cccc0ca..11a092880 100644 --- a/tests/src/Functional/GroupSubscribeFormatterTest.php +++ b/tests/src/Functional/GroupSubscribeFormatterTest.php @@ -19,7 +19,7 @@ class GroupSubscribeFormatterTest extends BrowserTestBase { /** * {@inheritdoc} */ - public static $modules = ['node', 'og']; + public static $modules = ['node', 'og', 'options']; /** * Test entity group. diff --git a/tests/src/Functional/OgComplexWidgetTest.php b/tests/src/Functional/OgComplexWidgetTest.php index 1d54bc9d8..c8b565f15 100644 --- a/tests/src/Functional/OgComplexWidgetTest.php +++ b/tests/src/Functional/OgComplexWidgetTest.php @@ -7,9 +7,9 @@ use Drupal\node\Entity\Node; use Drupal\og\Og; use Drupal\og\OgGroupAudienceHelperInterface; -use Drupal\simpletest\BrowserTestBase; -use Drupal\simpletest\ContentTypeCreationTrait; -use Drupal\simpletest\NodeCreationTrait; +use Drupal\Tests\BrowserTestBase; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; +use Drupal\Tests\node\Traits\NodeCreationTrait; /** * Tests the complex widget. @@ -88,7 +88,6 @@ public function testFields($field, $field_name) { $this->assertSession()->statusCodeEquals(200); // Retrieve the post that was created from the database. - /** @var QueryInterface $query */ $query = $this->container->get('entity_type.manager')->getStorage('node')->getQuery(); $result = $query ->condition('type', 'post') @@ -97,7 +96,7 @@ public function testFields($field, $field_name) { ->execute(); $post_nid = reset($result); - /** @var \Drupal\node\Entity\NodeInterface $post */ + /** @var \Drupal\node\NodeInterface $post */ $post = Node::load($post_nid); // Check that the post references the group correctly. diff --git a/tests/src/Kernel/Access/AccessByOgMembershipTest.php b/tests/src/Kernel/Access/AccessByOgMembershipTest.php index 37c50ead3..9949d611f 100644 --- a/tests/src/Kernel/Access/AccessByOgMembershipTest.php +++ b/tests/src/Kernel/Access/AccessByOgMembershipTest.php @@ -36,6 +36,7 @@ class AccessByOgMembershipTest extends KernelTestBase { 'field', 'node', 'og', + 'options', 'system', 'user', ]; @@ -72,7 +73,7 @@ public function setUp() { $this->installEntitySchema('node'); $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); // Create a user role for a standard authenticated user. $role = Role::create([ diff --git a/tests/src/Kernel/Access/OgAccessHookTest.php b/tests/src/Kernel/Access/OgAccessHookTest.php index 275770da7..ad35f4cc4 100644 --- a/tests/src/Kernel/Access/OgAccessHookTest.php +++ b/tests/src/Kernel/Access/OgAccessHookTest.php @@ -33,6 +33,7 @@ class OgAccessHookTest extends KernelTestBase { 'field', 'node', 'og', + 'options', 'system', 'user', ]; @@ -83,7 +84,7 @@ public function setUp() { $this->installEntitySchema('node'); $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); // Create two roles: one for normal users, and one for administrators. foreach (['authenticated', 'administrator'] as $role_id) { diff --git a/tests/src/Kernel/Access/OgEntityAccessTest.php b/tests/src/Kernel/Access/OgEntityAccessTest.php index b26be94a0..5446bedec 100644 --- a/tests/src/Kernel/Access/OgEntityAccessTest.php +++ b/tests/src/Kernel/Access/OgEntityAccessTest.php @@ -25,6 +25,7 @@ class OgEntityAccessTest extends KernelTestBase { 'user', 'field', 'og', + 'options', 'entity_test', ]; @@ -143,7 +144,7 @@ protected function setUp() { $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); $this->installEntitySchema('entity_test'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); $this->groupBundle = mb_strtolower($this->randomMachineName()); diff --git a/tests/src/Kernel/Access/OgGroupContentOperationAccessTest.php b/tests/src/Kernel/Access/OgGroupContentOperationAccessTest.php index 5a8573817..fd5cb4659 100644 --- a/tests/src/Kernel/Access/OgGroupContentOperationAccessTest.php +++ b/tests/src/Kernel/Access/OgGroupContentOperationAccessTest.php @@ -32,6 +32,7 @@ class OgGroupContentOperationAccessTest extends KernelTestBase { 'field', 'node', 'og', + 'options', 'system', 'user', ]; @@ -92,7 +93,7 @@ protected function setUp() { $this->installEntitySchema('node'); $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); $this->entityTypeManager = $this->container->get('entity_type.manager'); @@ -174,7 +175,7 @@ protected function setUp() { } // Create a 'blocked' user. This user is identical to the normal - // 'authenticated' member, except that she has the 'blocked' state. + // 'authenticated' member, except that they have the 'blocked' state. $this->users['blocked'] = User::create(['name' => $this->randomString()]); $this->users['blocked']->save(); $this->createOgMembership($this->group, $this->users['blocked'], NULL, OgMembershipInterface::STATE_BLOCKED); @@ -231,10 +232,12 @@ protected function setUp() { case 'comment': $values = [ + 'field_name' => $this->randomString(), 'subject' => 'subscribe', 'comment_type' => $bundle_id, 'entity_id' => $this->group->id(), 'entity_type' => 'entity_test', + 'field_name' => 'an_imaginary_field', OgGroupAudienceHelperInterface::DEFAULT_FIELD => [['target_id' => $this->group->id()]], ]; break; diff --git a/tests/src/Kernel/Action/ActionTestBase.php b/tests/src/Kernel/Action/ActionTestBase.php index adc59c38a..2ecf08f9a 100644 --- a/tests/src/Kernel/Action/ActionTestBase.php +++ b/tests/src/Kernel/Action/ActionTestBase.php @@ -31,7 +31,7 @@ abstract class ActionTestBase extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['node', 'og', 'system', 'user']; + public static $modules = ['node', 'og', 'system', 'user', 'options']; /** * An array of test users. @@ -84,7 +84,7 @@ public function setUp() { $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); $this->installEntitySchema('node'); - $this->installSchema('system', ['queue', 'sequences']); + $this->installSchema('system', ['sequences']); $this->membershipManager = $this->container->get('og.membership_manager'); $this->groupTypeManager = $this->container->get('og.group_type_manager'); diff --git a/tests/src/Kernel/Console/DrupalConsoleAddFieldTest.php b/tests/src/Kernel/Console/DrupalConsoleAddFieldTest.php index 99ae6e631..ba046c9c3 100644 --- a/tests/src/Kernel/Console/DrupalConsoleAddFieldTest.php +++ b/tests/src/Kernel/Console/DrupalConsoleAddFieldTest.php @@ -24,6 +24,7 @@ class DrupalConsoleAddFieldTest extends KernelTestBase { 'field', 'node', 'og', + 'options', 'system', 'user', ]; @@ -37,7 +38,7 @@ public function setUp() { $this->installConfig(['og']); $this->installEntitySchema('node'); $this->installEntitySchema('og_membership'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); NodeType::create([ 'name' => $this->randomString(), diff --git a/tests/src/Kernel/DefaultRoleEventIntegrationTest.php b/tests/src/Kernel/DefaultRoleEventIntegrationTest.php index 5f316e0e8..545de2ebf 100644 --- a/tests/src/Kernel/DefaultRoleEventIntegrationTest.php +++ b/tests/src/Kernel/DefaultRoleEventIntegrationTest.php @@ -18,7 +18,14 @@ class DefaultRoleEventIntegrationTest extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['entity_test', 'og', 'system', 'user', 'field']; + public static $modules = [ + 'entity_test', + 'og', + 'system', + 'user', + 'field', + 'options', + ]; /** * The Symfony event dispatcher. diff --git a/tests/src/Kernel/Entity/CacheInvalidationOnGroupChangeTest.php b/tests/src/Kernel/Entity/CacheInvalidationOnGroupChangeTest.php index 2546c5f97..307c4a733 100644 --- a/tests/src/Kernel/Entity/CacheInvalidationOnGroupChangeTest.php +++ b/tests/src/Kernel/Entity/CacheInvalidationOnGroupChangeTest.php @@ -24,6 +24,7 @@ class CacheInvalidationOnGroupChangeTest extends KernelTestBase { 'og', 'system', 'user', + 'options', ]; /** diff --git a/tests/src/Kernel/Entity/EntityCreateAccessTest.php b/tests/src/Kernel/Entity/EntityCreateAccessTest.php index 0c919b11a..25bf9caf5 100644 --- a/tests/src/Kernel/Entity/EntityCreateAccessTest.php +++ b/tests/src/Kernel/Entity/EntityCreateAccessTest.php @@ -7,8 +7,8 @@ use Drupal\node\Entity\NodeType; use Drupal\og\Og; use Drupal\og\OgGroupAudienceHelperInterface; -use Drupal\simpletest\ContentTypeCreationTrait; -use Drupal\simpletest\NodeCreationTrait; +use Drupal\Tests\node\Traits\ContentTypeCreationTrait; +use Drupal\Tests\node\Traits\NodeCreationTrait; use Drupal\user\Entity\Role; use Drupal\user\Entity\User; @@ -31,6 +31,7 @@ class EntityCreateAccessTest extends KernelTestBase { 'field', 'node', 'og', + 'options', 'system', 'user', ]; @@ -59,7 +60,7 @@ public function setUp() { $this->installEntitySchema('node'); $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); // Create a "group" node type and turn it into a group type. $this->groupType = NodeType::create([ diff --git a/tests/src/Kernel/Entity/GetBundleByBundleTest.php b/tests/src/Kernel/Entity/GetBundleByBundleTest.php index 24ed030ed..27a6b7cd2 100644 --- a/tests/src/Kernel/Entity/GetBundleByBundleTest.php +++ b/tests/src/Kernel/Entity/GetBundleByBundleTest.php @@ -24,6 +24,7 @@ class GetBundleByBundleTest extends KernelTestBase { 'field', 'node', 'og', + 'options', 'system', 'user', ]; @@ -60,7 +61,7 @@ protected function setUp() { $this->installEntitySchema('node'); $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); $this->groupTypeManager = $this->container->get('og.group_type_manager'); diff --git a/tests/src/Kernel/Entity/GetGroupContentTest.php b/tests/src/Kernel/Entity/GetGroupContentTest.php index c360b7440..e4f63652a 100644 --- a/tests/src/Kernel/Entity/GetGroupContentTest.php +++ b/tests/src/Kernel/Entity/GetGroupContentTest.php @@ -26,6 +26,7 @@ class GetGroupContentTest extends KernelTestBase { 'field', 'node', 'og', + 'options', 'system', 'user', ]; @@ -55,7 +56,7 @@ protected function setUp() { $this->installEntitySchema('node'); $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); /** @var \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager */ $entity_type_manager = $this->container->get('entity_type.manager'); diff --git a/tests/src/Kernel/Entity/GetMembershipsTest.php b/tests/src/Kernel/Entity/GetMembershipsTest.php index b8e813337..0c75604f1 100644 --- a/tests/src/Kernel/Entity/GetMembershipsTest.php +++ b/tests/src/Kernel/Entity/GetMembershipsTest.php @@ -28,6 +28,7 @@ class GetMembershipsTest extends KernelTestBase { 'field', 'node', 'og', + 'options', 'system', 'user', ]; @@ -63,7 +64,7 @@ protected function setUp() { $this->installEntitySchema('node'); $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); $this->installSchema('user', ['users_data']); $this->entityTypeManager = $this->container->get('entity_type.manager'); diff --git a/tests/src/Kernel/Entity/GetUserGroupsTest.php b/tests/src/Kernel/Entity/GetUserGroupsTest.php index d183fdb74..5fcd8f544 100644 --- a/tests/src/Kernel/Entity/GetUserGroupsTest.php +++ b/tests/src/Kernel/Entity/GetUserGroupsTest.php @@ -26,6 +26,7 @@ class GetUserGroupsTest extends KernelTestBase { 'user', 'field', 'og', + 'options', 'entity_test', ]; @@ -88,7 +89,7 @@ protected function setUp() { $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); $this->installEntitySchema('entity_test'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); $this->groupBundle = mb_strtolower($this->randomMachineName()); $this->groupContentBundle = mb_strtolower($this->randomMachineName()); diff --git a/tests/src/Kernel/Entity/GroupAudienceTest.php b/tests/src/Kernel/Entity/GroupAudienceTest.php index d1132d337..de907ca91 100644 --- a/tests/src/Kernel/Entity/GroupAudienceTest.php +++ b/tests/src/Kernel/Entity/GroupAudienceTest.php @@ -29,6 +29,7 @@ class GroupAudienceTest extends KernelTestBase { 'entity_test', 'field', 'og', + 'options', 'system', 'user', ]; diff --git a/tests/src/Kernel/Entity/GroupMembershipManagerTest.php b/tests/src/Kernel/Entity/GroupMembershipManagerTest.php index 705b81918..b3453ea8f 100644 --- a/tests/src/Kernel/Entity/GroupMembershipManagerTest.php +++ b/tests/src/Kernel/Entity/GroupMembershipManagerTest.php @@ -9,8 +9,12 @@ use Drupal\entity_test\Entity\EntityTest; use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; +use Drupal\og\Entity\OgRole; use Drupal\og\Og; use Drupal\og\OgGroupAudienceHelperInterface; +use Drupal\og\OgMembershipInterface; +use Drupal\og\OgRoleInterface; +use Drupal\Tests\og\Traits\OgMembershipCreationTrait; use Drupal\user\Entity\User; /** @@ -21,6 +25,8 @@ */ class GroupMembershipManagerTest extends KernelTestBase { + use OgMembershipCreationTrait; + /** * {@inheritdoc} */ @@ -29,6 +35,7 @@ class GroupMembershipManagerTest extends KernelTestBase { 'field', 'node', 'og', + 'options', 'system', 'user', ]; @@ -47,6 +54,13 @@ class GroupMembershipManagerTest extends KernelTestBase { */ protected $groupContent; + /** + * Test users. + * + * @var \Drupal\user\UserInterface[] + */ + protected $users; + /** * The entity type manager. * @@ -54,6 +68,13 @@ class GroupMembershipManagerTest extends KernelTestBase { */ protected $entityTypeManager; + /** + * The membership manager. This is the system under test. + * + * @var \Drupal\og\MembershipManagerInterface + */ + protected $membershipManager; + /** * {@inheritdoc} */ @@ -67,15 +88,17 @@ protected function setUp() { $this->installEntitySchema('node'); $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); $this->entityTypeManager = $this->container->get('entity_type.manager'); + $this->membershipManager = $this->container->get('og.membership_manager'); $this->groups = []; // Create group admin user. $group_admin = User::create(['name' => $this->randomString()]); $group_admin->save(); + $this->users[0] = $group_admin; // Create four groups of two different entity types. for ($i = 0; $i < 2; $i++) { @@ -156,10 +179,7 @@ protected function setUp() { * @dataProvider groupContentProvider */ public function testGetGroupIds($group_type_id, $group_bundle, array $expected) { - /** @var \Drupal\og\MembershipManagerInterface $membership_manager */ - $membership_manager = \Drupal::service('og.membership_manager'); - - $result = $membership_manager->getGroupIds($this->groupContent, $group_type_id, $group_bundle); + $result = $this->membershipManager->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)); @@ -181,8 +201,6 @@ public function testGetGroupIds($group_type_id, $group_bundle, array $expected) * @covers ::getGroups */ public function testStaticCache() { - /** @var \Drupal\og\MembershipManagerInterface $membership_manager */ - $membership_manager = \Drupal::service('og.membership_manager'); $bundle_rev = mb_strtolower($this->randomMachineName()); $bundle_with_bundle = mb_strtolower($this->randomMachineName()); EntityTestBundle::create(['id' => $bundle_with_bundle, 'label' => $bundle_with_bundle])->save(); @@ -222,11 +240,11 @@ public function testStaticCache() { // ensure that the next assertions are addressing the proper issue. $this->assertEquals($group_content_rev->id(), $group_content_with_bundle->id()); - $group_content_rev_group = $membership_manager->getGroups($group_content_rev); + $group_content_rev_group = $this->membershipManager->getGroups($group_content_rev); /** @var \Drupal\node\NodeInterface $group */ $group = reset($group_content_rev_group['node']); $this->assertEquals($this->groups['node'][0]->id(), $group->id()); - $group_content_with_bundle_group = $membership_manager->getGroups($group_content_with_bundle); + $group_content_with_bundle_group = $this->membershipManager->getGroups($group_content_with_bundle); $group = reset($group_content_with_bundle_group['node']); $this->assertEquals($this->groups['node'][1]->id(), $group->id()); } @@ -247,10 +265,7 @@ public function testStaticCache() { * @dataProvider groupContentProvider */ public function testGetGroups($group_type_id, $group_bundle, array $expected) { - /** @var \Drupal\og\MembershipManagerInterface $membership_manager */ - $membership_manager = \Drupal::service('og.membership_manager'); - - $result = $membership_manager->getGroups($this->groupContent, $group_type_id, $group_bundle); + $result = $this->membershipManager->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)); @@ -289,10 +304,7 @@ public function testGetGroups($group_type_id, $group_bundle, array $expected) { * @dataProvider groupContentProvider */ public function testGetGroupCount($group_type_id, $group_bundle, array $expected) { - /** @var \Drupal\og\MembershipManagerInterface $membership_manager */ - $membership_manager = \Drupal::service('og.membership_manager'); - - $result = $membership_manager->getGroupCount($this->groupContent, $group_type_id, $group_bundle); + $result = $this->membershipManager->getGroupCount($this->groupContent, $group_type_id, $group_bundle); // Check that the correct results is returned. $this->assertEquals(count($expected, COUNT_RECURSIVE) - count($expected), $result); @@ -318,4 +330,366 @@ public function groupContentProvider() { ]; } + /** + * Tests retrieval of group memberships filtered by role names. + * + * @covers ::getGroupMembershipsByRoleNames + */ + public function testGetGroupMembershipsByRoleNames() { + $retrieve_membership_owner_id = function (OgMembershipInterface $membership) { + return $membership->getOwnerId(); + }; + $this->doTestGetGroupMembershipsByRoleNames('getGroupMembershipsByRoleNames', $retrieve_membership_owner_id); + } + + /** + * Tests retrieval of group membership IDs filtered by role names. + * + * @covers ::getGroupMembershipIdsByRoleNames + */ + public function testGetGroupMembershipIdsByRoleNames() { + $membership_storage = $this->container->get('entity_type.manager')->getStorage('og_membership'); + $retrieve_membership_owner_id = function ($membership_id) use ($membership_storage) { + /** @var \Drupal\og\OgMembershipInterface $membership */ + $membership = $membership_storage->load($membership_id); + return $membership->getOwnerId(); + }; + $this->doTestGetGroupMembershipsByRoleNames('getGroupMembershipIdsByRoleNames', $retrieve_membership_owner_id); + } + + /** + * Tests retrieval of group memberships or their IDs filtered by role names. + * + * Contains the actual test logic of ::testGetGroupMembershipsByRoleNames() + * and ::testGetGroupMembershipIdsByRoleNames(). + * + * @param string $method_name + * The name of the method under test. Can be one of the following: + * - 'getGroupMembershipIdsByRoleNames' + * - 'getGroupMembershipsByRoleNames'. + * @param callable $retrieve_membership_owner_id + * A callable that will retrieve the ID of the owner of the membership or + * membership ID. + */ + protected function doTestGetGroupMembershipsByRoleNames($method_name, callable $retrieve_membership_owner_id) { + $this->createTestMemberships(); + + // Check that an exception is thrown if no role names are passed. + try { + $this->membershipManager->$method_name($this->groups['node'][0], []); + $this->fail('MembershipManager::getGroupsMembershipsByRoleNames() throws an exception when called without passing any role names.'); + } + catch (\InvalidArgumentException $e) { + // Expected result. + } + + // Define a test matrix to iterate over. We're not using a data provider + // because the large number of test cases would slow down the test too much. + // The test matrix has the following structure: + // @code + // [ + // // The machine name of the group entity type being tested. + // {entity_type_id} => [ + // // The key of the test group as created in ::setUp(). + // {group_key} => [ // + // // The roles being passed to the method. + // 'roles' => [{role_id}], + // // The membership states being passed to the method. + // 'states' => [{state_id}], + // // The memberships that should be returned by the method. + // 'expected_memberships' => [{expected_membership_id}], + // ], + // ], + // ]; + // @endcode + $matrix = [ + 'node' => [ + 0 => [ + // All memberships with all possible states. The authenticated role + // covers all memberships. + [ + 'roles' => [OgRoleInterface::AUTHENTICATED], + 'states' => OgMembershipInterface::ALL_STATES, + 'expected_memberships' => [0, 1, 4, 7], + ], + // All memberships with all possible states, by passing an empty + // states array, and passing all defined roles. + [ + 'roles' => [ + OgRoleInterface::AUTHENTICATED, + OgRoleInterface::ADMINISTRATOR, + 'moderator', + ], + 'states' => [], + 'expected_memberships' => [0, 1, 4, 7], + ], + // Pending members. + [ + 'roles' => [OgRoleInterface::AUTHENTICATED], + 'states' => [OgMembershipInterface::STATE_PENDING], + 'expected_memberships' => [4], + ], + ], + 1 => [ + // All administrators. + [ + 'roles' => [OgRoleInterface::ADMINISTRATOR], + 'states' => [], + 'expected_memberships' => [2, 6], + ], + // Pending administrators. + [ + 'roles' => [OgRoleInterface::ADMINISTRATOR], + 'states' => [OgMembershipInterface::STATE_PENDING], + 'expected_memberships' => [2], + ], + // Blocked administrators. There are none. + [ + 'roles' => [OgRoleInterface::ADMINISTRATOR], + 'states' => [OgMembershipInterface::STATE_BLOCKED], + 'expected_memberships' => [], + ], + // Pending and blocked administrators and moderators. Should be the + // same result as the pending administrators, since there are no + // moderators or blocked users. + [ + 'roles' => [OgRoleInterface::ADMINISTRATOR, 'moderator'], + 'states' => [ + OgMembershipInterface::STATE_BLOCKED, + OgMembershipInterface::STATE_PENDING, + ], + 'expected_memberships' => [2], + ], + // Switch the order of the arguments, this should not affect the + // result. + [ + 'roles' => ['moderator', OgRoleInterface::ADMINISTRATOR], + 'states' => [ + OgMembershipInterface::STATE_PENDING, + OgMembershipInterface::STATE_BLOCKED, + ], + 'expected_memberships' => [2], + ], + // There are no pending or blocked moderators. + [ + 'roles' => ['moderator'], + 'states' => [ + OgMembershipInterface::STATE_BLOCKED, + OgMembershipInterface::STATE_PENDING, + ], + 'expected_memberships' => [], + ], + ], + ], + 'entity_test' => [ + 0 => [ + // The first test entity group doesn't have any moderators or admins. + // Check that duplicated array values doesn't affect the result. + [ + 'roles' => [ + 'moderator', + OgRoleInterface::ADMINISTRATOR, + 'moderator', + 'moderator', + OgRoleInterface::ADMINISTRATOR, + ], + 'states' => [ + OgMembershipInterface::STATE_ACTIVE, + OgMembershipInterface::STATE_BLOCKED, + OgMembershipInterface::STATE_PENDING, + OgMembershipInterface::STATE_PENDING, + OgMembershipInterface::STATE_BLOCKED, + OgMembershipInterface::STATE_ACTIVE, + ], + 'expected_memberships' => [], + ], + ], + // Check active members. + [ + 'roles' => [ + OgRoleInterface::AUTHENTICATED, + ], + 'states' => [ + OgMembershipInterface::STATE_ACTIVE, + ], + 'expected_memberships' => [0, 3], + ], + 1 => [ + // There are two blocked users in the second test entity group. + [ + 'roles' => [ + OgRoleInterface::AUTHENTICATED, + OgRoleInterface::ADMINISTRATOR, + 'moderator', + ], + 'states' => [ + OgMembershipInterface::STATE_BLOCKED, + ], + 'expected_memberships' => [4, 7], + ], + // There is one blocked moderator. + [ + 'roles' => [ + OgRoleInterface::ADMINISTRATOR, + 'moderator', + ], + 'states' => [ + OgMembershipInterface::STATE_BLOCKED, + ], + 'expected_memberships' => [4], + ], + ], + ], + ]; + + foreach ($matrix as $entity_type_id => $groups) { + foreach ($groups as $group_key => $test_cases) { + foreach ($test_cases as $test_case) { + $group = $this->groups[$entity_type_id][$group_key]; + $role_names = $test_case['roles']; + $states = $test_case['states']; + $expected_memberships = $test_case['expected_memberships']; + + $actual_memberships = $this->membershipManager->$method_name($group, $role_names, $states); + $this->assertSameSize($expected_memberships, $actual_memberships); + + foreach ($expected_memberships as $expected_membership_key) { + $expected_user_id = $this->users[$expected_membership_key]->id(); + foreach ($actual_memberships as $actual_membership) { + if ($retrieve_membership_owner_id($actual_membership) == $expected_user_id) { + // Match found. + continue 2; + } + } + // The expected membership was not returned. Fail the test. + $this->fail("The membership for user $expected_membership_key is present in the result."); + } + } + } + } + } + + /** + * Creates a number of users that are members of the test groups. + */ + protected function createTestMemberships() { + // Create a 'moderator' role in each of the test group types. + foreach (['node', 'entity_test'] as $entity_type_id) { + for ($i = 0; $i < 2; $i++) { + $bundle = "${entity_type_id}_$i"; + $og_role = OgRole::create(); + $og_role + ->setName('moderator') + ->setGroupType($entity_type_id) + ->setGroupBundle($bundle) + ->save(); + } + } + + // Create test users with different membership states in the various groups. + // Note that the group admin (test user 0) is also a member of all groups. + $matrix = [ + // A user which is an active member of the first node group. + 1 => [ + 'node' => [ + 0 => [ + 'state' => OgMembershipInterface::STATE_ACTIVE, + 'roles' => [OgRoleInterface::AUTHENTICATED], + ], + ], + ], + + // A user which is a pending administrator of the second node group. + 2 => [ + 'node' => [ + 1 => [ + 'state' => OgMembershipInterface::STATE_PENDING, + 'roles' => [OgRoleInterface::ADMINISTRATOR], + ], + ], + ], + + // A user which is an active member of both test entity groups. + 3 => [ + 'entity_test' => [ + 0 => [ + 'state' => OgMembershipInterface::STATE_ACTIVE, + 'roles' => [OgRoleInterface::AUTHENTICATED], + ], + 1 => [ + 'state' => OgMembershipInterface::STATE_ACTIVE, + 'roles' => [OgRoleInterface::AUTHENTICATED], + ], + ], + ], + + // A user which is a pending member of the first node group and a blocked + // moderator in the second test entity group. + 4 => [ + 'node' => [ + 0 => [ + 'state' => OgMembershipInterface::STATE_PENDING, + 'roles' => [OgRoleInterface::AUTHENTICATED], + ], + ], + 'entity_test' => [ + 1 => [ + 'state' => OgMembershipInterface::STATE_BLOCKED, + 'roles' => ['moderator'], + ], + ], + ], + + // A user which is not subscribed to any of the groups. + 5 => [], + + // A user which is both an administrator and a moderator in the second + // node group. + 6 => [ + 'node' => [ + 1 => [ + 'state' => OgMembershipInterface::STATE_ACTIVE, + 'roles' => [OgRoleInterface::ADMINISTRATOR, 'moderator'], + ], + ], + ], + + // A troll who is banned everywhere. + 7 => [ + 'node' => [ + 0 => [ + 'state' => OgMembershipInterface::STATE_BLOCKED, + 'roles' => [OgRoleInterface::AUTHENTICATED], + ], + 1 => [ + 'state' => OgMembershipInterface::STATE_BLOCKED, + 'roles' => [OgRoleInterface::AUTHENTICATED], + ], + ], + 'entity_test' => [ + 0 => [ + 'state' => OgMembershipInterface::STATE_BLOCKED, + 'roles' => [OgRoleInterface::AUTHENTICATED], + ], + 1 => [ + 'state' => OgMembershipInterface::STATE_BLOCKED, + 'roles' => [OgRoleInterface::AUTHENTICATED], + ], + ], + ], + ]; + + foreach ($matrix as $user_key => $entity_types) { + $user = User::create(['name' => $this->randomString()]); + $user->save(); + $this->users[$user_key] = $user; + foreach ($entity_types as $entity_type_id => $groups) { + foreach ($groups as $group_key => $membership_info) { + $group = $this->groups[$entity_type_id][$group_key]; + $this->createOgMembership($group, $user, $membership_info['roles'], $membership_info['state']); + } + } + } + } + } diff --git a/tests/src/Kernel/Entity/GroupTypeTest.php b/tests/src/Kernel/Entity/GroupTypeTest.php index c3f16144e..eed80eb73 100644 --- a/tests/src/Kernel/Entity/GroupTypeTest.php +++ b/tests/src/Kernel/Entity/GroupTypeTest.php @@ -15,7 +15,7 @@ class GroupTypeTest extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['field', 'node', 'og', 'system', 'user']; + public static $modules = ['field', 'node', 'og', 'options', 'system', 'user']; /** * The group type manager. diff --git a/tests/src/Kernel/Entity/OgMembershipRoleReferenceTest.php b/tests/src/Kernel/Entity/OgMembershipRoleReferenceTest.php index 98a669fc0..66cd39034 100644 --- a/tests/src/Kernel/Entity/OgMembershipRoleReferenceTest.php +++ b/tests/src/Kernel/Entity/OgMembershipRoleReferenceTest.php @@ -20,11 +20,12 @@ class OgMembershipRoleReferenceTest extends KernelTestBase { * {@inheritdoc} */ public static $modules = [ - 'og', 'field', 'node', - 'user', + 'og', + 'options', 'system', + 'user', ]; /** @@ -59,7 +60,7 @@ protected function setUp() { $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); $this->installEntitySchema('node'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); // Create a "group" node type and turn it into a group type. $this->groupBundle = mb_strtolower($this->randomMachineName()); diff --git a/tests/src/Kernel/Entity/OgMembershipTest.php b/tests/src/Kernel/Entity/OgMembershipTest.php index e238d2528..c00bbf54c 100644 --- a/tests/src/Kernel/Entity/OgMembershipTest.php +++ b/tests/src/Kernel/Entity/OgMembershipTest.php @@ -30,6 +30,7 @@ class OgMembershipTest extends KernelTestBase { 'field', 'node', 'og', + 'options', 'system', 'user', ]; @@ -73,7 +74,7 @@ protected function setUp() { $this->installEntitySchema('entity_test'); $this->installEntitySchema('node'); $this->installEntitySchema('user'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); $this->entityTypeManager = $this->container->get('entity_type.manager'); $storage = $this->entityTypeManager->getStorage('user'); diff --git a/tests/src/Kernel/Entity/OgRoleTest.php b/tests/src/Kernel/Entity/OgRoleTest.php index 728e90444..932495d20 100644 --- a/tests/src/Kernel/Entity/OgRoleTest.php +++ b/tests/src/Kernel/Entity/OgRoleTest.php @@ -24,6 +24,7 @@ class OgRoleTest extends KernelTestBase { 'field', 'node', 'og', + 'options', 'system', 'user', ]; diff --git a/tests/src/Kernel/Entity/OgStandardReferenceItemTest.php b/tests/src/Kernel/Entity/OgStandardReferenceItemTest.php index 7e336ad35..4c40a8272 100644 --- a/tests/src/Kernel/Entity/OgStandardReferenceItemTest.php +++ b/tests/src/Kernel/Entity/OgStandardReferenceItemTest.php @@ -18,7 +18,14 @@ class OgStandardReferenceItemTest extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['user', 'entity_test', 'field', 'og', 'system']; + public static $modules = [ + 'user', + 'entity_test', + 'field', + 'og', + 'options', + 'system', + ]; protected $bundles; protected $fieldName; @@ -35,7 +42,7 @@ protected function setUp() { $this->installEntitySchema('entity_test'); $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); // Create several bundles. for ($i = 0; $i <= 2; $i++) { diff --git a/tests/src/Kernel/Entity/ReferenceStringIdTest.php b/tests/src/Kernel/Entity/ReferenceStringIdTest.php index d7575dd0e..4c2a8df94 100644 --- a/tests/src/Kernel/Entity/ReferenceStringIdTest.php +++ b/tests/src/Kernel/Entity/ReferenceStringIdTest.php @@ -17,7 +17,14 @@ class ReferenceStringIdTest extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['user', 'entity_test', 'field', 'og', 'system']; + public static $modules = [ + 'user', + 'entity_test', + 'field', + 'og', + 'options', + 'system', + ]; /** * Array of test bundles. The first is a group, the second group content. @@ -51,7 +58,7 @@ protected function setUp() { $this->installEntitySchema('entity_test_string_id'); $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); // Create two bundles, one will serve as group, the other as group content. for ($i = 0; $i < 2; $i++) { diff --git a/tests/src/Kernel/Entity/SelectionHandlerTest.php b/tests/src/Kernel/Entity/SelectionHandlerTest.php index 5e84de5d4..1c2a081fe 100644 --- a/tests/src/Kernel/Entity/SelectionHandlerTest.php +++ b/tests/src/Kernel/Entity/SelectionHandlerTest.php @@ -34,6 +34,7 @@ class SelectionHandlerTest extends KernelTestBase { 'entity_reference', 'node', 'og', + 'options', ]; /** @@ -82,7 +83,7 @@ protected function setUp() { $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); $this->installEntitySchema('node'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); // Setting up variables. $this->groupBundle = mb_strtolower($this->randomMachineName()); @@ -133,14 +134,15 @@ public function testSelectionHandler() { /** * Testing OG selection handler results. * - * We need to verify that each user get the groups he own in the normal widget - * and the other users group's in the other groups widget and vice versa. + * We need to verify that each user gets the groups they own in the normal + * widget and the other users' groups in the other groups widget and vice + * versa. */ public function testSelectionHandlerResults() { $user1_groups = $this->createGroups(2, $this->user1); $user2_groups = $this->createGroups(2, $this->user2); - // Checking that the user get the groups he mange. + // Check that users get the groups they manage. $this->setCurrentAccount($this->user1); $groups = $this->selectionHandler->getReferenceableEntities(); $this->assertEquals($user1_groups, array_keys($groups[$this->groupBundle])); diff --git a/tests/src/Kernel/EntityReference/Views/OgStandardReferenceRelationshipTest.php b/tests/src/Kernel/EntityReference/Views/OgStandardReferenceRelationshipTest.php index 03beeacfa..1a12e55d3 100644 --- a/tests/src/Kernel/EntityReference/Views/OgStandardReferenceRelationshipTest.php +++ b/tests/src/Kernel/EntityReference/Views/OgStandardReferenceRelationshipTest.php @@ -43,6 +43,7 @@ class OgStandardReferenceRelationshipTest extends ViewsKernelTestBase { 'views', 'og_standard_reference_test_views', 'og', + 'options', ]; /** diff --git a/tests/src/Kernel/Field/AudienceFieldFormatterTest.php b/tests/src/Kernel/Field/AudienceFieldFormatterTest.php index 1b95a78db..d485441e8 100644 --- a/tests/src/Kernel/Field/AudienceFieldFormatterTest.php +++ b/tests/src/Kernel/Field/AudienceFieldFormatterTest.php @@ -15,7 +15,7 @@ class AudienceFieldFormatterTest extends KernelTestBase { /** * {@inheritdoc} */ - public static $modules = ['field', 'og']; + public static $modules = ['field', 'og', 'options']; /** * Testing og_field_formatter_info_alter(). diff --git a/tests/src/Kernel/Form/GroupSubscribeFormTest.php b/tests/src/Kernel/Form/GroupSubscribeFormTest.php index 6b0fccafd..174e4005f 100644 --- a/tests/src/Kernel/Form/GroupSubscribeFormTest.php +++ b/tests/src/Kernel/Form/GroupSubscribeFormTest.php @@ -29,6 +29,7 @@ class GroupSubscribeFormTest extends KernelTestBase { 'field', 'node', 'og', + 'options', ]; /** @@ -69,7 +70,7 @@ protected function setUp() { $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); $this->installEntitySchema('node'); - $this->installSchema('system', 'sequences'); + $this->installSchema('system', ['sequences']); // Create 3 test bundles and declare them as groups. $bundle_names = []; diff --git a/tests/src/Kernel/GroupManagerSubscriptionTest.php b/tests/src/Kernel/GroupManagerSubscriptionTest.php index aa932451d..efdf4039d 100644 --- a/tests/src/Kernel/GroupManagerSubscriptionTest.php +++ b/tests/src/Kernel/GroupManagerSubscriptionTest.php @@ -26,6 +26,7 @@ class GroupManagerSubscriptionTest extends KernelTestBase { 'og_test', 'system', 'user', + 'options', ]; /** @@ -63,7 +64,7 @@ protected function setUp() { $this->installEntitySchema('node'); $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); - $this->installSchema('system', ['queue', 'sequences']); + $this->installSchema('system', ['sequences']); // Create a group type. NodeType::create([ diff --git a/tests/src/Kernel/GroupTypeConditionTest.php b/tests/src/Kernel/GroupTypeConditionTest.php index ff2c52320..7ee4361c6 100644 --- a/tests/src/Kernel/GroupTypeConditionTest.php +++ b/tests/src/Kernel/GroupTypeConditionTest.php @@ -22,6 +22,7 @@ class GroupTypeConditionTest extends KernelTestBase { 'field', 'node', 'og', + 'options', 'system', 'user', ]; @@ -61,7 +62,7 @@ protected function setUp() { $this->installEntitySchema('entity_test'); $this->installEntitySchema('node'); $this->installEntitySchema('user'); - $this->installSchema('system', ['queue', 'sequences']); + $this->installSchema('system', ['sequences']); // Create three test groups of different types. for ($i = 0; $i < 2; $i++) { diff --git a/tests/src/Kernel/OgDeleteOrphansTest.php b/tests/src/Kernel/OgDeleteOrphansTest.php index 17fa75f3c..fd80646c2 100644 --- a/tests/src/Kernel/OgDeleteOrphansTest.php +++ b/tests/src/Kernel/OgDeleteOrphansTest.php @@ -26,6 +26,7 @@ class OgDeleteOrphansTest extends KernelTestBase { 'entity_reference', 'node', 'og', + 'options', ]; /** @@ -60,8 +61,8 @@ public function setUp() { $this->installEntitySchema('og_membership'); $this->installEntitySchema('user'); $this->installEntitySchema('node'); - $this->installSchema('node', 'node_access'); - $this->installSchema('system', ['queue', 'sequences']); + $this->installSchema('node', ['node_access']); + $this->installSchema('system', ['sequences']); /** @var \Drupal\og\OgDeleteOrphansPluginManager $plugin_manager */ $plugin_manager = \Drupal::service('plugin.manager.og.delete_orphans'); diff --git a/tests/src/Kernel/OgRoleManagerTest.php b/tests/src/Kernel/OgRoleManagerTest.php index f226fe7a4..2d8378b94 100644 --- a/tests/src/Kernel/OgRoleManagerTest.php +++ b/tests/src/Kernel/OgRoleManagerTest.php @@ -23,6 +23,7 @@ class OgRoleManagerTest extends KernelTestBase { 'field', 'node', 'og', + 'options', ]; /** diff --git a/tests/src/Kernel/PermissionEventTest.php b/tests/src/Kernel/PermissionEventTest.php index c9a0ea94c..05089d1ec 100644 --- a/tests/src/Kernel/PermissionEventTest.php +++ b/tests/src/Kernel/PermissionEventTest.php @@ -28,6 +28,7 @@ class PermissionEventTest extends KernelTestBase { 'og', 'system', 'user', + 'options', ]; /** diff --git a/tests/src/Kernel/SelectionHandlerSettingsSchemaTest.php b/tests/src/Kernel/SelectionHandlerSettingsSchemaTest.php index c1effe04c..8d1e38da3 100644 --- a/tests/src/Kernel/SelectionHandlerSettingsSchemaTest.php +++ b/tests/src/Kernel/SelectionHandlerSettingsSchemaTest.php @@ -22,6 +22,7 @@ class SelectionHandlerSettingsSchemaTest extends KernelTestBase { 'og_ui', 'system', 'user', + 'options', ]; /** diff --git a/tests/src/Kernel/Views/OgAdminMembersViewTest.php b/tests/src/Kernel/Views/OgAdminMembersViewTest.php index 0a41fbc16..5565b070b 100644 --- a/tests/src/Kernel/Views/OgAdminMembersViewTest.php +++ b/tests/src/Kernel/Views/OgAdminMembersViewTest.php @@ -5,7 +5,7 @@ use Drupal\node\Entity\Node; use Drupal\node\Entity\NodeType; use Drupal\og\Og; -use Drupal\simpletest\UserCreationTrait; +use Drupal\Tests\user\Traits\UserCreationTrait; use Drupal\Tests\views\Kernel\ViewsKernelTestBase; use Drupal\views\Views; @@ -27,6 +27,7 @@ class OgAdminMembersViewTest extends ViewsKernelTestBase { 'field', 'node', 'og', + 'options', 'views', ]; diff --git a/tests/src/Unit/CreateMembershipTest.php b/tests/src/Unit/CreateMembershipTest.php index 3a461c003..dd00b211c 100644 --- a/tests/src/Unit/CreateMembershipTest.php +++ b/tests/src/Unit/CreateMembershipTest.php @@ -13,6 +13,7 @@ use Drupal\Tests\UnitTestCase; use Drupal\user\UserInterface; use Prophecy\Argument; +use Drupal\Core\Database\Connection; /** * Tests create membership helper function. @@ -85,6 +86,13 @@ class CreateMembershipTest extends UnitTestCase { */ protected $membership; + /** + * The database service. + * + * @var \Drupal\Core\Database\Connection + */ + protected $database; + /** * {@inheritdoc} */ @@ -98,6 +106,7 @@ public function setUp() { $this->entityTypeManager = $this->prophesize(EntityTypeManagerInterface::class); $this->entityTypeRepository = $this->prophesize(EntityTypeRepositoryInterface::class); $this->groupAudienceHelper = $this->prophesize(OgGroupAudienceHelperInterface::class); + $this->database = $this->prophesize(Connection::class); $this->entityTypeManager->getStorage('og_membership') ->willReturn($this->entityStorage->reveal()); @@ -130,6 +139,7 @@ public function setUp() { $container = new ContainerBuilder(); $container->set('entity_type.manager', $this->entityTypeManager->reveal()); $container->set('entity_type.repository', $this->entityTypeRepository->reveal()); + $container->set('database', $this->database->reveal()); \Drupal::setContainer($container); } @@ -139,7 +149,7 @@ public function setUp() { * @covers ::createMembership */ public function testNewGroup() { - $membership_manager = new MembershipManager($this->entityTypeManager->reveal(), $this->groupAudienceHelper->reveal()); + $membership_manager = new MembershipManager($this->entityTypeManager->reveal(), $this->groupAudienceHelper->reveal(), $this->database->reveal()); $membership = $membership_manager->createMembership($this->group->reveal(), $this->user->reveal()); $this->assertInstanceOf(OgMembershipInterface::class, $membership); } diff --git a/tests/src/Unit/GroupTypeManagerTest.php b/tests/src/Unit/GroupTypeManagerTest.php index c00815481..37ebdc1b3 100644 --- a/tests/src/Unit/GroupTypeManagerTest.php +++ b/tests/src/Unit/GroupTypeManagerTest.php @@ -2,13 +2,13 @@ namespace Drupal\Tests\og\Unit; +use Drupal\Core\Cache\CacheBackendInterface; 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\Routing\RouteBuilderInterface; -use Drupal\Core\State\StateInterface; use Drupal\og\Event\GroupCreationEvent; use Drupal\og\Event\GroupCreationEventInterface; use Drupal\og\GroupTypeManager; @@ -87,11 +87,11 @@ class GroupTypeManagerTest extends UnitTestCase { protected $permissionEvent; /** - * The state prophecy used in the test. + * The cache prophecy used in the test. * - * @var \Drupal\Core\State\StateInterface|\Prophecy\Prophecy\ObjectProphecy + * @var \Drupal\Core\Cache\CacheBackendInterface|\Prophecy\Prophecy\ObjectProphecy */ - protected $state; + protected $cache; /** * The OG permission manager prophecy used in the test. @@ -135,7 +135,7 @@ public function setUp() { $this->ogRoleManager = $this->prophesize(OgRoleManagerInterface::class); $this->permissionEvent = $this->prophesize(PermissionEventInterface::class); $this->permissionManager = $this->prophesize(PermissionManagerInterface::class); - $this->state = $this->prophesize(StateInterface::class); + $this->cache = $this->prophesize(CacheBackendInterface::class); $this->routeBuilder = $this->prophesize(RouteBuilderInterface::class); $this->groupAudienceHelper = $this->prophesize(OgGroupAudienceHelperInterface::class); } @@ -331,7 +331,7 @@ protected function createGroupManager() { $this->entityTypeManager->reveal(), $this->entityTypeBundleInfo->reveal(), $this->eventDispatcher->reveal(), - $this->state->reveal(), + $this->cache->reveal(), $this->permissionManager->reveal(), $this->ogRoleManager->reveal(), $this->routeBuilder->reveal(), diff --git a/tests/src/Unit/SubscriptionControllerTest.php b/tests/src/Unit/SubscriptionControllerTest.php index db2e2e67f..19b69d9e9 100644 --- a/tests/src/Unit/SubscriptionControllerTest.php +++ b/tests/src/Unit/SubscriptionControllerTest.php @@ -5,6 +5,7 @@ use Drupal\Core\DependencyInjection\ContainerBuilder; use Drupal\Core\Entity\ContentEntityInterface; use Drupal\Core\Entity\EntityFormBuilderInterface; +use Drupal\Core\Messenger\MessengerInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; use Drupal\og\Controller\SubscriptionController; @@ -50,6 +51,13 @@ class SubscriptionControllerTest extends UnitTestCase { */ protected $ogAccess; + /** + * The mocked messenger service. + * + * @var \Drupal\Core\Messenger\MessengerInterface|\Prophecy\Prophecy\ObjectProphecy + */ + protected $messenger; + /** * The OG membership entity. * @@ -79,16 +87,18 @@ public function setUp() { $this->group = $this->prophesize(ContentEntityInterface::class); $this->membershipManager = $this->prophesize(MembershipManagerInterface::class); $this->ogAccess = $this->prophesize(OgAccessInterface::class); + $this->messenger = $this->prophesize(MessengerInterface::class); $this->ogMembership = $this->prophesize(OgMembershipInterface::class); $this->url = $this->prophesize(Url::class); $this->user = $this->prophesize(AccountInterface::class); - + $this->messenger = $this->prophesize(MessengerInterface::class); // Set the container for the string translation service. $container = new ContainerBuilder(); $container->set('current_user', $this->user->reveal()); $container->set('entity.form_builder', $this->entityFormBuilder->reveal()); $container->set('og.membership_manager', $this->membershipManager->reveal()); $container->set('string_translation', $this->getStringTranslationStub()); + $container->set('messenger', $this->messenger->reveal()); \Drupal::setContainer($container); } @@ -255,21 +265,8 @@ public function testGroupManager($state) { * Invoke the unsubscribe method. */ protected function unsubscribe() { - $controller = new SubscriptionController($this->ogAccess->reveal()); + $controller = new SubscriptionController($this->ogAccess->reveal(), $this->messenger->reveal()); $controller->unsubscribe($this->group->reveal()); } } - -// @todo Delete after https://www.drupal.org/node/1858196 is in. -namespace Drupal\og\Controller; - -if (!function_exists('drupal_set_message')) { - - /** - * Mocking for drupal_set_message(). - */ - function drupal_set_message() { - } - -}