Index: project.api.php =================================================================== RCS file: project.api.php diff -N project.api.php --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ project.api.php 17 Aug 2010 16:11:50 -0000 @@ -0,0 +1,115 @@ + array( + 'title' => t('Name of this permission'), + 'description' => t('Description of what this this permission allows project maintainers to do.'), + ), + ); +} + +/** + * Alter hook for per-project permissions supported by a module. + * + * @param $permissions + * Reference to an array of all the permissions defined via + * hook_project_permission_info(). + * + * @see hook_project_permission_info(). + * @see project_permission_load(). + * @see drupal_alter() + */ +function hook_project_permission_alter(&$permissions) { + // I can't yet fathom why we need an alter hook here, but we might need it + // and it was free to include it, so why not? ;) +} + +/** + * Invoked whenever a project maintainer is added or updated. + * + * This gives any modules that are providing their own per-project permissions + * a chance to store the data about a maintainer's permissions whenever the + * record for that maintainer is being saved. + * + * @param $nid + * The Project NID to save the maintainer information for. + * @param $uid + * The user ID of the maintainer to save. + * @param array $permissions + * Associative array of which project-level permissions the maintainer + * should have. The keys are permission names, and the values are if the + * permission should be granted or not. + * + * @see hook_project_permission_info() + */ +function hook_project_maintainer_save($nid, $uid, $permissions) { + // Try to update an existing record for this maintainer for our permission. + db_query("UPDATE {example_project_maintainer} SET some_project_permission = %d WHERE nid = %d AND uid = %d", !empty($permissions['some project permission']), $nid, $uid); + if (!db_affected_rows()) { + // If we didn't have a record to update, add this as a new maintainer. + db_query("INSERT INTO {example_project_maintainer} (nid, uid, some_project_permission) VALUES (%d, %d, %d)", $nid, $uid, !empty($permissions['some project permission'])); + } +} + +/** + * Invoked whenever a maintainer is removed from a given project. + * + * @param $nid + * The Project NID to remove the maintainer from. + * @param $uid + * The user ID of the maintainer to remove. + * + * @see project_maintainer_remove() + */ +function hook_project_maintainer_remove($nid, $uid) { + db_query("DELETE FROM {example_project_maintainer} WHERE nid = %d and uid = %d", $nid, $uid); +} + +/** + * Populate the maintainer information for a given project. + * + * Whenever a project node is being loaded, this hook is invoked to give any + * modules providing per-project permissions a chance to update the maintainer + * array. This array is stored in the project as $node->project['maintainers']. + * + * The maintainers array is keyed by the UID of each maintainer. Each value is + * itself a nested array of information about the maintainer. These arrays + * have the keys 'name' for the username and 'permissions', which is an array + * of per-project permissions associated with the maintainer. This + * 'permissions' subarray is keyed by permission name, and the values are 0 or + * 1 to indicate if the maintainer should have access to that permission. + * + * @param $nid + * The Project NID to populate maintainer information about. + * @param $maintainers + * Reference to a nested array of maintainers. + */ +function hook_project_maintainer_project_load($nid, &$maintainers) { + $query = db_query('SELECT u.uid, u.name, epm.some_project_permission FROM {example_project_maintainer} epm INNER JOIN {users} u ON epm.uid = u.uid WHERE epm.nid = %d', $nid); + while ($maintainer = db_fetch_object($query)) { + if (empty($maintainers[$maintainer->uid])) { + $maintainers[$maintainer->uid]['name'] = $maintainer->name; + } + $maintainers[$maintainer->uid]['permissions']['some project permission'] = $maintainer->some_project_permission; + } +} Index: project.inc =================================================================== RCS file: /Users/wright/drupal/local_repo/contributions/modules/project/project.inc,v retrieving revision 1.150 diff -u -p -r1.150 project.inc --- project.inc 30 Jul 2010 21:59:28 -0000 1.150 +++ project.inc 17 Aug 2010 16:11:50 -0000 @@ -425,11 +425,15 @@ function project_project_nodeapi(&$node, function project_project_insert($node) { db_query("INSERT INTO {project_projects} (nid, uri, homepage, changelog, cvs, demo, screenshots, documentation, license) VALUES (%d, '%s', '%s', '%s', '%s', '%s', '%s', '%s', '%s')", $node->nid, $node->project['uri'], $node->project['homepage'], $node->project['changelog'], $node->project['cvs'], $node->project['demo'], $node->project['screenshots'], $node->project['documentation'], $node->project['license']); // project_release_scan_directory($node->project['uri']); + $perms = array_fill_keys(array_keys(project_permission_load()), 1); + project_maintainer_save($node->nid, $node->uid, $perms); } function project_project_update($node) { db_query("UPDATE {project_projects} SET uri = '%s', homepage = '%s', changelog = '%s', cvs = '%s', demo = '%s', screenshots = '%s', documentation = '%s', license = '%s' WHERE nid = %d", $node->project['uri'], $node->project['homepage'], $node->project['changelog'], $node->project['cvs'], $node->project['demo'], $node->project['screenshots'], $node->project['documentation'], $node->project['license'], $node->nid); // project_release_scan_directory($node->project['uri']); + $perms = array_fill_keys(array_keys(project_permission_load()), 1); + project_maintainer_save($node->nid, $node->uid, $perms); } function project_project_delete($node) { Index: project.install =================================================================== RCS file: /Users/wright/drupal/local_repo/contributions/modules/project/project.install,v retrieving revision 1.28 diff -u -p -r1.28 project.install --- project.install 22 Apr 2010 06:10:24 -0000 1.28 +++ project.install 17 Aug 2010 16:11:50 -0000 @@ -104,6 +104,41 @@ function project_schema() { 'project_projects_uri' => array(array('uri', 8)), ), ); + $schema['project_maintainer'] = array( + 'description' => t('Users who have various per-project maintainer permissions.'), + 'fields' => array( + 'nid' => array( + 'description' => t('Foreign key: {project_projects}.nid of the project.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'uid' => array( + 'description' => t('Foreign key: {users}.uid of a user with any project maintainer permissions.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'administer_settings' => array( + 'description' => t('Can this user edit the given project and modify its settings.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'administer_maintainers' => array( + 'description' => t('Can this user manipulate the maintainers for the given project.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('nid', 'uid'), + ); + return $schema; } @@ -137,3 +172,58 @@ function project_update_6001() { return $ret; } +/** + * Add the {project_maintainer} table. + */ +function project_update_6002() { + $ret = array(); + $table = array( + 'description' => t('Users who have various per-project maintainer permissions.'), + 'fields' => array( + 'nid' => array( + 'description' => t('Foreign key: {project_projects}.nid of the project.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'uid' => array( + 'description' => t('Foreign key: {users}.uid of a user with any project maintainer permissions.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'administer_settings' => array( + 'description' => t('Can this user edit the given project and modify its settings.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'administer_maintainers' => array( + 'description' => t('Can this user manipulate the maintainers for the given project.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('nid', 'uid'), + ); + db_create_table($ret, 'project_maintainer', $table); + + // Initially populate the table so that every project owner has full + // powers on their own projects. + $ret[] = update_sql("INSERT INTO {project_maintainer} (nid, uid, administer_settings, administer_maintainers) SELECT nid, uid, 1, 1 FROM {node} WHERE type = 'project_project'"); + + // If CVS module is enabled, also populate the table from the + // {cvs_project_maintainers} table so that anyone with CVS access + // who is not the project owner can administer the project but not + // manipulate the per-project permissions. + if (module_exists('cvs')) { + $ret[] = update_sql("INSERT INTO {project_maintainer} (nid, uid, administer_settings, administer_maintainers) SELECT cpm.nid, cpm.uid, 1, 0 FROM {cvs_project_maintainers} cpm INNER JOIN {node} n ON cpm.nid = n.nid WHERE cpm.uid != n.uid"); + } + + return $ret; +} Index: project.module =================================================================== RCS file: /Users/wright/drupal/local_repo/contributions/modules/project/project.module,v retrieving revision 1.361 diff -u -p -r1.361 project.module --- project.module 23 Jul 2010 04:12:12 -0000 1.361 +++ project.module 17 Aug 2010 16:11:50 -0000 @@ -518,6 +518,25 @@ function project_menu() { 'type' => MENU_NORMAL_ITEM, ); + $items['node/%project_node/maintainers'] = array( + 'title' => 'Maintainers', + 'page callback' => 'drupal_get_form', + 'page arguments' => array('project_maintainers_form', 1), + 'access callback' => 'project_user_access', + 'access arguments' => array(1, 'administer maintainers'), + 'file' => 'includes/project_maintainers.inc', + 'type' => MENU_LOCAL_TASK, + 'weight' => 4, + ); + $items['node/%project_node/maintainers/delete/%user'] = array( + 'page callback' => 'drupal_get_form', + 'page arguments' => array('project_maintainer_delete_confirm', 1, 4), + 'access callback' => 'project_user_access', + 'access arguments' => array(1, 'administer maintainers'), + 'file' => 'includes/project_maintainers.inc', + 'type' => MENU_CALLBACK, + ); + $items['node/%project_edit_project/edit/project'] = array( 'title' => 'Project', 'page callback' => 'node_page', @@ -567,7 +586,16 @@ function project_edit_project_load($nid) return project_node_load($nid); } -function project_check_admin_access($project, $cvs_access = NULL) { +/** + * See if the current user has the given permission on a given project. + * + * @param $project + * The project to check access against. Can be either a numeric node ID + * (nid) or a fully-loaded $node object. + * @param $permission + * The string representing the permission to check access for. + */ +function project_user_access($project, $permission) { global $user; if (empty($user->uid)) { return FALSE; @@ -578,31 +606,22 @@ function project_check_admin_access($pro return FALSE; } + // If the user has the site-wide admin permission, always grant access. if (user_access('administer projects')) { return TRUE; } - // If $cvs_access is not defined, check to make sure the user has cvs access - // and that the user's cvs account is approved. - if (project_use_cvs($project_obj) && !isset($cvs_access)) { - if (db_result(db_query("SELECT COUNT(*) FROM {cvs_accounts} WHERE uid = %d AND status = %d", $user->uid, CVS_APPROVED))) { - $cvs_access = TRUE; - } - else { - $cvs_access = FALSE; - } - } - if (user_access('maintain projects')) { + // Project owners are treated as super users and can always access. if ($user->uid == $project_obj->uid) { return TRUE; } - if (project_use_cvs($project_obj) && $cvs_access) { - if (db_result(db_query("SELECT COUNT(*) FROM {cvs_project_maintainers} WHERE uid = %d AND nid = %d", $user->uid, $project_obj->nid))) { - return TRUE; - } - } + + // Otherwise, see if the user has the right permission for this project. + return !empty($project_obj->project['maintainers'][$user->uid]['permissions'][$permission]); } + + // If we haven't granted access yet, deny it. return FALSE; } @@ -693,11 +712,11 @@ function project_project_access($op, $no switch ($op) { case 'view': // Since this function is shared for project_release nodes, we have to - // be careful what node we pass to project_check_admin_access(). + // be careful what node we pass to project_user_access(). if ($node->type == 'project_release') { $node = node_load($node->project_release['pid']); } - if (project_check_admin_access($node)) { + if (project_user_access($node, 'administer settings')) { return TRUE; } if (!user_access('access projects')) { @@ -717,7 +736,7 @@ function project_project_access($op, $no } break; case 'update': - if (project_check_admin_access($node)) { + if (project_user_access($node, 'administer settings')) { return TRUE; } break; @@ -730,12 +749,136 @@ function project_project_access($op, $no } /** + * Load all per-project permission information and return it. + * + * This invokes hook_project_permission_info() and + * hook_project_permission_alter(), and caches the results in RAM. + * + * @see hook_project_permission_info() + * @see hook_project_permission_alter() + * @see drupal_alter() + */ +function project_permission_load() { + static $project_permissions = array(); + if (empty($project_permissions)) { + $project_permissions = module_invoke_all('project_permission_info'); + drupal_alter('project_permission', $project_permissions); + } + return $project_permissions; +} + +/** + * Implement hook_project_permission_info() + */ +function project_project_permission_info() { + return array( + 'administer settings' => array( + 'title' => t('Administer settings'), + 'description' => t('Allows a user to edit a project and modify its settings.'), + ), + 'administer maintainers' => array( + 'title' => t('Administer maintainers'), + 'description' => t('Allows a user to add and remove other project maintainers and to modify their permissions.'), + ), + ); +} + +/** + * Save the permissions associated with a maintainer for a given project. + * + * This creates a new maintainer record if none currently exists. Furthermore, + * it invokes hook_project_maintainer_save() to give other modules a chance to + * act on the fact that a maintainer is being saved. + * + * @param $nid + * The Project NID to update the maintainer for. + * @param $uid + * The user ID of the maintainer to update. + * @param array $permissions + * Associative array of which project-level permissions the maintainer + * should have. The keys are permission names, and the values are if the + * permission should be granted or not. + * + * @see hook_project_maintainer_save() + * @see hook_project_permission_info() + */ +function project_maintainer_save($nid, $uid, $permissions = array()) { + // Try to update an existing record, if any. + db_query("UPDATE {project_maintainer} SET administer_settings = %d, administer_maintainers = %d WHERE nid = %d AND uid = %d", !empty($permissions['administer settings']), !empty($permissions['administer maintainers']), $nid, $uid); + if (!db_affected_rows()) { + // Didn't update anything, add this as a new maintainer, instead. + db_query("INSERT INTO {project_maintainer} (nid, uid, administer_settings, administer_maintainers) VALUES (%d, %d, %d, %d)", $nid, $uid, !empty($permissions['administer settings']), !empty($permissions['administer maintainers'])); + } + + // Invoke hook_project_maintainer_save() to let other modules know this + // maintainer is being saved so they can take any actions or record any + // data they need to. + module_invoke_all('project_maintainer_save', $nid, $uid, $permissions); +} + +/** + * Remove a maintainer from a given project. + * + * @param $nid + * The Project NID to remove the maintainer from. + * @param $uid + * The user ID of the maintainer to remove. + */ +function project_maintainer_remove($nid, $uid) { + db_query("DELETE FROM {project_maintainer} WHERE nid = %d and uid = %d", $nid, $uid); + + // Invoke hook_project_maintainer_remove() to let other modules know this + // maintainer is being removed so they can take any actions or record any + // data they need to. + module_invoke_all('project_maintainer_remove', $nid, $uid); +} + +/** + * Load all the per-project maintainer info for a given project. + * + * @param $nid + * Node ID of the project to load maintainer info about. + * + * @return + * Array of maintainer info for the given project. + * + * @see hook_project_maintainer_project_load(). + */ +function project_maintainer_project_load($nid) { + $maintainers = array(); + + // We don't want to load all the permissions here, just the ones that + // Project itself is responsible for, so we use our implementation of the + // hook, instead of the global load function. + $project_perms = project_project_permission_info(); + $query = db_query('SELECT u.name, pm.* FROM {project_maintainer} pm INNER JOIN {users} u ON pm.uid = u.uid WHERE pm.nid = %d ORDER BY u.name', $nid); + while ($maintainer = db_fetch_object($query)) { + $maintainers[$maintainer->uid]['name'] = $maintainer->name; + foreach ($project_perms as $perm_name => $perm_info) { + $db_field = str_replace(' ', '_', $perm_name); + $maintainers[$maintainer->uid]['permissions'][$perm_name] = $maintainer->$db_field; + } + } + + // Invoke hook_project_maintainer_project_load(). We can't use + // module_invoke_all() since we want a reference to the $maintainers array. + foreach (module_implements('project_maintainer_project_load') as $module) { + $function_name = $module . '_project_maintainer_project_load'; + $function_name($nid, $maintainers); + } + + return $maintainers; +} + +/** * Implement hook_load(). */ function project_project_load($node) { $additions = db_fetch_array(db_query('SELECT * FROM {project_projects} WHERE nid = %d', $node->nid)); $project = new stdClass; $project->project = $additions; + $project->project['maintainers'] = project_maintainer_project_load($node->nid); + return $project; } @@ -996,6 +1139,12 @@ function project_theme() { 'title' => NULL, ), ), + 'project_maintainers_form' => array( + 'file' => 'includes/project_maintainers.inc', + 'arguments' => array( + 'form' => NULL, + ), + ), 'project_project_node_form_taxonomy' => array( 'file' => 'project.inc', 'arguments' => array( Index: project.test =================================================================== RCS file: /Users/wright/drupal/local_repo/contributions/modules/project/project.test,v retrieving revision 1.4 diff -u -p -r1.4 project.test --- project.test 20 Apr 2010 23:23:56 -0000 1.4 +++ project.test 17 Aug 2010 16:11:50 -0000 @@ -12,6 +12,75 @@ class ProjectWebTestCase extends DrupalW // We can't call parent::setUp() with a single array argument, so we need // this ugly call_user_func_array(). call_user_func_array(array($this, 'parent::setUp'), $modules); + + $perms = array('maintain projects', 'access user profiles', 'access projects'); + + $this->owner = $this->drupalCreateUser($perms); + $this->drupalLogin($this->owner); + + $this->maintainer = $this->drupalCreateUser($perms); + } + + /** + * Assert that a field in the current page is enabled. + * @TODO Remove this when http://drupal.org/node/882564 is committed. + * + * @param $name + * name of field to assert. + * @param $message + * Message to display. + * @return + * TRUE on pass, FALSE on fail. + */ + function assertFieldEnabled($name, $message = '') { + $elements = $this->xpath('//input[@name="' . $name . '"]'); + return $this->assertTrue(isset($elements[0]) && empty($elements[0]['disabled']), $message ? $message : t('Field @name is enabled.', array('@name' => $name))); + } + + /** + * Assert that a field in the current page is disabled. + * @TODO Remove this when http://drupal.org/node/882564 is committed. + * + * @param $name + * name of field to assert. + * @param $message + * Message to display. + * @return + * TRUE on pass, FALSE on fail. + */ + function assertFieldDisabled($name, $message = '') { + $elements = $this->xpath('//input[@name="' . $name . '"]'); + return $this->assertTrue(isset($elements[0]) && !empty($elements[0]['disabled']), $message ? $message : t('Field @name is disabled.', array('@name' => $name))); + } + + /** + * Assert that a checkbox field in the current page is not checked. + * + * @param $name + * name of field to assert. + * @param $message + * Message to display. + * @return + * TRUE on pass, FALSE on fail. + */ + protected function assertNoFieldCheckedByName($name, $message = '') { + $elements = $this->xpath('//input[@name="' . $name . '"]'); + return $this->assertTrue(isset($elements[0]) && empty($elements[0]['checked']), $message ? $message : t('Checkbox field @id is not checked.', array('@id' => $name)), t('Browser')); + } + + /** + * Assert that a checkbox field in the current page is checked. + * + * @param $name + * name of field to assert. + * @param $message + * Message to display. + * @return + * TRUE on pass, FALSE on fail. + */ + protected function assertFieldCheckedByName($name, $message = '') { + $elements = $this->xpath('//input[@name="' . $name . '"]'); + return $this->assertTrue(isset($elements[0]) && !empty($elements[0]['checked']), $message ? $message : t('Checkbox field @id is checked.', array('@id' => $name)), t('Browser')); } /** @@ -310,3 +379,109 @@ class ProjectDrupalOrgWebTestCase extend } } } + +class ProjectMaintainersTestCase extends ProjectWebTestCase { + public static function getInfo() { + return array( + 'name' => 'Project maintainers functionality', + 'description' => 'Test Project module maintainers access control system.', + 'group' => 'Project' + ); + } + + function setUp() { + parent::setUp(); + } + + /** + * Test maintainer permissions. + */ + function testProjectMaintainerPermissions() { + // Create project, make sure Maintainers link is shown + $project = $this->createProject(); + + // Check that owner can access + $this->drupalGet("node/$project->nid/edit"); + $this->assertResponse(200, 'Project owner can edit project.'); + + // Check the maintainers tab is shown and owner is included correctly + $this->drupalGet("node/$project->nid"); + $this->assertLink(t('Maintainers'), 0, ('Maintainers tab is shown.')); + $this->drupalGet("node/$project->nid/maintainers"); + $this->assertLink($this->owner->name, 0, ('Project owner is displayed on form.')); + $this->assertFieldDisabled("maintainers[{$this->owner->uid}][permissions][administer settings]", 'Checkbox is disabled for project owner'); + $this->assertFieldDisabled("maintainers[{$this->owner->uid}][permissions][administer maintainers]", 'Checkbox is disabled for project owner'); + $this->assertFieldCheckedByName("maintainers[{$this->owner->uid}][permissions][administer settings]", 'Owners permissions are automatically granted'); + $this->assertFieldCheckedByName("maintainers[{$this->owner->uid}][permissions][administer maintainers]", 'Owners permissions are automatically granted'); + $this->assertNoRaw("node/$project->nid/maintainers/delete/{$this->owner->uid}", 'No delete link is displayed for the project owner.'); + + // Try to delete the owner anyway and make sure it fails. + $this->drupalGet("node/$project->nid/maintainers/delete/{$this->owner->uid}"); + $this->assertText("You can not delete the project owner ({$this->owner->name}) as a maintainer.", 'Project owner can not be deleted as a maintainer.'); + + // Verify that other users do not have access + $this->drupalLogin($this->maintainer); + $this->drupalGet("node/$project->nid/edit"); + $this->assertResponse(403, 'Project edit form is protected.'); + $this->drupalGet("node/$project->nid/maintainers"); + $this->assertResponse(403, 'Project maintainers form is protected.'); + $this->drupalGet("node/$project->nid/maintainers/delete/{$this->maintainer->uid}"); + $this->assertResponse(403, 'Project delete maintainer form is protected.'); + + // Add a new user and verify that they are added: + // Login as owner + $this->drupalLogin($this->owner); + // Add new user + $edit = array(); + $edit['new_maintainer[user]'] = $this->maintainer->name; + $this->drupalPost("node/$project->nid/maintainers", $edit, t('Update')); + $this->assertLink($this->maintainer->name, 0, 'New user is displayed on form correctly.'); + $this->assertNoFieldCheckedByName("maintainers[{$this->maintainer->uid}][permissions][administer settings]", 'Permissions not explicitly granted.'); + $this->assertNoFieldCheckedByName("maintainers[{$this->maintainer->uid}][permissions][administer maintainers]", 'Permissions not explicitly granted.'); + + // Test validation for adding a duplicate maintainer + $edit = array(); + $edit['new_maintainer[user]'] = $this->maintainer->name; + $this->drupalPost("node/$project->nid/maintainers", $edit, t('Update')); + $this->assertText("{$this->maintainer->name} is already a maintainer of this project.", 'Duplicate maintainers are not permitted.'); + + // Add permissions to user + $edit = array(); + $edit["maintainers[{$this->maintainer->uid}][permissions][administer settings]"] = TRUE; + $this->drupalPost("node/$project->nid/maintainers", $edit, t('Update')); + $this->assertFieldCheckedByName("maintainers[{$this->maintainer->uid}][permissions][administer settings]", 'Permissions are displayed correctly on maintainers form.'); + // Login as maintainer and check access + $this->drupalLogin($this->maintainer); + $this->drupalGet("node/$project->nid/edit"); + $this->assertResponse(200, 'User is correctly granted access to project edit form.'); + $this->drupalGet("node/$project->nid/maintainers"); + $this->assertResponse(403, 'Project maintainers form is protected.'); + $this->drupalGet("node/$project->nid/maintainers/delete/{$this->maintainer->uid}"); + $this->assertResponse(403, 'Project delete maintainer form is protected.'); + + // Have owner grant administer maintainers permission + $this->drupalLogin($this->owner); + // Add permissions to user + $edit = array(); + $edit["maintainers[{$this->maintainer->uid}][permissions][administer maintainers]"] = TRUE; + $this->drupalPost("node/$project->nid/maintainers", $edit, t('Update')); + $this->assertFieldCheckedByName("maintainers[{$this->maintainer->uid}][permissions][administer maintainers]", 'Permissions are displayed correctly on maintainers form.'); + // Login as maintainer and check access + $this->drupalLogin($this->maintainer); + $this->drupalGet("node/$project->nid/maintainers"); + $this->assertResponse(200, 'User is correctly granted access to project edit form.'); + + // Remove the user from the project + $this->drupalLogin($this->owner); + $this->drupalGet("node/$project->nid/maintainers/delete/{$this->maintainer->uid}"); + $this->assertText("Are you sure you want to delete {$this->maintainer->name} as a maintainer of {$project->title}?", 'Deletion page is displayed properly.'); + $this->drupalPost(NULL, array(), t('Delete')); + $this->assertText("Removed {$this->maintainer->name} as a maintainer.", 'Project maintainer successfully deleted.'); + // Verify that access has been removed + $this->drupalLogin($this->maintainer); + $this->drupalGet("node/$project->nid/edit"); + $this->assertResponse(403, 'Project edit form is protected.'); + $this->drupalGet("node/$project->nid/maintainers"); + $this->assertResponse(403, 'Project maintainers form is protected.'); + } +} Index: includes/project_maintainers.inc =================================================================== RCS file: includes/project_maintainers.inc diff -N includes/project_maintainers.inc --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ includes/project_maintainers.inc 17 Aug 2010 16:31:46 -0000 @@ -0,0 +1,266 @@ + t('User')); + foreach ($project_perms as $perm_name => $perm_info) { + $form['#header'][$perm_name] = array('data' => $perm_info['title']); + } + $form['#header']['operations'] = array('data' => t('Operations')); + + if (!empty($project->project['maintainers'])) { + foreach ($project->project['maintainers'] as $uid => $maintainer) { + $form['maintainers'][$uid] = array(); + $form['maintainers'][$uid]['name'] = array( + '#type' => 'value', + '#value' => $maintainer['name'], + ); + foreach ($project_perms as $perm_name => $perm_info) { + $form['maintainers'][$uid]['permissions'][$perm_name] = array( + '#type' => 'checkbox', + '#default_value' => !empty($maintainer['permissions'][$perm_name]), + ); + } + $form['maintainers'][$uid]['operations'] = array(); + if ($uid == $project->uid) { + // We special-case the project owner with disabled checkboxes. + foreach ($project_perms as $perm_name => $perm_info) { + $form['maintainers'][$uid]['permissions'][$perm_name]['#disabled'] = TRUE; + } + $form['maintainers'][$uid]['operations']['delete'] = array( + '#value' => t('locked'), + ); + } + else { + $form['maintainers'][$uid]['operations']['delete'] = array( + '#value' => l(t('delete'), "node/$project->nid/maintainers/delete/$uid"), + ); + } + } + } + + $form['new_maintainer'] = array(); + $form['new_maintainer']['user'] = array( + '#type' => 'textfield', + '#size' => 20, + '#maxlength' => 40, + '#autocomplete_path' => 'user/autocomplete', + ); + // we'll fill this in with a real value during validate() + $form['new_maintainer']['uid'] = array( + '#type' => 'value', + '#value' => 0, + ); + foreach ($project_perms as $perm_name => $perm_info) { + $form['new_maintainer']['permissions'][$perm_name] = array( + '#type' => 'checkbox', + ); + } + + $form['submit'] = array('#type' => 'submit', '#value' => t('Update')); + + return $form; +} + +/** + * Render the final markup for the project maintainers form. + * + * @param $form + * The fully-built form array for the project maintainers form. + * + * @return + * String containing the markup to output for the maintainers form. + * + * @see theme() + * @see project_maintainers_form() + */ +function theme_project_maintainers_form($form) { + $output = ''; + + $header = $form['#header']; + $rows = array(); + + // Render all the existing maintainers. + if (is_array($form['maintainers'])) { + foreach (element_children($form['maintainers']) as $uid) { + $row = array(); + $account = new stdClass; + $account->uid = $uid; + $account->name = $form['maintainers'][$uid]['name']['#value']; + $row[] = theme('username', $account); + foreach (element_children($form['maintainers'][$uid]['permissions']) as $perm) { + $row[] = drupal_render($form['maintainers'][$uid]['permissions'][$perm]); + } + $row[] = drupal_render($form['maintainers'][$uid]['operations']); + if ($form['#project']->uid == $uid) { + $owner_row = $row; + } + else { + $rows[] = $row; + } + } + } + + // Create the final row for adding a new maintainer. + $row = array(); + $row[] = drupal_render($form['new_maintainer']['user']); + foreach (element_children($form['new_maintainer']['permissions']) as $perm) { + $row[] = drupal_render($form['new_maintainer']['permissions'][$perm]); + } + $row[] = ''; // Empty cell for the 'Operations' column on new maintainers. + $rows[] = $row; + + // Always put the owner row at the top of the table. + $rows = array_merge(array($owner_row), $rows); + + // Although using named keys in the $header array makes this form easier to + // alter, theme_table() freaks out if the $header array has non-numeric + // keys. So we ditch the keys at this point to avoid notices. + $output .= theme('table', array_values($header), $rows); + + $project_perms = project_permission_load(); + $output .= '
'; + foreach ($project_perms as $perm => $perm_info) { + $output .= '
' . $perm_info['title'] . '
'; + $output .= '
' . $perm_info['description'] . '
'; + } + $output .= "
\n"; + + $output .= drupal_render($form); + return $output; +} + +/** + * Validation callback for the project maintainers form. + */ +function project_maintainers_form_validate($form, &$form_state) { + $new_maintainer = $form_state['values']['new_maintainer']; + if (!empty($new_maintainer['user'])) { + $user_result = db_fetch_object(db_query("SELECT name, uid FROM {users} WHERE name = '%s'", $new_maintainer['user'])); + if (empty($user_result->uid)) { + form_set_error('new_maintainer][user', t('%user is not a valid user on this site.', array('%user' => $new_maintainer['user'])), 'error'); + return; + } + if (!empty($form['#project']->project['maintainers'][$user_result->uid])) { + form_set_error('new_maintainer][user', t('%user is already a maintainer of this project.', array('%user' => $new_maintainer['user'])), 'error'); + return; + } + // Save the uid in the form so we don't have to look it up again at submit. + form_set_value($form['new_maintainer']['uid'], $user_result->uid, $form_state); + } + else { + foreach ($new_maintainer['permissions'] as $name => $value) { + if (!empty($value)) { + form_set_error('new_maintainer][user', t('You must specify a valid user name to grant permissions.')); + } + } + } +} + +/** + * Submit callback for the project maintainers form. + */ +function project_maintainers_form_submit($form, &$form_state) { + $project_nid = $form['#project']->nid; + + // Loop over all the maintainers and update their permissions accordingly. + if (!empty($form_state['values']['maintainers'])) { + foreach ($form_state['values']['maintainers'] as $uid => $maintainer) { + // Just to be extra safe, always give the project owner full permissions. + if ($uid == $form['#project']->uid) { + $perms = array_fill_keys(array_keys(project_permission_load()), 1); + } + else { + $perms = $maintainer['permissions']; + } + project_maintainer_save($project_nid, $uid, $perms); + } + } + + // See if we need to insert a record for a new maintainer. + if (!empty($form_state['values']['new_maintainer']['uid'])) { + project_maintainer_save($project_nid, $form_state['values']['new_maintainer']['uid'], $form_state['values']['new_maintainer']['permissions']); + } + +} + +/** + * Confirm form for removing a uid as a cvs maintainer from a given project. + */ +function project_maintainer_delete_confirm($form_state, $project, $user) { + if ($user->uid == $project->uid) { + drupal_set_message(t('You can not delete the project owner (!user) as a maintainer.', array('!user' => theme('username', $user))), 'error'); + return drupal_goto("node/$project->nid/maintainers/"); + } + + $form['nid'] = array('#type' => 'value', '#value' => $project->nid); + $form['uid'] = array('#type' => 'value', '#value' => $user->uid); + + return confirm_form($form, + t('Are you sure you want to delete !user as a maintainer of !project?', + array( + '!user' => theme('username', $user), + '!project' => l($project->title, "node/$project->nid"), + )), + "node/$project->nid/maintainers", + t('This action cannot be undone.'), + t('Delete'), + t('Cancel')); +} + +/** + * Delete the requested user as a maintainer. + * + * Invoked when the delete button on the confirm_form() page is pressed. + */ +function project_maintainer_delete_confirm_submit($form, &$form_state) { + $nid = $form_state['values']['nid']; + $uid = $form_state['values']['uid']; + $user = user_load(array('uid' => $uid)); + + project_maintainer_remove($nid, $uid); + + drupal_set_message(t('Removed !user as a maintainer.', array('!user' => theme('username', $user)))); + $form_state['redirect'] = "node/$nid/maintainers"; +} Index: release/project_release.install =================================================================== RCS file: /Users/wright/drupal/local_repo/contributions/modules/project/release/project_release.install,v retrieving revision 1.31 diff -u -p -r1.31 project_release.install --- release/project_release.install 7 Jun 2010 22:45:39 -0000 1.31 +++ release/project_release.install 17 Aug 2010 16:11:50 -0000 @@ -351,6 +351,35 @@ function project_release_schema() { 'expire' => array('expire') ), ); + + $schema['project_release_project_maintainer'] = array( + 'description' => t('Users who have various per-project maintainer permissions.'), + 'fields' => array( + 'nid' => array( + 'description' => t('Foreign key: {project_projects}.nid of the project.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'uid' => array( + 'description' => t('Foreign key: {users}.uid of a user with any project maintainer permissions.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'administer_releases' => array( + 'description' => t('Can this user create and administer releases for the given project.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('nid', 'uid'), + ); + return $schema; } @@ -706,3 +735,52 @@ function project_release_update_6009() { } return $ret; } + +/** + * Add the {project_release_project_maintainer} table. + */ +function project_release_update_6010() { + $ret = array(); + + $table = array( + 'description' => t('Users who have various per-project maintainer permissions.'), + 'fields' => array( + 'nid' => array( + 'description' => t('Foreign key: {project_projects}.nid of the project.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'uid' => array( + 'description' => t('Foreign key: {users}.uid of a user with any project maintainer permissions.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + 'administer_releases' => array( + 'description' => t('Can this user create and administer releases for the given project.'), + 'type' => 'int', + 'unsigned' => TRUE, + 'not null' => TRUE, + 'default' => 0, + ), + ), + 'primary key' => array('nid', 'uid'), + ); + db_create_table($ret, 'project_release_project_maintainer', $table); + + // Initially populate the table so that every project owner has full + // powers on their own projects. + $ret[] = update_sql("INSERT INTO {project_release_project_maintainer} (nid, uid, administer_releases) SELECT nid, uid, 1 FROM {node} WHERE type = 'project_project'"); + + // If CVS module is enabled, also populate the table from the + // {cvs_project_maintainers} table so that anyone with CVS access + // who is not the project owner can administer releases. + if (module_exists('cvs')) { + $ret[] = update_sql("INSERT INTO {project_release_project_maintainer} (nid, uid, administer_releases) SELECT cpm.nid, cpm.uid, 1 FROM {cvs_project_maintainers} cpm INNER JOIN {node} n ON cpm.nid = n.nid WHERE cpm.uid != n.uid"); + } + + return $ret; +} Index: release/project_release.module =================================================================== RCS file: /Users/wright/drupal/local_repo/contributions/modules/project/release/project_release.module,v retrieving revision 1.153 diff -u -p -r1.153 project_release.module --- release/project_release.module 8 Jul 2010 23:16:17 -0000 1.153 +++ release/project_release.module 17 Aug 2010 16:11:50 -0000 @@ -40,8 +40,8 @@ function project_release_menu() { 'title' => 'Releases', 'page callback' => 'project_release_project_edit_releases', 'page arguments' => array(1), - 'access callback' => 'node_access', - 'access arguments' => array('update', 1), + 'access callback' => 'project_user_access', + 'access arguments' => array(1, 'administer releases'), 'type' => MENU_LOCAL_TASK, 'file' => 'includes/project_edit_releases.inc', ); @@ -120,7 +120,7 @@ function project_release_access($op, $no // We can't just use project_project_access() here, since we // need to check access to the project itself, not the release // node, so we use the helper method and pass the project id. - return project_check_admin_access($node->project_release['pid']); + return project_user_access($node->project_release['pid'], 'administer releases'); case 'delete': // No one should ever delete a release node, only unpublish it. return FALSE; @@ -142,6 +142,49 @@ function project_release_node_info() { } /** + * Implement hook_project_permission_info() + */ +function project_release_project_permission_info() { + return array( + 'administer releases' => array( + 'title' => t('Administer releases'), + 'description' => t('Allows a user to create and update releases, and to control which branches are recommended or supported.'), + ), + ); +} + +/** + * Implement hook_project_maintainer_save() + */ +function project_release_project_maintainer_save($nid, $uid, $permissions = array()) { + db_query("UPDATE {project_release_project_maintainer} SET administer_releases = %d WHERE nid = %d AND uid = %d", !empty($permissions['administer releases']), $nid, $uid); + if (!db_affected_rows()) { + // If we didn't have a record to update, add this as a new maintainer. + db_query("INSERT INTO {project_release_project_maintainer} (nid, uid, administer_releases) VALUES (%d, %d, %d)", $nid, $uid, !empty($permissions['administer releases'])); + } +} + +/** + * Implement hook_project_maintainer_remove() + */ +function project_release_project_maintainer_remove($nid, $uid) { + db_query("DELETE FROM {project_release_project_maintainer} WHERE nid = %d and uid = %d", $nid, $uid); +} + +/** + * Implement hook_project_maintainer_project_load() + */ +function project_release_project_maintainer_project_load($nid, &$maintainers) { + $query = db_query('SELECT u.uid, u.name, prpm.administer_releases FROM {project_release_project_maintainer} prpm INNER JOIN {users} u ON prpm.uid = u.uid WHERE prpm.nid = %d', $nid); + while ($maintainer = db_fetch_object($query)) { + if (empty($maintainers[$maintainer->uid])) { + $maintainers[$maintainer->uid]['name'] = $maintainer->name; + } + $maintainers[$maintainer->uid]['permissions']['administer releases'] = $maintainer->administer_releases; + } +} + +/** * Implement of hook_form() for project_release nodes. */ function project_release_form(&$release, &$form_state) { @@ -609,7 +652,7 @@ function project_release_view($node, $te } // Display packaging errors to admins. - if (project_check_admin_access($node->project_release['pid'])) { + if (project_user_access($node->project_release['pid'], 'administer releases')) { $rows = array(); $result = db_query('SELECT * FROM {project_release_package_errors} WHERE nid = %d', $node->nid); $error = db_fetch_object($result); @@ -690,7 +733,7 @@ function project_release_get_releases($p $where = ''; $join = ''; $args = array($project->nid); - if (!project_check_admin_access($project)) { + if (!project_user_access($project, 'administer releases')) { if (!empty($rids)) { $where = "AND (n.status = %d OR n.nid IN (". db_placeholders($rids) ."))"; $args[] = 1; @@ -1230,7 +1273,7 @@ function project_release_project_page_li ), ); - if (project_check_admin_access($node->nid)) { + if (project_user_access($node->nid, 'administer releases')) { $links['project_release']['links']['add_new_release'] = l(t('Add new release'), 'node/add/project_release/'. $node->nid); $links['project_release']['links']['administer_releases'] = l(t('Administer releases'), 'node/'. $node->nid .'/edit/releases'); } Index: release/project_release.test =================================================================== RCS file: release/project_release.test diff -N release/project_release.test --- /dev/null 1 Jan 1970 00:00:00 -0000 +++ release/project_release.test 17 Aug 2010 16:21:04 -0000 @@ -0,0 +1,98 @@ + 'Project release maintainers functionality', + 'description' => 'Test Project release maintainers access control system.', + 'group' => 'Project Release' + ); + } + + function setUp() { + parent::setUp('project_release', 'views'); + } + + /** + * Test maintainer permissions. + */ + function testProjectMaintainerPermissions() { + // Create project, make sure Maintainers link is shown + $project = $this->createProject(); + + // Check that project_release permissions show up correctly + $this->drupalGet("node/$project->nid/edit"); + $this->assertResponse(200, 'Project owner can edit project.'); + + // Check the project release permissions appear correctly + $this->drupalGet("node/$project->nid/maintainers"); + $this->assertFieldCheckedByName("maintainers[{$this->owner->uid}][permissions][administer releases]", 'Checkbox is checked for project owner.'); + $this->assertFieldDisabled("maintainers[{$this->owner->uid}][permissions][administer releases]", 'Checkbox is disabled for project owner.'); + + // Make sure access is properly denied to start with + $this->drupalLogin($this->maintainer); + $this->drupalGet("node/$project->nid"); + $this->assertNoLink(t('Add new release')); + $this->assertNoLink(t('Administer releases')); + $this->drupalGet("node/$project->nid/edit/releases"); + $this->assertResponse(403, 'Administer releases form is properly protected.'); + $this->drupalGet("node/add/project-release/$project->nid"); + $this->assertResponse(403, 'Add new release form is properly protected.'); + + // Add permissions and check admin and new release pages again + $this->drupalLogin($this->owner); + $edit = array(); + $edit['new_maintainer[user]'] = $this->maintainer->name; + $edit['new_maintainer[permissions][administer releases]'] = TRUE; + $this->drupalPost("node/$project->nid/maintainers", $edit, t('Update')); + $this->assertFieldCheckedByName("maintainers[{$this->maintainer->uid}][permissions][administer releases]", 'Permissions are displayed correctly on maintainers form.'); + $this->drupalLogin($this->maintainer); + $this->drupalGet("node/$project->nid"); + $this->assertLink(t('Add new release')); + $this->assertLink(t('Administer releases')); + $this->drupalGet("node/$project->nid/edit/releases"); + $this->assertResponse(200, 'User is correctly granted access to project releases administration form'); + $this->drupalGet("node/add/project-release/$project->nid"); + $this->assertResponse(200, 'User is correctly granted access to add new project releases.'); + + // Create a project release and check update access + $edit = array(); + $edit['project_release[version_major]'] = '1'; + $edit['project_release[version_patch]'] = '2'; + $edit['project_release[version_extra]'] = 'beta'; + $edit['body'] = $this->randomString(128); + $this->drupalPost("node/add/project-release/$project->nid", $edit, t('Save')); + $release = $this->drupalGetNodeByTitle("{$project->project['uri']} 1.2-beta"); + $this->drupalGet("node/$release->nid/edit"); + $this->assertResponse(200, 'User is correctly granted permission to edit release.'); + + // Add errors to project_release_package_errors and check that they are shown + $errors = new stdClass; + $errors->nid = $release->nid; + $error_messages = array($this->randomName(), $this->randomName(), $this->randomName()); + $errors->messages = serialize($error_messages); + $success = drupal_write_record('project_release_package_errors', $errors); + $this->drupalGet("node/$release->nid"); + $this->assertText(t('Packaging error messages'), 'Packaging error messages shown correctly.'); + foreach ($error_messages as $message) { + $this->assertText($message); + } + + // Remove the permissions and check access + $this->drupalLogin($this->owner); + $edit = array(); + $edit["maintainers[{$this->maintainer->uid}][permissions][administer releases]"] = FALSE; + $this->drupalPost("node/$project->nid/maintainers", $edit, t('Update')); + $this->assertNoFieldCheckedByName("maintainers[{$this->maintainer->uid}][permissions][administer releases]", 'Permissions are displayed correctly on maintainers form.'); + $this->drupalLogin($this->maintainer); + $this->drupalGet("node/$project->nid/edit/releases"); + $this->assertResponse(403, 'Administer releases form is properly protected.'); + $this->drupalGet("node/add/project-release/$project->nid"); + $this->assertResponse(403, 'Add new release form is properly protected.'); + $this->drupalGet("node/$release->nid/edit"); + $this->assertResponse(403, 'User is correctly denied permission to edit release.'); + $this->drupalGet("node/$release->nid"); + $this->assertNoText(t('Packaging error messages'), 'Packaging error messages not shown.'); + } +} Index: release/includes/release_node_form.inc =================================================================== RCS file: /Users/wright/drupal/local_repo/contributions/modules/project/release/includes/release_node_form.inc,v retrieving revision 1.11 diff -u -p -r1.11 release_node_form.inc --- release/includes/release_node_form.inc 30 Jan 2010 02:33:40 -0000 1.11 +++ release/includes/release_node_form.inc 17 Aug 2010 16:11:50 -0000 @@ -24,7 +24,7 @@ function _project_release_form(&$release } // Make sure this user should have permissions to add releases for // the requested project - if (!project_check_admin_access($project)) { + if (!project_user_access($project, 'administer releases')) { drupal_access_denied(); module_invoke_all('exit'); exit;