diff --git a/forms/group_entity.inc b/forms/group_entity.inc new file mode 100644 index 0000000..20226dd --- /dev/null +++ b/forms/group_entity.inc @@ -0,0 +1,42 @@ +uid); + if ($groups_account) { + + //Get groups of the current node if updated. + $groups_node = array(); + if (!empty($node->nid)) { + $groups_node = group_get_entity_groups('node', $node->nid); + } + + //Build options + $options = array(); + foreach ($groups_account as $group) { + $options[$group->gid] = $group->title; + } + + //Check that if node is attach to a group, be sure current user is member of + //this group. If occured, this is an unexpected bug. + $disabled = ($groups_node && !array_intersect_key($groups_account, $groups_node)) ? TRUE : FALSE; + + $form[GROUP_EXTRA_FIELD_GROUP] = array( + '#type' => 'select', + '#title' => t('Select a group to attach to this node.'), + '#description' => ($disabled) ? t('You can not use this field because the node is attach to a group which is not one of yours memberships') : t('By selecting a group, this content will be private and accessible only to this group. If no group is selected, the node is public.'), + '#multiple' => FALSE, + '#options' => $options, + '#empty_options' => t('No group - public node'), + '#empty_value' => 0, + '#default_value' => (!empty($groups_node)) ? key($groups_node) : 0, + '#disabled' => $disabled, + ); + } +} diff --git a/group.info b/group.info index 4e5b781..6eeecdc 100644 --- a/group.info +++ b/group.info @@ -18,3 +18,7 @@ files[] = classes/group_type.inc files[] = classes/group_type.controller.inc files[] = classes/group_type.ui_controller.inc files[] = classes/group_type.features_controller.inc + +files[] = tests/group_helper.test +files[] = tests/group_node_access.test +files[] = tests/group_node_access_ui.test diff --git a/group.install b/group.install index e6853e5..506fa7e 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); } /** diff --git a/group.module b/group.module index b650408..5c31193 100644 --- a/group.module +++ b/group.module @@ -5,6 +5,16 @@ */ /** + * Realm node access used by group module. + */ +define('GROUP_REALM', 'group_realm'); + +/** + * Extra field used by this module to map groups and group entities. + */ +define('GROUP_EXTRA_FIELD_GROUP', 'group_group'); + +/** * Load our helper functions without polluting the .module file. */ module_load_include('inc', 'group', 'helpers/entity.group'); @@ -426,6 +436,30 @@ function group_set_properties(&$data, $name, $value, $langcode, $type, $info) { } /** + * Implements hook_field_extra_fields() + */ +function group_field_extra_fields() { + $extra = array(); + + //Add an extra field for every content type, including disabled. + $node_types = node_type_get_names(); + foreach (array_keys($node_types) as $bundle) { + + $extra['node'][$bundle] = array( + 'form' => array( + GROUP_EXTRA_FIELD_GROUP => array( + 'label' => t('Group selector'), + 'description' => t('Allow user to attach/detach a group to the node.'), + 'weight' => 0, + ), + ), + ); + } + + return $extra; +} + +/** * Implements hook_theme(). */ function group_theme() { @@ -480,6 +514,27 @@ function group_group_permission() { ), ); + //Node type permissions, including disabled. + $node_types = node_type_get_types(); + foreach ($node_types as $node_type) { + + $permissions["create $node_type->type"] = array( + 'title' => t('%node_type: Create new content', array('%node_type' => $node_type->name)), + ); + $permissions["edit own $node_type->type content"] = array( + 'title' => t('%node_type: Edit own content', array('%node_type' => $node_type->name)), + ); + $permissions["edit any $node_type->type content"] = array( + 'title' => t('%node_type: Edit any content', array('%node_type' => $node_type->name)), + ); + $permissions["delete own $node_type->type content"] = array( + 'title' => t('%node_type: Delete own content', array('%node_type' => $node_type->name)), + ); + $permissions["delete any $node_type->type content"] = array( + 'title' => t('%node_type: Delete any content', array('%node_type' => $node_type->name)), + ); + } + return $permissions; } @@ -571,6 +626,174 @@ function group_type_access($op, $group_type_or_role, $account = NULL, $entity_ty } /** + * Implements hook_node_access(). + */ +function group_node_access($node, $op, $account) { + + //View access is deals with grant access. + if ($op !== 'view') { + + $is_node = (is_object($node)); + $node_groups = ($is_node) ? group_get_entity_groups('node', $node->nid) : array(); + + //If node is private or having a node type(for e.g : op create), check if + //provided account have group permission for this operation. + if ($node_groups || !$is_node) { + + $node_type = ''; + $owner = FALSE; + + //Get account groups and check matches with node groups. + $account_groups = group_load_by_member($account->uid); + if ($is_node) { + $matching_groups = array_intersect_key($node_groups, $account_groups); + $node_type = $node->type; + $owner = ($node->uid == $account->uid); + } + else { + //Affect all account groups to see if it have permission for this + //operation on this node type. + $matching_groups = $account_groups; + $node_type = $node; + } + + if ($matching_groups) { + + //Iterate over each matching groups and check if access is allowed for + //the node/node type in the group context permissions. + foreach ($matching_groups as $group) { + + $permissions = array(); + + switch ($op) { + + case 'create' : + $permissions[] = "create $node_type"; + break; + + case 'update' : + $permissions[] = "edit any $node_type content"; + if ($owner) { + $permissions[] = "edit own $node_type content"; + } + break; + + case 'delete' : + $permissions[] = "delete any $node_type content"; + if ($owner) { + $permissions[] = "delete own $node_type content"; + } + break; + } + + foreach ($permissions as $permission) { + if (group_access($permission, $group, $account)) { + return NODE_ACCESS_ALLOW; + } + } + } + } + + //Only denied access on private node. Otherwise, ignore it. + if ($is_node) { + return NODE_ACCESS_DENY; + } + } + } + + return NODE_ACCESS_IGNORE; +} + +/** + * Implements hook_node_grants(). + */ +function group_node_grants($account, $op) { + $grants = array(); + + if ($op === 'view') { + + $groups = group_load_by_member($account->uid); + foreach ($groups as $group) { + $realm = GROUP_REALM . ':' . $group->type; + $grants[$realm][] = $group->gid; + } + } + + return $grants; +} + +/** + * Implements hook_node_access_records(). + */ +function group_node_access_records($node) { + $grants = array(); + + if ($node->status) { + + $groups = group_get_entity_groups('node', $node->nid); + foreach ($groups as $group) { + $grants[] = array ( + 'realm' => GROUP_REALM . ':' . $group->type, + 'gid' => $group->gid, + 'grant_view' => 1, + 'grant_update' => 0, + 'grant_delete' => 0, + 'priority' => 0, + ); + } + } + + return $grants; +} + +/** + * Implements hook_node_insert() + */ +function group_node_insert($node) { + + //Add a group entity to the node. Only one group could be added for the moment + //to avoid conflicts. + if (!empty($node->{GROUP_EXTRA_FIELD_GROUP})) { + + $gids = array($node->{GROUP_EXTRA_FIELD_GROUP}); + $groups = group_load_multiple($gids); + foreach ($groups as $group) { + $group->addEntity($node->nid, 'node', $node->type); + } + } +} + +/** + * Implements hook_node_update() + */ +function group_node_update($node) { + + //Add/remove group entity mapping. Note that for the moment, only one of this + //action could be done and only one group could be added/removed to avoid conflicts. + if (isset($node->{GROUP_EXTRA_FIELD_GROUP})) { + + //Get current groups attach to the node + $groups = group_get_entity_groups('node', $node->nid); + + //Retrieve group from node + $gids = array($node->{GROUP_EXTRA_FIELD_GROUP}); + $new_groups = group_load_multiple($gids); + + //Remove groups to node (only one for the moment to avoid conflicts). + $group_removed = array_diff_key($groups, $new_groups); + foreach ($group_removed as $group) { + $group->removeEntity($node->nid, 'node'); + } + + //Add groups to node (only one for the moment to avoid conflicts). + $group_added = array_diff_key($new_groups, $groups); + foreach ($group_added as $group) { + $group->addEntity($node->nid, 'node', $node->type); + } + } +} + +/** * Implements hook_menu_local_tasks_alter(). * * Adds a local task to admin/group. @@ -637,3 +860,15 @@ function group_group_operations() { return $operations; } + +/** + * Implements hook_form_alter() + */ +function group_form_alter(&$form, &$form_state, $form_id) { + + //Alter node form, not only ones from core with '_node_form' suffix. + if (!empty($form['#node_edit_form'])) { + module_load_include('inc', 'group', 'forms/group_entity'); + group_entity_node_form_alter($form, $form_state); + } +} diff --git a/tests/group_helper.test b/tests/group_helper.test new file mode 100644 index 0000000..c37d75a --- /dev/null +++ b/tests/group_helper.test @@ -0,0 +1,99 @@ + $this->randomName(), + 'label' => $this->randomString(), + ); + $group_type = entity_create('group_type', $values); + group_type_save($group_type); + return $group_type; + } + + /** + * Create a group entity. + * + * @param $group_type + * A group type entity as bundle of the group entity about to be created. + * @param $values. + * Values to create the entities. + * + * @return + * A group entity. + */ + protected function createGroup(GroupType $group_type, array $values = array()) { + $values += array( + 'type' => $group_type->name, + 'title' => $this->randomString(), + ); + $group = entity_create('group', $values); + group_save($group); + return $group; + } + + /** + * Create a group role entity. + * + * @param $group_type + * An optional group type entity if group role is not global. + * @param $values + * Values to create the group role entity. + * + * @return + * A group role entity. + */ + protected function createGroupRole(GroupType $group_type = NULL, array $values = array()) { + $values += array( + 'type' => $group_type->name, + 'name' => $this->randomName(), + 'label' => $this->randomString(), + 'permissions' => array(), + ); + $group_role = entity_create('group_role', $values); + group_role_save($group_role); + return $group_role; + } + + /** + * Create a user entity and add it as a member of the provided group entity. + * + * @param $group + * A group entity to attach the created user entity. + * @param $group_roles + * An array of group role entity names to add for the member created into + * the group. + * @param $core_permissions + * An array of default permissions to set for the member created. + * + * @return + * A user entity, member of the group provided with group roles and + * permissions provided if any in addition. Note that this user entity have + * the access content permission. + */ + protected function createGroupMember(Group $group, array $group_roles = array(), array $permissions = array()) { + $permissions += array('access content'); + $account = $this->drupalCreateUser($permissions); + $group->addMember($account->uid, $group_roles); + return $account; + } + +} diff --git a/tests/group_node_access.test b/tests/group_node_access.test new file mode 100644 index 0000000..130ec63 --- /dev/null +++ b/tests/group_node_access.test @@ -0,0 +1,758 @@ + 'Group node access', + 'description' => 'Run tests for group node access', + 'group' => 'Group', + ); + } + + /** + * Override parent::setUp() + */ + public function setUp() { + + //Call parent::setUp() and set modules dependencies. + parent::setUp('entity', 'group'); + + //Rebuild node access + node_access_rebuild(FALSE); + + //Create a default group type + $this->groupType = $this->createGroupType(); + + //Create a default group + $this->group = $this->createGroup($this->groupType); + + //Create a node type + $this->nodeType = $this->drupalCreateContentType(); + + //Create a default admin member, reload default group roles to get content type group permissions. + entity_defaults_rebuild(); + $this->groupMemberAdmin = $this->createGroupMember($this->group, array('group_admin')); + + //Retrieve all group permissions from group module. + $permissions = module_invoke('group', 'group_permission'); + + //Create a global group role 'editor all' for previous content type + $group_role = array(); + foreach ($permissions as $name => $permission) { + if (strpos($name, $this->nodeType->type) !== FALSE) { + $group_role['permissions'][] = $name; + } + } + $this->groupRoleEditorAny = $this->createGroupRole($this->groupType, $group_role); + + //Create a global group role 'editor owner only' for previous content type + $node_type = $this->nodeType->type; + $group_role = array(); + foreach ($permissions as $name => $permission) { + if (strpos($name, "create $node_type") !== FALSE + || strpos($name, "own $node_type") !== FALSE) { + $group_role['permissions'][] = $name; + } + } + $this->groupRoleEditorOwn = $this->createGroupRole($this->groupType, $group_role); + + //Create members for these both group roles + $this->groupMemberEditorAny = $this->createGroupMember( + $this->group, + array($this->groupRoleEditorAny->name) + ); + + $this->groupMemberEditorOwn = $this->createGroupMember( + $this->group, + array($this->groupRoleEditorOwn->name) + ); + } + + /** + * Having only one main test method could save a lot of time when possible + * because setUp() won't be run for every test methods. These test methods + * have been written to have no dependencies between each other and don't need + * a new set up. Each new methods added should follow these instructions. So + * these original test methods are prefixed by an underscore and are called + * manually. + * + * If you want to waste your time and have a lot of coffee to drink, you could + * still remove all underscore of these methods and remove this method :-) + */ + public function testMain() { + + $class_methods = get_class_methods($this); + foreach ($class_methods as $method) { + if (stripos($method, '_test') === 0) { + $this->$method(); + } + } + } + + /** + * Test node access switch from private to public + */ + public function _testNodeAccessPublicToPrivate() { + + $node = $this->drupalCreateNode(array( + 'type' => $this->nodeType->type, + 'uid' => $this->groupMemberAdmin->uid, + )); + $entity_uri = entity_uri('node', $node); + + $account = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($account); + + $this->drupalGet($entity_uri['path'], $entity_uri['options']); + $this->assertResponse(200, t('Node is public.')); + + $this->group->addEntity($node->nid, 'node', $node->type); + node_save($node); + + $this->drupalGet($entity_uri['path'], $entity_uri['options']); + $this->assertResponse(403, t('Node is private.')); + + $this->drupalLogout(); + } + + /** + * Test node access switch from private to public + */ + public function _testNodeAccessPrivateToPublic() { + + $node = $this->createGroupPrivateNode(); + $entity_uri = entity_uri('node', $node); + + $account = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($account); + $this->drupalGet($entity_uri['path'], $entity_uri['options']); + $this->assertResponse(403, t('Node is private.')); + + $this->group->removeEntity($node->nid, 'node'); + node_save($node); + + $this->drupalGet($entity_uri['path'], $entity_uri['options']); + $this->assertResponse(200, t('Node is public.')); + + $this->drupalLogout(); + } + + /** + * Test outsider access on public node created by a group member and also + * check that a public unpublished node could be view by the outsider author + * which have node permission 'view own unpublished content'. + */ + public function _testNodeAccessPublicViewAllow() { + + $node = $this->drupalCreateNode(array( + 'type' => $this->nodeType->type, + 'uid' => $this->groupMemberAdmin->uid, + )); + $entity_uri = entity_uri('node', $node); + + $account = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($account); + $this->drupalGet($entity_uri['path'], $entity_uri['options']); + $this->assertResponse(200, t('Outsider could access a public node.')); + $this->drupalLogout(); + + $account = $this->drupalCreateUser(array('access content', 'view own unpublished content')); + $node = $this->drupalCreateNode(array( + 'type' => $this->nodeType->type, + 'uid' => $account->uid, + 'status' => NODE_NOT_PUBLISHED, + )); + $entity_uri = entity_uri('node', $node); + + $this->drupalLogin($account); + $this->drupalGet($entity_uri['path'], $entity_uri['options']); + $this->assertResponse(200, t('Outsider with "view own unpublished content" could view its own unpublished public node.')); + $this->drupalLogout(); + } + + /** + * Test member access on a private node : + * - published by a member of the group + * - not published by a member which have node permission 'view unpublished node' + */ + public function _testNodeAccessPrivateViewAllow() { + + $account = $this->createGroupMember( + $this->group, + array($this->groupRoleEditorOwn->name), + array('access content', 'view own unpublished content') + ); + + $node = $this->createGroupPrivateNode(array('uid' => $account->uid)); + $entity_uri = entity_uri('node', $node); + + //Check node page with a member. + $this->drupalLogin($this->groupMemberEditorOwn); + $this->drupalGet($entity_uri['path'], $entity_uri['options']); + $this->assertResponse(200, t('Member could access a private node.')); + $this->drupalLogout(); + + //Unpublished the node and check access for the author + $node->status = NODE_NOT_PUBLISHED; + node_save($node); + $this->drupalLogin($account); + $this->drupalGet($entity_uri['path'], $entity_uri['options']); + $this->assertResponse(200, t('Member with node permission "view own unpublished content" could access its own unpublished private node.')); + $this->drupalLogout(); + } + + /** + * Test outsider access on a public node which did'nt have node permission + * 'access content'. + */ + public function _testNodeAccessPublicViewDenied() { + + $node = $this->drupalCreateNode(array( + 'type' => $this->nodeType->type, + 'status' => NODE_NOT_PUBLISHED, + )); + $entity_uri = entity_uri('node', $node); + + if ($this->loggedInUser) { + $this->drupalLogout(); + } + + $this->drupalGet($entity_uri['path'], $entity_uri['options']); + $this->assertResponse(403, t('Outsider could not view unpublished public node.')); + + $node->status = NODE_PUBLISHED; + node_save($node); + + //Remove 'access content' permission from anonymous role temporary. + user_role_revoke_permissions(DRUPAL_ANONYMOUS_RID, array('access content')); + + $this->drupalGet($entity_uri['path'], $entity_uri['options']); + $this->assertResponse(403, t('Outsider without access content node permission could not view a public node.')); + + //Reset default anonymous role permissions. + user_role_grant_permissions(DRUPAL_ANONYMOUS_RID, array('access content')); + } + + /** + * Test outsider access on a private node with : + * - an outsider which have access content permission + * - a member of an another group + * - a member of the group but node is unpublished + */ + public function _testNodeAccessPrivateViewDenied() { + + $node = $this->createGroupPrivateNode(); + $entity_uri = entity_uri('node', $node); + + $account = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($account); + $this->drupalGet($entity_uri['path'], $entity_uri['options']); + $this->assertResponse(403, t('Outsider could not view private node.')); + $this->drupalLogout(); + + //Create a new group and add a new member. + $group = $this->createGroup($this->groupType); + $group_member = $this->createGroupMember($group); + $this->drupalLogin($group_member); + $this->drupalGet($entity_uri['path'], $entity_uri['options']); + $this->assertResponse(403, t('Member of an another group could not view private node.')); + $this->drupalLogout(); + + //Unpublished the node + $node->status = NODE_NOT_PUBLISHED; + node_save($node); + $this->drupalLogin($this->groupMemberEditorAny); + $this->drupalGet($entity_uri['path'], $entity_uri['options']); + $this->assertResponse(403, t('Member of the group could not view unpublished private node.')); + $this->drupalLogout(); + } + + /** + * Test create node access for an outsider which have node permission. + */ + public function _testNodeAccessPublicCreateAllow() { + + $node_type = $this->nodeType->type; + $account = $this->drupalCreateUser(array("create $node_type content")); + $path = 'node/add/' . str_replace('_', '-', $node_type); + + $this->drupalLogin($account); + $this->drupalGet($path); + $this->assertResponse(200, t('Outsider with "create" node permission could create a node.')); + $this->drupalLogout(); + } + + /** + * Test create node access for a member which have : + * - not the node permission + * - the group permission to create this node content type + */ + public function _testNodeAccessPrivateCreateAllow() { + + $this->drupalLogin($this->groupMemberEditorOwn); + $path = 'node/add/' . str_replace('_', '-', $this->nodeType->type); + $this->drupalGet($path); + $this->assertResponse(200, t('Member with "create" group permission and without node permission "create" could create a node.')); + $this->drupalLogout(); + } + + /** + * Test create node access for an outsider which didn't have node permission. + */ + public function _testNodeAccessPublicCreateDenied() { + + $account = $this->drupalCreateUser(); + $this->drupalLogin($account); + $path = 'node/add/' . str_replace('_', '-', $this->nodeType->type); + $this->drupalGet($path); + $this->assertResponse(403, t('Outsider without "create" node permission could not create a node.')); + $this->drupalLogout(); + } + + /** + * Test create node access for a member which didn't have both permissions + * "create" from node permission and group node permission. + */ + public function _testNodeAccessPrivateCreateDenied() { + + $member = $this->createGroupMember($this->group); + $this->drupalLogin($member); + $path = 'node/add/' . str_replace('_', '-', $this->nodeType->type); + $this->drupalGet($path); + $this->assertResponse(403, t('Member without both "create" node and group permissions could not create a node.')); + $this->drupalLogout(); + } + + /** + * Test update node access on a public node created and update by an outsider + * author which has node module permission 'edit own'. + */ + public function _testNodeAccessPublicUpdateOwnAllow() { + + $node_type = $this->nodeType->type; + + $account = $this->drupalCreateUser(array("edit own $node_type content")); + + $node = $this->drupalCreateNode(array( + 'type' => $node_type, + 'uid' => $account->uid, + )); + + $this->drupalLogin($account); + $this->drupalGet('node/' . $node->nid. '/edit'); + $this->assertResponse(200, t('Outsider with node permission "edit own" could edit its own public node.')); + $this->drupalLogout(); + } + + /** + * Test update node access on a private node by the author which have the + * group permission 'edit own'. + */ + public function _testNodeAccessPrivateUpdateOwnAllow() { + + $this->drupalLogin($this->groupMemberEditorOwn); + $node = $this->createGroupPrivateNode(array('uid' => $this->groupMemberEditorOwn->uid)); + + $this->drupalGet('node/' . $node->nid. '/edit'); + $this->assertResponse(200, t('Member with "edit own" group permission could edit its own private node.')); + $this->drupalLogout(); + } + + /** + * Test update node access on a public node by an outsider which don't have + * node permission. + */ + public function _testNodeAccessPublicUpdateOwnDenied() { + + $account = $this->drupalCreateUser(); + + $node = $this->drupalCreateNode(array( + 'type' => $this->nodeType->type, + 'uid' => $account->uid, + )); + + $this->drupalLogin($account); + $this->drupalGet('node/' . $node->nid. '/edit'); + $this->assertResponse(403, t('Outsider without node permission "edit own" could not edit its own public node.')); + $this->drupalLogout(); + } + + /** + * Test update node access on a private node by the private author which don't + * have both 'edit own' from group permission and node permission. + */ + public function _testNodeAccessPrivateUpdateOwnDenied() { + + //Create a new member without any role. + $author_member = $this->createGroupMember($this->group); + $this->drupalLogin($author_member); + $node = $this->createGroupPrivateNode(array('uid' => $author_member->uid)); + + $path = 'node/' . $node->nid . '/edit'; + + $this->drupalGet($path); + $this->assertResponse(403, t('Author without both "edit own" group and node permissions could not edit its own private node.')); + $this->drupalLogout(); + } + + /** + * Test update access for an outsider which have node permission 'edit any' + * and is : + * - the author + * - not the author + */ + public function _testNodeAccessPublicUpdateAnyAllow() { + + $node_type = $this->nodeType->type; + + $account = $this->drupalCreateUser(array("edit any $node_type content")); + $this->drupalLogin($account); + + $node = $this->drupalCreateNode(array( + 'type' => $node_type, + 'uid' => $this->groupMemberAdmin->uid, + )); + $this->drupalGet('node/' . $node->nid. '/edit'); + $this->assertResponse(200, t('Outsider with node permission "edit any" only could edit any public node.')); + + $node = $this->drupalCreateNode(array( + 'type' => $node_type, + 'uid' => $account->uid, + )); + $this->drupalGet('node/' . $node->nid. '/edit'); + $this->assertResponse(200, t('Outsider with node permission "edit any" only could also edit its own public node.')); + + $this->drupalLogout(); + } + + /** + * Test update node access on a private node by a member which has only group + * permission 'edit any' and is : + * - the author + * - not the author + */ + public function _testNodeAccessPrivateUpdateAnyAllow() { + + $this->drupalLogin($this->groupMemberEditorAny); + + $node = $this->createGroupPrivateNode(); + $this->drupalGet('node/' . $node->nid. '/edit'); + $this->assertResponse(200, t('Member with "edit any" group permission could edit any private node.')); + + $node = $this->createGroupPrivateNode(array('uid' => $this->groupMemberEditorAny->uid)); + $this->drupalGet('node/' . $node->nid. '/edit'); + $this->assertResponse(200, t('Member with "edit any" group permission only could also edit its own private node.')); + + $this->drupalLogout(); + } + + /** + * Test update node access on public node by : + * - an outsider not the author which have not node permission 'edit any'. + * - a member of a group which have only group permission 'edit any'. + */ + public function _testNodeAccessPublicUpdateAnyDenied() { + + $node = $this->drupalCreateNode(array('type' => $this->nodeType->type)); + $path = 'node/' . $node->nid . '/edit'; + + $account = $this->drupalCreateUser(); + $this->drupalLogin($account); + $this->drupalGet($path); + $this->assertResponse(403, t('Outsider without "edit any" node permission could not edit public node.')); + $this->drupalLogout(); + + $this->drupalLogin($this->groupMemberEditorAny); + $this->drupalGet($path); + $this->assertResponse(403, t('A member with "edit any" group permission and without "edit any" node permission could not edit a public node.')); + $this->drupalLogout(); + } + + /** + * Test update node access on a private node by : + * - an outsider which has node permission 'edit any'. + * - a member of the group which didn't have the "edit any" group permission. + * - a member of an another group which has node permission 'edit any'. + */ + public function _testNodeAccessPrivateUpdateAnyDenied() { + + //Create a node private with the group admin + $node = $this->createGroupPrivateNode(); + $path = 'node/' . $node->nid . '/edit'; + + //Create an outsider with 'edit any' node permission. + $account = $this->drupalCreateUser(array("edit any $node->type content")); + $this->drupalLogin($account); + $this->drupalGet($path); + $this->assertResponse(403, t('Outsider with "edit any" node permission can\'t edit the private node.')); + $this->drupalLogout(); + + $this->drupalLogin($this->groupMemberEditorOwn); + $this->drupalGet($path); + $this->assertResponse(403, t('Member of the group without "edit any" group permission can\'t edit the private node.')); + $this->drupalLogout(); + + //Create a member of another group with group permission 'edit any'. + $group = $this->createGroup($this->groupType); + $member = $this->createGroupMember($group, array($this->groupRoleEditorAny->name)); + $this->drupalLogin($member); + $this->drupalGet($path); + $this->assertResponse(403, t('Member of another group with "edit any" group permission can\'t edit the private node.')); + $this->drupalLogout(); + } + + /** + * Test delete node access on a public node by an outsider author which have + * permission 'delete own'. + */ + public function _testNodeAccessPublicDeleteOwnAllow() { + + $node_type = $this->nodeType->type; + + $account = $this->drupalCreateUser(array("delete own $node_type content")); + + $node = $this->drupalCreateNode(array( + 'type' => $node_type, + 'uid' => $account->uid, + )); + $path = 'node/' . $node->nid . '/delete'; + + $this->drupalLogin($account); + $this->drupalGet($path); + $this->assertResponse(200, t('Outsider with "delete own" node permission can delete its own public node.')); + $this->drupalLogout(); + } + + /** + * Test delete node access on a private node by the author which has the group + * permission 'delete own'. + */ + public function _testNodeAccessPrivateDeleteOwnAllow() { + + $this->drupalLogin($this->groupMemberEditorOwn); + $node = $this->createGroupPrivateNode(array('uid' => $this->groupMemberEditorOwn->uid)); + $path = 'node/' . $node->nid . '/delete'; + + $this->drupalGet($path); + $this->assertResponse(200, t('Member owner of the private node and with group permission "delete own" could delete its own private node.')); + $this->drupalLogout(); + } + + /** + * Test delete node access on a public node by an outsider author which don't + * have node permission 'delete own'. + */ + public function _testNodeAccessPublicDeleteOwnDenied() { + + $account = $this->drupalCreateUser(); + + $node = $this->drupalCreateNode(array( + 'type' => $this->nodeType->type, + 'uid' => $account->uid, + )); + + $this->drupalLogin($account); + $this->drupalGet('node/' . $node->nid . '/delete'); + $this->assertResponse(403, t('Outsider without node permission "delete own" could not delete its own content.')); + $this->drupalLogout(); + } + + /** + * Test delete node access on a private node by the member author of the node + * which have not the group permission 'delete own'. + */ + public function _testNodeAccessPrivateDeleteOwnDenied() { + + //Create a new member without any role. + $member = $this->createGroupMember($this->group); + $this->drupalLogin($member); + $node = $this->createGroupPrivateNode(array('uid' => $member->uid)); + + $this->drupalGet('node/' . $node->nid . '/delete'); + $this->assertResponse(403, t('Author without "delete own" group permission can\'t delete its own private node.')); + $this->drupalLogout(); + } + + /** + * Test delete node access on a public node by an outsider which have the node + * permission 'delete any' and is : + * - the author of the node + * - not the author of the node + */ + public function _testNodeAccessPublicDeleteAnyAllow() { + + $node_type = $this->nodeType->type; + + $account = $this->drupalCreateUser(array("delete any $node_type content")); + $this->drupalLogin($account); + + $node = $this->drupalCreateNode(array( + 'type' => $node_type, + 'uid' => $this->groupMemberAdmin->uid, + )); + $this->drupalGet('node/' . $node->nid . '/delete'); + $this->assertResponse(200, t('Outsider with "delete any" node permission could delete the public node.')); + + $node = $this->drupalCreateNode(array( + 'type' => $node_type, + 'uid' => $account->uid, + )); + $this->drupalGet('node/' . $node->nid . '/delete'); + $this->assertResponse(200, t('Outsider with "delete any" node permission only could also delete its own public node.')); + + $this->drupalLogout(); + } + + /** + * Test delete node access on a private node by a member which has + * the 'delete any' group permission and is : + * - the author of the node + * - not the author of the node + */ + public function _testNodeAccessPrivateDeleteAnyAllow() { + + $this->drupalLogin($this->groupMemberEditorAny); + + $node = $this->createGroupPrivateNode(); + $this->drupalGet('node/' . $node->nid . '/delete'); + $this->assertResponse(200, t('Member with group permission "delete any" could delete any private node.')); + + $node = $this->createGroupPrivateNode(array('uid' => $this->groupMemberEditorAny->uid)); + $this->drupalGet('node/' . $node->nid . '/delete'); + $this->assertResponse(200, t('Member with group permission "delete any" only could also delete its own private node.')); + + $this->drupalLogout(); + } + + /** + * Test delete node access on a public node by : + * - an outsider which don't have node permission 'delete any'. + * - a member which have the group permission 'delete any' but don't have the + * node permission 'delete any'. + */ + public function _testNodeAccessPublicDeleteAnyDenied() { + + $node = $this->drupalCreateNode(array( + 'type' => $this->nodeType->type, + )); + $path = 'node/' . $node->nid . '/delete'; + + $account = $this->drupalCreateUser(); + $this->drupalLogin($account); + $this->drupalGet($path); + $this->assertResponse(403, t('An outsider without "delete any" node permission could not delete a public node.')); + $this->drupalLogout(); + + $this->drupalLogin($this->groupMemberEditorAny); + $this->drupalGet($path); + $this->assertResponse(403, t('A member with "delete any" group permission and without "delete any" node permission could not delete a public node.')); + $this->drupalLogout(); + } + + /** + * Test delete node access on a private node by : + * - an outsider which has node permission 'delete any'. + * - a member of the group which don't have the group permission 'delete any'. + * - a member of an another group which has group permission 'delete any'. + */ + public function _testNodeAccessPrivateDeleteAnyDenied() { + + //Create a node private with the group admin + $node = $this->createGroupPrivateNode(); + $path = 'node/' . $node->nid . '/delete'; + + //Create an outsider with 'edit any' node permission. + $account = $this->drupalCreateUser(array("delete any $node->type content")); + $this->drupalLogin($account); + $this->drupalGet($path); + $this->assertResponse(403, t('Outsider with "delete any" node permission can\'t delete the private node.')); + $this->drupalLogout(); + + $this->drupalLogin($this->groupMemberEditorOwn); + $this->drupalGet($path); + $this->assertResponse(403, t('Member of the group without "delete any" group permission can\'t delete the private node.')); + $this->drupalLogout(); + + //Create a member of another group with group permission 'edit any'. + $group = $this->createGroup($this->groupType); + $member = $this->createGroupMember($group, array($this->groupRoleEditorAny->name)); + $this->drupalLogin($member); + $this->drupalGet($path); + $this->assertResponse(403, t('Member of another group with "delete any" group permission can\'t delete the private node.')); + $this->drupalLogout(); + } + + /** + * Internal method to create a private node for the default group. + * + * @param $node_settings + * A settings array as expected by ::drupalCreateNode(). + * + * @return + * A node object created. + */ + protected function createGroupPrivateNode(array $node_settings = array()) { + + $node_settings += array( + 'type' => $this->nodeType->type, + 'uid' => $this->groupMemberAdmin->uid, + ); + $node = $this->drupalCreateNode($node_settings); + + //Add node group entity and update it to save node access records. + $this->group->addEntity($node->nid, 'node', $node->type); + node_save($node); + return $node; + } + +} diff --git a/tests/group_node_access_ui.test b/tests/group_node_access_ui.test new file mode 100644 index 0000000..8116b94 --- /dev/null +++ b/tests/group_node_access_ui.test @@ -0,0 +1,368 @@ + 'Group node access UI', + 'description' => 'Run tests for group node access UI', + 'group' => 'Group', + ); + } + + /** + * Override parent::setUp() + */ + public function setUp() { + + //Call parent::setUp() and set modules dependencies. + parent::setUp('entity', 'group'); + + //Rebuild node access + node_access_rebuild(FALSE); + + //Create a default group type + $this->groupType = $this->createGroupType(); + + //Create a default group + $this->group = $this->createGroup($this->groupType); + + //Create a node type + $this->nodeType = $this->drupalCreateContentType(); + $node_type = $this->nodeType->type; + variable_set('node_options_' . $node_type, array('status')); + + //Create a default admin member, reload default group roles to get content + //type group permissions. + entity_defaults_rebuild(); + $this->groupMemberAdmin = $this->createGroupMember($this->group, array('group_admin'), array("edit any $node_type content")); + } + + /** + * @see GroupNodeAccessTestCase::testMain() + */ + public function testMain() { + + $class_methods = get_class_methods($this); + foreach ($class_methods as $method) { + if (stripos($method, '_test') === 0) { + $this->$method(); + } + } + } + + /** + * Test : + * - field exists for member and default to public when creating. + * - create a public node by selecting no group option. + * - outsider could view the public node previously created. + */ + protected function _testCreatePublicNode() { + + $this->drupalLogin($this->groupMemberAdmin); + + //Check that node edit form have the group extra field. + $path = 'node/add/' . str_replace('_', '-', $this->nodeType->type); + $this->drupalGet($path); + $this->assertFieldByName(GROUP_EXTRA_FIELD_GROUP, 0, t('Field group exists and set to default.')); + + //Prepare node form and submit it. + $edit = array(); + $edit['title'] = $this->randomString(); + $this->drupalPost(NULL, $edit, t('Save')); + + //Retrieve nid from url after redirect. + $url = $this->getUrl(); + $url_parts = explode('/', $url); + $nid = array_pop($url_parts); + + //Check that anonymous could view public node. + $account = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($account); + $path = 'node/' . $nid; + $this->drupalGet($path); + $this->assertResponse(200, t('Anonymous user could view public node.')); + $this->drupalLogout(); + } + + /** + * Test : + * - a member could create a private node by selecting one of its groups + * - outsider could not view the private node + * - member of another group could not view the private node + */ + protected function _testCreatePrivateNode() { + + $this->drupalLogin($this->groupMemberAdmin); + + $edit = array(); + $edit['title'] = $this->randomString(); + $edit[GROUP_EXTRA_FIELD_GROUP] = $this->group->gid; + + $path = 'node/add/' . str_replace('_', '-', $this->nodeType->type); + $this->drupalPost($path, $edit, t('Save')); + + //Retrieve nid from url after redirect. + $url = $this->getUrl(); + $url_parts = explode('/', $url); + $nid = array_pop($url_parts); + + //Check that anonymous could not view private node. + $path = 'node/' . $nid; + $account = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($account); + $this->drupalGet($path); + $this->assertResponse(403, t('Anonymous user could not view private node.')); + $this->drupalLogout(); + + //Check that member of another group could not access private node too. + $group_other = $this->createGroup($this->groupType); + $member = $this->createGroupMember($group_other); + $this->drupalLogin($member); + $this->drupalGet($path); + $this->assertResponse(403, t('Member of another group could not view private node.')); + $this->drupalLogout(); + } + + /** + * Test that all groups of the user editor are available in the field options. + */ + protected function _testCreatePrivateNodeFieldOptions() { + + //Create an another group and create a membership for group admin account. + $group = $this->createGroup($this->groupType); + $group->addMember($this->groupMemberAdmin->uid); + + $this->drupalLogin($this->groupMemberAdmin); + + $path = 'node/add/' . str_replace('_', '-', $this->nodeType->type); + $this->drupalGet($path); + + //Check that select element have all groups of the member as options + $found = FALSE; + $xpath = $this->constructFieldXpath('name', GROUP_EXTRA_FIELD_GROUP); + $fields = $this->xpath($xpath); + if ($fields) { + //Iterate over each potential fields with the same name. + foreach ($fields as $field) { + if (isset($field->option)) { + $items = $this->getAllOptions($field); + if ($items) { + $values = array(); + foreach ($items as $item) { + $values[] = (int)$item['value']; + } + //Check that all groups of the user are exposed + if (count(array_intersect(array($this->group->gid, $group->gid), $values)) === 2) { + $found = TRUE; + break; + } + } + } + } + } + $this->assertTrue($found, t('Field group exists and all groups of the user are available.')); + + $this->drupalLogout(); + + //Reset admin group membership. + $group->removeMember($this->groupMemberAdmin->uid); + } + + /** + * Test that updated public node have still a field group value to no group. + */ + protected function _testUpdatePublicNode() { + + $node = $this->drupalCreateNode(array('type' => $this->nodeType->type)); + + $this->drupalLogin($this->groupMemberAdmin); + + $this->drupalGet('node/' . $node->nid . '/edit'); + $this->assertFieldByName(GROUP_EXTRA_FIELD_GROUP, 0, t('Field group exists and have still no value.')); + $this->drupalLogout(); + } + + /** + * Test that updated private node have still a field group with group options + * selected. + */ + protected function _testUpdatePrivateNode() { + + $node = $this->drupalCreateNode(array( + 'type' => $this->nodeType->type, + )); + $this->group->addEntity($node->nid, 'node', $node->type); + + $this->drupalLogin($this->groupMemberAdmin); + + $this->drupalGet('node/' . $node->nid . '/edit'); + $this->assertFieldByName(GROUP_EXTRA_FIELD_GROUP, $this->group->gid, t('Field group exists and have the group previously selected.')); + + $this->drupalLogout(); + } + + /** + * Test that field group doesn't exists if user editor have no group. + */ + protected function _testUpdatePrivateNodeNoGroupNoField() { + + //Create a user with bypass access content and create + $node_type = $this->nodeType->type; + $account = $this->drupalCreateUser(array('bypass node access', "create $node_type content")); + + $this->drupalLogin($account); + + $path = 'node/add/' . str_replace('_', '-', $this->nodeType->type); + $this->drupalGet($path); + $this->assertNoFieldByName(GROUP_EXTRA_FIELD_GROUP, NULL, t('Field group not exists because current user have no group.')); + $this->drupalLogout(); + } + + /** + * Test disabled field group if user editor have : + * - bypass content permission + * - group memberships but no the same as the node + */ + protected function _testUpdatePrivateNodeFieldDisabled() { + + $node_type = $this->nodeType->type; + + //Create a new group and a new member with some node permissions. + $group = $this->createGroup($this->groupType); + $member = $this->createGroupMember($group, array(), array('bypass node access', "create $node_type content")); + + //Create a private node of another group and by another author. + $node = $this->drupalCreateNode(array( + 'type' => $node_type, + 'uid' => $this->groupMemberAdmin->uid, + )); + $this->group->addEntity($node->nid, 'node', $node->type); + + //Check that field is disabled. + $this->drupalLogin($member); + + $this->drupalGet('node/' . $node->nid . '/edit'); + + $found = FALSE; + $xpath = $this->constructFieldXpath('name', GROUP_EXTRA_FIELD_GROUP); + $fields = $this->xpath($xpath); + if ($fields) { + //Iterate over each potential fields with the same name. + foreach ($fields as $field) { + if (isset($field->option) && !empty($field['disabled']) && (string)$field['disabled'] === 'disabled') { + $found = TRUE; + } + } + } + $this->assertTrue($found, t('Field group found and disabled.')); + + $this->drupalLogout(); + } + + /** + * Test switch accessibility from public to private + */ + protected function _testUpdatePublicNodeToPrivate () { + + //Create a public node + $node = $this->drupalCreateNode(array('type' => $this->nodeType->type)); + + $this->drupalLogin($this->groupMemberAdmin); + + //Switch node to be private. + $edit = array(); + $edit[GROUP_EXTRA_FIELD_GROUP] = $this->group->gid; + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + $this->drupalLogout(); + + $path = 'node/' . $node->nid; + + //Create a new member to check access + $member = $this->createGroupMember($this->group); + $this->drupalLogin($member); + $this->drupalGet($path); + $this->assertResponse(200, t('Member of the group could view the private node.')); + $this->drupalLogout(); + + //Create a member of another group + $group = $this->createGroup($this->groupType); + $member = $this->createGroupMember($group); + $this->drupalLogin($member); + $this->drupalGet($path); + $this->assertResponse(403, t('Member of another group could not view the private node.')); + $this->drupalLogout(); + + //Create an outsider + $account = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($account); + $this->drupalGet($path); + $this->assertResponse(403, t('Outsider could not view the private node.')); + $this->drupalLogout(); + } + + /** + * Test switch accessibility from private to public + */ + protected function _testUpdatePrivateNodeToPublic () { + + //Create a private node. + $node = $this->drupalCreateNode(array( + 'type' => $this->nodeType->type, + 'uid' => $this->groupMemberAdmin->uid, + )); + $this->group->addEntity($node->nid, 'node', $node->type); + + $this->drupalLogin($this->groupMemberAdmin); + + //Switch node to be public. + $edit = array(); + $edit[GROUP_EXTRA_FIELD_GROUP] = 0; + $this->drupalPost('node/' . $node->nid . '/edit', $edit, t('Save')); + //Check response code of redirect + $this->assertResponse(200, t('Member could view public node')); + $this->drupalLogout(); + + //Check access with an outsider. + $account = $this->drupalCreateUser(array('access content')); + $this->drupalLogin($account); + $this->drupalGet('node/' . $node->nid); + $this->assertResponse(200, t('Outsider could view public node')); + $this->drupalLogout(); + } +}