diff --git a/admin/group.config.inc b/admin/group.config.inc index c8efdbe..1e4848b 100644 --- a/admin/group.config.inc +++ b/admin/group.config.inc @@ -31,5 +31,20 @@ function group_config_form($form, &$form_state) { '#default_value' => variable_get('group_autocomplete_suffix'), ); + $disabled = (bool)db_select('group_entity', 'ge') + ->fields('ge', array('entity_id')) + ->condition('ge.entity_type', 'node') + ->range(0, 1) + ->execute() + ->fetchField(); + + $form['group_node_access_grant_override'] = array( + '#type' => 'checkbox', + '#title' => t('Override group grant'), + '#description' => t('By default, group module will rely completly on grant system except operation "create". It\'s allow other contrib modules to interact with the node access policy. As it depends entirely on database, it will insert all grants into "node_access" table and will extends node queries. For website with an important number of group nodes published and don\'t/wouldn\t have any other modules which interact with group node access, you could override this behaviour by using node access only instead for operation "update" and "delete". This is an important choice because you won\'t be able to switch from one method to another later if at least one group node exists. Otherwise, you will need to force this option manually and migrate nodes to be updated yourself on your own risks. That\'s why we recommend to set this option only if you are sure about what you are doing.'), + '#default_value' => variable_get('group_node_access_grant_override', FALSE), + '#disabled' => $disabled, + ); + return system_settings_form($form); } diff --git a/group.install b/group.install index e6853e5..aaf50a5 100644 --- a/group.install +++ b/group.install @@ -10,6 +10,8 @@ function group_install() { variable_set('group_admin_theme', '1'); variable_set('group_autocomplete_suffix', array('general' => 0)); + + node_access_needs_rebuild(TRUE); } /** @@ -18,6 +20,7 @@ function group_install() { function group_uninstall() { variable_del('group_admin_theme'); variable_del('group_autocomplete_suffix'); + variable_del('group_node_access_grant_override'); } /** diff --git a/group.module b/group.module index e40aee8..8b2823e 100644 --- a/group.module +++ b/group.module @@ -14,6 +14,11 @@ module_load_include('inc', 'group', 'helpers/entity.group_type'); module_load_include('inc', 'group', 'helpers/group'); /** + * Load module implementations without polluting the .module file. + */ +module_load_include('inc', 'group', 'implementations/group.node'); + +/** * Implements hook_hook_info(). * * Makes sure this module automatically finds exported Group @@ -493,7 +498,7 @@ function group_group_permission() { $permissions = array( 'administer group' => array( 'title' => t('Administer group'), - 'description' => t('Administer the group, its users and permissions'), + 'description' => t('Administer the group, its content and members'), 'restrict access' => TRUE, ), 'view group' => array( diff --git a/implementations/group.node.inc b/implementations/group.node.inc new file mode 100644 index 0000000..2712038 --- /dev/null +++ b/implementations/group.node.inc @@ -0,0 +1,535 @@ + $node_type->name); + + $permissions["view $node_type->type"] = array( + 'title' => t('%node_type: View content', $replace), + ); + $permissions["create $node_type->type"] = array( + 'title' => t('%node_type: Create new content', $replace), + ); + $permissions["edit own $node_type->type content"] = array( + 'title' => t('%node_type: Edit own content', $replace), + ); + $permissions["edit any $node_type->type content"] = array( + 'title' => t('%node_type: Edit any content', $replace), + ); + $permissions["delete own $node_type->type content"] = array( + 'title' => t('%node_type: Delete own content', $replace), + ); + $permissions["delete any $node_type->type content"] = array( + 'title' => t('%node_type: Delete any content', $replace), + ); + } + + return $permissions; +} + +/** + * Implements hook_node_access(). + * + * We rely on the grant system to allow access to a node. Only node creation + * cannot be handled by the grant system and is therefore taken care of here. + * + * @see group_node_grants(). + * @see group_node_access_records(). + */ +function group_node_access($node, $op, $account) { + if (is_string($node)) { + if ($op == 'create') { + + // If the user can bypass group access, he is allowed access. + if (user_access('bypass group access', $account)) { + return NODE_ACCESS_ALLOW; + } + + // Gather all groups the user is a member of. + $member_groups = group_load_by_member($account->uid); + + // Gather all groups an outsider can create nodes of the given type in. + $outsider_groups = array(); + foreach (group_types() as $type => $group_type) { + $has_access = in_array('administer group', $group_type->outsider_permissions); + $has_access = $has_access || in_array("create $node", $group_type->outsider_permissions); + + if ($has_access) { + $outsider_groups += group_load_by_type($type); + } + } + + // If any group in $outsider_groups is not present in $member_groups, + // it means that the user is indeed an outsider for that group. Seeing + // as all groups in $outsider_groups should allow access to outsiders, + // we allow access for the provided user. + if (array_diff_key($outsider_groups, $member_groups)) { + return NODE_ACCESS_ALLOW; + } + + // Check the user's groups for creation rights. + foreach ($member_groups as $group) { + $has_access = group_access('administer group', $group, $account); + $has_access = $has_access || group_access("create $node", $group, $account); + + if ($has_access) { + return NODE_ACCESS_ALLOW; + } + } + } + } + elseif (variable_get('group_node_access_grant_override', FALSE) + && in_array($op, array('update', 'delete'), TRUE)) { + + // Node could not have more than one group. + $node_groups = group_get_entity_groups('node', $node->nid); + $node_group = array_shift($node_groups); + + //With this implementation, we don't ignore access to rely on group + //grant system because we want to force it. That's why using this + //implementation should be choice carefully. We didn't allow any other + //modules except node module itself to interact with node access permission. + if ($node_group) { + + //Deny access by default. + $access = NODE_ACCESS_DENY; + + // If the user can bypass group access, he is allowed access. + if (user_access('bypass group access', $account)) { + $access = NODE_ACCESS_ALLOW; + } + else { + + //Check that account is author of the node. + $owner = ((int)$node->uid === (int)$account->uid); + + //Get permissions of account if member or outsider. + $permissions = $node_group->userPermissions($account->uid); + + //Administer account could edit or delete any content of this group. + if (in_array('administer group', $permissions, TRUE)) { + $access = NODE_ACCESS_ALLOW; + } + //Otherwise, check specific permissions based on op. + elseif ($op === 'update' && (in_array("edit any $node->type content", $permissions, TRUE) + || ($owner && in_array("edit own $node->type content", $permissions, TRUE)))) { + $access = NODE_ACCESS_ALLOW; + } + elseif ($op === 'delete' && (in_array("delete any $node->type content", $permissions, TRUE) + || ($owner && in_array("delete own $node->type content", $permissions, TRUE)))) { + $access = NODE_ACCESS_ALLOW; + } + } + + //Force allow or denied access. + return $access; + } + } + + return NODE_ACCESS_IGNORE; +} + +/** + * Implements hook_node_grants(). + * + * @see group_node_access_records(). + */ +function group_node_grants($account, $op) { + $grants = array(); + + // If the user can bypass group access, he only needs the master grant. + if (user_access('bypass group access', $account)) { + return array('group:bypass' => array(GROUP_BYPASS_GRANT_ID)); + } + + // Gather the machine names of all node types. + $node_types = array_keys(node_type_get_types()); + + // Gather all groups the user is a member of. + $member_groups = group_load_by_member($account->uid); + + // Provide grants for groups the user is an outsider to. + foreach (group_types() as $type => $group_type) { + $outsider_groups = group_load_by_type($type); + $outsider_gids = array_diff_key($outsider_groups, $member_groups); + if ($outsider_gids) { + + // If the user is an admin, he requires no further specific grants. + if (in_array('administer group', $group_type->outsider_permissions)) { + foreach ($outsider_gids as $gid) { + $grants['group:administer'][] = $gid; + } + continue; + } + + foreach ($node_types as $node_type) { + // Shorten variable name for readability's sake. + $permissions = $group_type->outsider_permissions; + + // Allow the user to view content of this type. + if ($op == 'view' && in_array("view $node_type", $permissions)) { + foreach ($outsider_gids as $gid) { + $grants["group:$node_type:view"][] = $gid; + } + } + // Allow the user to edit any content of this type. + elseif ($op == 'update' && in_array("edit any $node_type", $permissions)) { + foreach ($outsider_gids as $gid) { + $grants["group:$node_type:update"][] = $gid; + } + } + // Allow the user to delete any content of this type. + elseif ($op == 'delete' && in_array("delete any $node_type", $permissions)) { + foreach ($outsider_gids as $gid) { + $grants["group:$node_type:delete"][] = $gid; + } + } + // Allow the user to edit own content of this type. + elseif ($op == 'update' && in_array("edit own $node_type", $permissions)) { + foreach ($outsider_gids as $gid) { + $grants["group:$gid:$node_type:update"][] = $account->uid; + } + } + // Allow the user to delete own content of this type. + elseif ($op == 'delete' && in_array("delete own $node_type", $permissions)) { + foreach ($outsider_gids as $gid) { + $grants["group:$gid:$node_type:delete"][] = $account->uid; + } + } + } + } + } + + // Provide grants for groups the user is a member of. + foreach ($member_groups as $group) { + // If the user is an admin, he requires no further specific grants. + if (group_access('administer group', $group, $account)) { + $grants['group:administer'][] = $group->gid; + continue; + } + + foreach ($node_types as $node_type) { + // Allow the user to view content of this type. + if ($op == 'view' && group_access("view $node_type", $group, $account)) { + $grants["group:$node_type:view"][] = $group->gid; + } + // Allow the user to edit any content of this type. + elseif ($op == 'update' && group_access("edit any $node_type", $group, $account)) { + $grants["group:$node_type:update"][] = $group->gid; + } + // Allow the user to delete any content of this type. + elseif ($op == 'delete' && group_access("delete any $node_type", $group, $account)) { + $grants["group:$node_type:delete"][] = $group->gid; + } + // Allow the user to edit own content of this type. + elseif ($op == 'update' && group_access("edit own $node_type", $group, $account)) { + $grants["group:$group->gid:$node_type:update"][] = $account->uid; + } + // Allow the user to delete own content of this type. + elseif ($op == 'delete' && group_access("delete own $node_type", $group, $account)) { + $grants["group:$group->gid:$node_type:delete"][] = $account->uid; + } + } + } + + return $grants; +} + +/** + * Implements hook_node_grants_alter(). + */ +function group_node_grants_alter(&$grants, $account, $op) { + + if (variable_get('group_node_access_grant_override', FALSE)) { + + //Remove all grant group permission except 'view'. + foreach (array_keys($grants) as $realm) { + if (strpos($realm, 'group:') === 0 + && substr($realm, (strlen($realm) - 5)) === ':view') { + + unset($grants[$realm]); + } + } + } +} + +/** + * Implements hook_node_access_records(). + * + * We define the following realms: + * - 'group:bypass': Grants full access to any user having the + * 'bypass group access' permission. Because a realm id isn't really + * needed here, we specify the module author's date of birth: 1986. + * - 'group:administer': Grants full access to any user having the + * 'administer group' permission for the group the node belongs to. Because + * this realm is defined for every group, we use the group id as realm id. + * - 'group:NODE_TYPE:OP': Grants access for the specified operation to any + * user having one of the following group permissions: + * - view NODE_TYPE + * - update any NODE_TYPE + * - delete any NODE_TYPE + * Because these realms are defined for every group, we use the group id as + * realm id. + * - 'group:GROUP_ID:NODE_TYPE:OP': Grants access for the specified operation + * to any user having one of the following group permissions: + * - update own NODE_TYPE + * - delete own NODE_TYPE + * Because these realms are defined for specific group-user combinations, we + * should use the user id as the grant id. However, we still need to know + * what group the grant is for, so we incorporate the group id into the + * realm name. We chose to incorporate the group id instead of the user id + * because there will almost always be more users than groups. + * + * @see group_node_grants(). + */ +function group_node_access_records($node) { + $grants = array(); + + // Add realms specific to this group. + if (!empty($node->group)) { + // Add the realm corresponding to the 'bypass group access' permission. + $grants[] = array( + 'realm' => 'group:bypass', + 'gid' => GROUP_BYPASS_GRANT_ID, + 'grant_view' => 1, + 'grant_update' => 1, + 'grant_delete' => 1, + 'priority' => 0, + ); + + // Add the realm corresponding to the 'administer group' permission. + $grants[] = array( + 'realm' => 'group:administer', + 'gid' => $node->group, + 'grant_view' => 1, + 'grant_update' => 1, + 'grant_delete' => 1, + 'priority' => 0, + ); + + // Add realms for published nodes. + if ($node->status) { + $defaults_any = array( + 'gid' => $node->group, + 'grant_view' => 0, + 'grant_update' => 0, + 'grant_delete' => 0, + 'priority' => 0, + ); + + $defaults_own = array( + 'gid' => $node->uid, + ) + $defaults_any; + + // View any content of this type. + $grants[] = array ( + 'realm' => "group:$node->type:view", + 'grant_view' => 1, + ) + $defaults_any; + + // Edit any content of this type. + $grants[] = array ( + 'realm' => "group:$node->type:update", + 'grant_update' => 1, + ) + $defaults_any; + + // Delete any content of this type. + $grants[] = array ( + 'realm' => "group:$node->type:delete", + 'grant_delete' => 1, + ) + $defaults_any; + + // Edit own content of this type. + $grants[] = array ( + 'realm' => "group:$node->group:$node->type:update", + 'grant_update' => 1, + ) + $defaults_own; + + // Delete own content of this type. + $grants[] = array ( + 'realm' => "group:$node->group:$node->type:delete", + 'grant_delete' => 1, + ) + $defaults_own; + } + } + + return $grants; +} + +/** + * Implements hook_node_access_records_alter(). + */ +function group_node_access_records_alter(&$grants, $node) { + + if (variable_get('group_node_access_grant_override', FALSE)) { + + //For node group, only insert 'view' op into node_access table. + if (!empty($node->group)) { + + foreach ($grants as $key => $grant) { + if (strpos($grant['realm'], 'group:') === 0 && $grant['realm'] !== "group:$node->type:view") { + unset($grants[$key]); + } + } + } + } +} + +/** + * Implements hook_node_prepare(). + */ +function group_node_prepare($node) { + if (empty($node->group)) { + $groups = isset($node->nid) ? group_get_entity_groups('node', $node->nid) : array(); + + // Nodes can only have one group. + $node->group = !empty($groups) ? key($groups) : 0; + } +} + +/** + * Implements hook_node_insert(). + */ +function group_node_insert($node) { + group_node_save($node); +} + +/** + * Implements hook_node_update(). + */ +function group_node_update($node) { + // Delete all existing links. + $query = db_delete('group_entity'); + $query->condition('entity_id', $node->nid); + $query->condition('entity_type', 'node'); + $query->execute(); + + // Saving takes care of creating new links. + group_node_save($node); +} + +/** + * Helper for hook_node_insert() and hook_node_update(). + */ +function group_node_save($node) { + if (isset($node->group)) { + group_load($node->group)->addEntity($node->nid, 'node', $node->type); + } +} + +/** + * Implements hook_form_BASE_FORM_ID_alter(). + * + * Adds a Group vertical tab to the node form. + * + * You can only select those groups that you can create nodes of this type in. + * It would not make sense if someone could move nodes to a group where he does + * not have creation rights. + * + * @see group_node_submit() + */ +function group_form_node_form_alter(&$form, $form_state) { + global $user; + $node_type = $form['#node']->type; + + // Gather all groups the user is a member of. + $member_groups = group_load_by_member($user->uid); + + // Gather all groups an outsider can create nodes of the given type in. + $outsider_groups = array(); + foreach (group_types() as $type => $group_type) { + $has_access = $user->uid == 1; + $has_access = $has_access || in_array('administer group', $group_type->outsider_permissions); + $has_access = $has_access || in_array("create $node_type", $group_type->outsider_permissions); + + if ($has_access) { + $outsider_groups += group_load_by_type($type); + } + } + + // If any group in $outsider_groups is not present in $member_groups, + // it means that the user is indeed an outsider for that group. Seeing + // as all groups in $outsider_groups should allow access to outsiders, + // we allow access for the provided user. + $user_groups = array_diff_key($outsider_groups, $member_groups); + foreach ($user_groups as $gid => $group) { + $user_groups[$gid] = check_plain($group->title); + } + + // Check the user's groups for creation rights and add them to $user_groups. + foreach ($member_groups as $group) { + $has_access = $group->userHasPermission($user->uid, "create $node_type"); + $has_access = $has_access || $group->userHasPermission($user->uid, 'administer group'); + + if ($has_access) { + $user_groups[$group->gid] = check_plain($group->title); + } + } + + $form['group_settings'] = array( + '#type' => 'fieldset', + '#title' => t('Group settings'), + '#access' => !empty($user_groups), + '#collapsible' => TRUE, + '#collapsed' => FALSE, + '#group' => 'additional_settings', + '#attributes' => array( + 'class' => array('node-form-group-information'), + ), + '#attached' => array( + 'js' => array(drupal_get_path('module', 'group') . '/misc/group.js'), + ), + '#tree' => TRUE, + '#weight' => -50, + ); + + $form['group_settings']['group'] = array( + '#type' => 'select', + '#title' => t('Select a group to attach this node to.'), + '#description' => t("By selecting a group, the node will inherit the group's access control."), + '#options' => $user_groups, + '#empty_option' => t('- No group -'), + '#empty_value' => 0, + '#default_value' => $form['#node']->group, + // If the user can't create this node outside of a group, he is not allowed + // to move it to the sitewide scope either. + '#required' => !user_access("create $node_type"), + ); +} + +/** + * Implements hook_node_submit(). + * + * @see group_form_node_form_alter() + */ +function group_node_submit($node, $form, $form_state) { + // Decompose the selected menu parent option into 'menu_name' and 'plid', if + // the form used the default parent selection widget. + if (!empty($form_state['values']['group_settings']['group'])) { + $node->group = $form_state['values']['group_settings']['group']; + } +} diff --git a/misc/group.js b/misc/group.js new file mode 100644 index 0000000..8551a76 --- /dev/null +++ b/misc/group.js @@ -0,0 +1,17 @@ +(function ($) { + +Drupal.behaviors.groupFieldsetSummaries = { + attach: function (context) { + $('fieldset.node-form-group-information', context).drupalSetSummary(function (context) { + var option = $('.form-item-group-settings-group option:selected', context); + + if (option.val() != '0') { + return $.trim(option.text()); + } + + return Drupal.t('Not a group node'); + }); + } +}; + +})(jQuery);